开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

JVM:类加载器 概述 类加载规则 类加载器种类 类加载器选

发表于 2024-03-10

概述

  • 类加载器是一个负责加载类的对象,用于实现类加载过程中的加载这一步。
  • 每个 Java 类都有一个引用指向加载它的 ClassLoader。
  • 数组类不是通过 ClassLoader 创建的(数组类没有对应的二进制字节流),是由 JVM 直接生成的。

简单来说,类加载器的主要作用就是加载 Java 类的字节码( .class 文件)到 JVM 中(在内存中生成一个代表该类的 Class 对象)。

类加载规则

JVM 启动的时候,并不会一次性加载所有的类,而是根据需要去动态加载。也就是说,大部分类在具体用到的时候才会去加载,这样对内存更加友好。

对于已经加载的类会被放在 ClassLoader 中。

  • 在类加载的时候,系统会首先判断当前类是否被加载过。
  • 已经被加载的类会直接返回,否则才会尝试加载。

也就是说,对于一个类加载器来说,相同二进制名称的类只会被加载一次。

1
2
3
4
5
6
7
8
9
10
11
java复制代码public abstract class ClassLoader {
...
private final ClassLoader parent;
// 由这个类加载器加载的类。
private final Vector<Class<?>> classes = new Vector<>();
// 由VM调用,用此类加载器记录每个已加载类。
void addClass(Class<?> c) {
classes.addElement(c);
}
...
}

类加载器种类

JVM 中内置了三个重要的 ClassLoader:

  • BootstrapClassLoader(启动类加载器) :最顶层的加载类,没有父级。Bootstrap ClassLoader是C++写的,它本身是虚拟机的一部分,并不是一个JAVA类,也就是无法在java代码中获取它的引用(其他所有的类加载器都是在 JVM 外部实现的,并且全都继承自 ClassLoader抽象类)。该加载器主要用来加载 JDK 内部的核心类库,包括:
    • %JAVA_HOME%/lib目录下的 rt.jar(Java 基础类库)、resources.jar、charsets.jar等 jar 包和类)。
    • 被 -Xbootclasspath参数指定的路径下的所有类。
  • ExtensionClassLoader(扩展类加载器) :
    • 主要负责加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类
    • 以及被 java.ext.dirs 系统变量所指定的路径下的所有类。
  • AppClassLoader(应用程序类加载器) :面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。

除了这三种类加载器之外,用户还可以加入自定义的类加载器来进行拓展,以满足自己的特殊需求。就比如说,我们可以对 Java 类的字节码( .class 文件)进行加密,加载时再利用自定义的类加载器对其解密。

案例:获取ClassLoader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码public class PrintClassLoaderTree {

public static void main(String[] args) {

ClassLoader classLoader = PrintClassLoaderTree.class.getClassLoader();

StringBuilder split = new StringBuilder("|--");
boolean needContinue = true;
while (needContinue){
System.out.println(split.toString() + classLoader);
if(classLoader == null){
needContinue = false;
}else{
classLoader = classLoader.getParent();
split.insert(0, "\t");
}
}
}

}

输出结果

1
2
3
kotlin复制代码|--sun.misc.Launcher$AppClassLoader@18b4aac2
|--sun.misc.Launcher$ExtClassLoader@53bd815b
|--null

结论:

  • 我们编写的 Java 类 PrintClassLoaderTree 的 ClassLoader 是AppClassLoader;
  • AppClassLoader的父 ClassLoader 是ExtClassLoader;
  • ExtClassLoader的父ClassLoader是Bootstrap ClassLoader,因此输出结果为 null。

类加载器选择规则

双亲委派模型概述

类加载器有很多种,当我们想要加载一个类的时候,具体是哪个类加载器加载呢?这就需要提到双亲委派模型了。

  • ClassLoader 类使用委托模型来搜索类和资源。
  • 双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。
  • ClassLoader 实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载器。
  • bootstrap class loader的内置类加载器本身没有父类加载器,但是可以作为 ClassLoader 实例的父类加载器。

下图中的类加载层次图就是双亲委派模型。
image.png
(图源:javaguide.cn)

双亲委派模型执行流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
java复制代码protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
//首先,检查该类是否已经加载过
Class c = findLoadedClass(name);
if (c == null) {
//如果 c 为 null,则说明该类没有被加载过
long t0 = System.nanoTime();
try {
if (parent != null) {
//当父类的加载器不为空,则通过父类的loadClass来加载该类
c = parent.loadClass(name, false);
} else {
//当父类的加载器为空,则调用启动类加载器来加载该类
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//非空父类的类加载器无法找到相应的类,则抛出异常
}

if (c == null) {
//当父类加载器无法加载时,则调用findClass方法来加载该类
//用户可通过覆写该方法,来自定义类加载器
long t1 = System.nanoTime();
c = findClass(name);

//用于统计类加载器相关的信息
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
//对类进行link操作
resolveClass(c);
}
return c;
}
}

总结:

  • 在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载(每个父类加载器都会走一遍这个流程)。
  • 类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器 loadClass()方法来加载类)。这样的话,所有的请求最终都会传送到顶层的启动类加载器 BootstrapClassLoader 中。
  • 只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载(调用自己的 findClass() 方法来加载类)。
  • 如果子类加载器也无法加载这个类,那么它会抛出一个 ClassNotFoundException 异常。

🌈 拓展一下:

JVM 判定两个 Java 类是否相同的具体规则:JVM 不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即使两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相同。

双亲委派模型的好处

  • 双亲委派模型可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类)
  • 也保证了 Java 的核心 API 不被篡改。

如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如:

  • 我们编写一个称为 java.lang.String 类的话,那么程序运行的时候,系统就会出现两个不同的 String 类。
  • 双亲委派模型可以保证加载的是 JRE 里的那个 String 类,而不是你写的 String 类。
  • 这是因为 AppClassLoader 在加载你的 String 类时,会委托给 ExtClassLoader 去加载,而 ExtClassLoader 又会委托给 BootstrapClassLoader,BootstrapClassLoader 发现自己已经加载过了 String 类,会直接返回,不会去加载你写的 String 类。

参考文章

  1. javaguide.cn
  2. JVM - 类加载器(ClassLoader) 原理 详解

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

别让 Docker 毁了你的 MySQL!

发表于 2024-03-10

近年来,随着 Docker 容器技术的火热,越来越多的应用开始被”装箱”。我们这些开发者享受着 Docker 带来的便捷和灵活,几乎恨不得把所有东西都塞进容器里。于是,连 MySQL 这样的有状态服务,也开始频频出现在 Docker 的舞台上。

但是,把 MySQL 装进 Docker,真的是个明智的选择吗?是否会给系统稳定性和数据安全带来隐患?这个问题在社区里引发了激烈的讨论。

“Docker 与有状态服务无缘”派

持反对意见的一派认为,所有有状态的服务,都不应该放在 Docker 里跑。他们列举了 MySQL、Redis、Elasticsearch、ActiveMQ 等服务作为例子,理由是这些服务都需要保证高可用和稳定性,而这恰恰是 Docker 难以做到的。

即便是在 Kubernetes 这样的容器编排平台中,他们也不建议把有状态服务放进去。这也是为什么云厂商会专门提供 MySQL、Redis、MQ 等服务的原因。

那么,究竟是什么原因让”Docker 与有状态服务无缘”呢?

首先,传统的高可用方案在容器中难以实现。以 MySQL 为例,传统的 HA 方案通常是两台机器+共享存储+Fence 设备。

但在 Kubernetes 中,这种方案如何配置?Fence 设备又该如何在容器中实现?这些都是没有现成答案的问题。

其次,一些 MySQL 的高可用和集群方案与 Docker 的理念不太兼容。

比如,MySQL 8.0 的 InnoDB Cluster 通常采用 1 主 2 从的模式,这在 Kubernetes 中如何实现?虽然有 MySQL Operator 这样的尝试,但成熟度和可行性还有待验证。

最后,对于 DBA 来说,在虚拟机上部署 MySQL 才是最熟悉和放心的方式。

毕竟数据库最需要的是稳定,调整 CPU、内存、磁盘等参数,再加上定期备份,就可以做到大多数时候”甩手掌柜”。

如果是在公有云上,那就更应该直接用云厂商提供的 MySQL 服务。

在他们看来,Docker 适合运行无状态服务。因为无状态服务挂了就拉起来,不需要做额外的检查,而有状态服务则可能由于数据不一致而带来新的问题。

“MySQL 照样可以容器化”派

但支持在 Docker 中运行 MySQL 的声音也不小。毕竟,所有主流数据库都已经提供了官方镜像,多数也有容器化的集群方案。他们认为,只要在部署时遵循一些最佳实践,就可以充分发挥 Docker 的优势,而不必担心稳定性和性能问题。

首先,在部署 MySQL 容器时,一定要记得挂载数据卷(Data Volume) 。通过数据卷,可以将容器内的数据目录映射到宿主机上,这样不仅可以避免容器删除时数据丢失,还能减少额外的 I/O 开销。例如,如果我们希望将容器内的 /var/lib/mysql 目录(MySQL 默认的数据目录)映射到宿主机的 /my/own/datadir 目录,可以在启动容器时使用 -v 参数:

1
bash复制代码docker run --name some-mysql -v /my/own/datadir:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:tag

除了手动指定目录,我们也可以使用 Docker 的命名卷(Named Volume)功能。命名卷可以由 Docker 来管理,不需要我们关心具体的存储位置:

1
bash复制代码docker run --name some-mysql -v mysql-data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:tag

如果要实现 MySQL 的配置文件外挂,也可以通过数据卷来实现:

1
bash复制代码docker run --name some-mysql -v /my/own/datadir:/var/lib/mysql -v /my/custom/mysql.cnf:/etc/mysql/conf.d/mysql.cnf -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:tag

其次,使用容器部署 MySQL 有诸多优点。容器可以封装不同版本的 MySQL,而不用担心与宿主机的兼容性问题。同时,由于容器之间是隔离的,可以保证 MySQL 不会与其他服务产生干扰。此外,通过容器还能在单机上部署多个 MySQL 实例,提高资源利用率。

至于性能问题,支持者认为在正确使用数据卷的情况下,MySQL 容器的性能损失微乎其微。毕竟容器只是一层轻量级的封装,并不会对 I/O、网络等产生实质影响。

在 Kubernetes 中部署有状态服务,通常需要用到 StatefulSet 和 PersistentVolumeClaim(PVC) 。StatefulSet 用于管理有状态的应用,它为每个 Pod 提供一个唯一且固定的标识符。PVC 则用于申请持久化存储。下面是一个简单的 StatefulSet 示例,用于部署一个单节点的 MySQL:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
yaml复制代码apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
serviceName: mysql
replicas: 1
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:5.7
env:
- name: MYSQL_ROOT_PASSWORD
value: my-secret-pw
volumeMounts:
- name: data
mountPath: /var/lib/mysql
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10Gi

在这个例子中,StatefulSet 会自动为 MySQL 的 Pod 创建一个专属的 PVC,并将其挂载到容器的 /var/lib/mysql 目录。这样,即使 Pod 重建,它的数据也不会丢失。

当然,这只是一个最简单的示例。在实际使用中,我们可能还需要配置 MySQL 的主从复制、读写分离等,这就需要更复杂的 StatefulSet 和 Service 配置了。

求同存异,因地制宜

对于我这个又做开发又做 DevOps 的过来人觉得,MySQL 是否适合容器化,还是要具体问题具体分析。正如 Stackoverflow 上一位答主所说,

技术只是达成目标的工具,适合自己的才是最好的。

对于开发和测试环境,或者对性能和稳定性要求不高的场景,Docker 化的 MySQL 确实能带来不少便利。开发者可以快速搭建数据库,方便地进行各种实验和测试。

但对于生产环境尤其是核心系统的数据库,稳定性和数据安全性无疑是第一位的,这时谨慎一些,采用成熟的方案或许更有保障。

同时也要认识到,容器技术正在飞速发展,很多以前难以想象的场景,现在都已逐渐成为可能。比如有了 Kubernetes、Operator 这样的云原生技术,有状态服务在容器中的管理已经比以前简单很多。未来会怎样,现在下定论还为时尚早。

在 Docker 和 Kubernetes 中运行有状态服务,关键是要充分利用数据卷和 PVC,确保数据的持久性和可恢复性。同时,也要根据实际需求,合理配置服务的高可用和负载均衡。

或许,比较理想的方式是在掌握传统运维的基础上,多了解和学习云原生的新技术,根据自己的实际情况分析利弊,然后再决定是否要上容器的”车”。

与其教条地说”必须用”或”绝不用”,不如开放一些,拥抱变化。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

什么是移动计算?中国Java之父余胜军被刷爆的CDN又是什么

发表于 2024-03-10

移动计算

移动计算通常指的是将**计算任务从其原始位置转移到距离数据存储地更近的地点**进行处理的概念。这个概念基于一个核心原则:数据传输的成本(包括时间、带宽和资源消耗)通常高于将计算任务移动到数据所在位置的成本。因此,移动计算到数据附近可以提高效率,减少延迟,并优化资源使用。

理解移动计算

  1. 减少数据传输:在大数据环境中,数据集可以非常庞大,将这些数据从存储位置传输到计算位置可能会消耗大量的网络带宽,并增加延迟。如果计算任务移动到数据所在地,可以显著减少需要传输的数据量。
  2. 优化资源使用:通过在数据所在地进行计算,可以更有效地利用那里的计算资源,减少整个系统的资源浪费。
  3. 减少延迟:计算过程中的数据访问如果发生在本地,可以避免网络延迟,加快计算速度。
  4. 增加灵活性和可扩展性:分布式计算环境,如在Hadoop、Spark等大数据处理框架中,本质上就是移动计算的实践。这些框架将**计算任务分散到数据所在的节点**上,从而提高处理效率,并增加系统的可扩展性。

应用实例

  • 分布式数据处理:如Hadoop的MapReduce和Spark的RDD(弹性分布式数据集)都是基于将计算移动到数据附近的原则设计的。在这些系统中,计算任务会分配到存储相应数据块的节点上执行。
  • 边缘计算:在物联网(IoT)环境中,边缘计算是一种将数据处理任务移动到网络边缘的设备上的策略,从而减少数据传输到云或中心数据中心的需要,降低延迟,并快速响应。
  • 数据缓存和内容分发网络(CDN):这些技术通过将数据和计算任务移动到用户附近的节点来提高数据检索和服务交付的速度。

移动计算的核心思想是将计算任务靠近数据的存储位置,从而减少数据传输的成本,提高计算效率。这是现代分布式系统设计的一个关键原则,特别适用于处理大规模数据集的场景。

数据缓存和内容分发网络(CDN)

数据缓存和内容分发网络(CDN)是两种常用的技术,用于提高数据检索速度和优化服务交付。这两种技术通过将数据和计算任务靠近用户来减少延迟和提高效率。

数据缓存

数据缓存是一种存储技术,它将频繁访问的数据保存在快速访问的存储层中,从而加快数据检索速度和减少对原始数据源的访问压力。

  • 工作原理:当数据第一次被请求时,它会被加载到缓存中。后续的请求可以直接从缓存中获取数据,而不是每次都从原始数据源(如数据库或磁盘)读取。
  • 应用场景:数据缓存广泛应用于数据库、网站和应用程序中,以提高数据检索速度和降低后端系统的负载。

内容分发网络(CDN)

CDN是一种分布式网络服务,它通过在全球多个地理位置部署节点来缓存和传递内容(如网页、视频、图像和其他文件),从而加快内容的加载时间和提高用户体验。

  • 工作原理:
+ **数据复制**:CDN将内容复制到多个分布在不同地理位置的服务器上。这样,**用户的请求可以被路由到最近的服务器**,从而减少数据传输的距离和时间。
+ **智能路由**:CDN使用智能路由技术确定哪个服务器最接近用户,确保用户从最近的节点获取内容,减少延迟。
+ **负载均衡**:CDN还实现**负载均衡**,自动将用户请求分配到多个服务器,避免任何单个服务器的过载。
  • 应用场景:
+ 加速网站和网页内容的加载。
+ 提供大规模的视频流和媒体分发。
+ 优化大型文件的下载速度。
+ 增强大型在线应用的性能和可靠性。

CDN与数据缓存的联系

CDN可以视为一种特殊的数据缓存,它在全球范围内缓存数据和内容,以优化内容的分发和访问速度。数据缓存通常指的是更局部的数据存储优化,比如单个系统或应用程序内的缓存,而CDN提供的是全球分布的缓存和内容分发服务。

数据缓存和CDN都是为了减少延迟和提高数据检索及内容交付的速度。数据缓存通常用于提高特定应用或服务的性能,而CDN则用于全球范围内的内容分发,帮助用户更快地获取和加载数据和媒体内容。通过将数据和计算任务移动到用户附近,这些技术显著提高了数据检索的效率和用户体验。

余胜军开了个网站,说CDN被刷爆了,他是什么意思?

余胜军提到的“CDN被刷爆了”是指他的网站遭受了大量的访问请求,这些请求超出了为网站配置的CDN服务的处理能力,导致CDN资源过度使用,甚至可能出现服务暂时不可用的情况。这通常发生在以下几种情况中:

  • DDoS攻击:分布式拒绝服务(DDoS)攻击会发起大量的请求到目标网站,意图耗尽网站的资源,包括CDN的缓存和带宽资源。如果攻击流量过大,可能会导致CDN服务“爆满”,影响正常用户的访问。
  • 意外的流量峰值:网站可能因为某些活动、事件或热门内容突然获得大量访问,这些意外的流量峰值有时会超过CDN服务的预设限额或处理能力。
  • 缓存策略不当:如果CDN的缓存策略配置不恰当(例如缓存时间过短或不缓存重要内容),可能导致CDN不能有效减轻后端服务器的负载,从而在大量访问请求下压力过大。
  • CDN带宽限制:如果CDN服务的带宽有上限,当网站流量突破这一上限时,CDN服务可能无法处理更多的请求,导致网站访问速度变慢或服务不可用。

解决措施

  • 增加CDN资源:提高CDN服务的带宽和资源上限,确保可以处理更高的流量。
  • 优化缓存策略:调整CDN的缓存规则,确保高访问频率的内容被有效缓存。
  • DDoS防护:使用专业的DDoS防护服务来减轻或阻断恶意流量。

什么叫CDN资源被过度使用?

CDN资源包括带宽、服务器处理能力、存储空间等,这些都有可能被过度使用。我们来具体分析这些概念:

CDN服务/资源的组成

网络资源、CDN边缘节点的存储和计算能力:

  • 带宽:CDN分配给客户的网络带宽是有限的。如果访问量极大,超出了分配的带宽,就会导致带宽被过度使用。
  • 服务器处理能力:CDN的每个边缘节点都有处理请求的能力限制。如果请求量过大,超过节点的处理能力,节点可能会变得响应缓慢或无响应。
  • 存储空间:CDN缓存的内容存储在边缘节点上,这些节点的存储空间是有限的。过多的缓存需求可能导致存储空间不足。

过度使用的理解

  • 硬件与服务结合考虑:CDN的运行依赖于底层的硬件(如服务器、网络设备等),服务/资源的过度使用实质上是硬件资源使用到极限。因此,在考虑CDN资源被过度使用时,确实需要将其附属的硬件考虑在内。
  • 抽象与具体的结合:从用户角度看,CDN服务是一个抽象的概念,它通过网络提供内容分发和加速服务。但从实现角度,CDN服务是建立在具体硬件资源上的,包括具体的服务器、网络连接和存储设备。因此,CDN服务/资源既有抽象的层面,也有具体的物理实现。

管理CDN资源的过度使用

  • 监控与调整:通过持续监控CDN的使用情况,可以及时发现资源使用的高峰和瓶颈,据此调整配置,比如增加带宽、扩展存储空间或优化内容分发策略。
  • 按需扩展:采用弹性的资源分配模式,如根据流量自动调整CDN资源,可以更有效地应对流量波动,避免资源过度使用。

CDN服务/资源的过度使用是一个既具体又抽象的概念。它具体表现为CDN依赖的硬件资源(如带宽、服务器处理能力、存储空间)被大量使用到接近或达到其极限。理解和管理CDN资源的过度使用需要从资源监控、配置优化和按需扩展等多个角度综合考虑。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Go如何解析不定JSON数据

发表于 2024-03-09

前言

在开发中常常会碰到很多JSON类型的数据进行交互,而其中有很多JSON数据你是不能确定它的字段和结构的,而Go语言是一门静态强类型的语言,在进行JSON解析的时候必须要确定字段的类型,定义出对应的结构体,然后再进行Unmarshal,那这二者之间的冲突我们该如何解决呢?

什么是JSON

  • json是JavaScript Object Notation(JavaScript对象表示法)
  • json是轻量级的文本数据交换格式
  • json独立于语言
  • json具有自我描述性,更容易理解
  • json使用js语法来描述数据对象,但是json仍然独立于语言和平台,json解析器和json库支持许多不同的编程语言

json是一种轻量级的数据交换格式,易于人阅读和编写,同时也易于机器解析和生成,之所以json这么流行,是因为json的结构和多级结构体(对象)刚好能对应上,并且本身也十分易读。而前后端交互的时候后端通常会返回给前端一个多级的结构体,于是json慢慢开始流行了,且json是跨语言和跨平台的,自身也足够轻量级。

json的几种标准格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
css复制代码一个标准的json数据
//每个key对应的是一个value
{
“k1": 1,
"k2": 2 //注意结尾的这个不能有逗号
}



json字符串
{
"k1": "1",
"k2": "2"
}


json数组
{
“k1”: [1,2],
“k2”: [3,4]
}


json对象
{
“k1”: {“1”: “haihai”},
“k2”: {“2”:”haihahai”}
}


json对象数组
{
“k1”: [
{“k11”: “hellohello”},
{“k12”: “badbad”}
]
}



json数组对象
{
“k2”: {
“hello”: [1,2,3]
}
}

所有的JSON数据都是由上述几种JSON数据组合而成

如何在Go中解析不确定的JSON数据

通过看文档的方式去确定对应的JSON数据,然后构造对应的结构体

这是最靠谱的方式,最合理也是效率最高的方式。

1
2
3
4
5
6
7
8
go复制代码// 请求其他服务   
jsonStr := xxx

var data interface{}

err := json.Unmarshal([]byte(jsonStr),&data)

fmt.Println(data)

比如可以先拿一个interface{}类型来接住JSON数据,然后看这个interface{}的值,来确定这个JSON数据哪些字段是string 哪些是object 哪些是int float等等

当然这也不是完全适用的,比如下面这种情况,有一个字段如下

type : []

能看出来type是一个切片类型的值,但是具体的类型你并不知道,可能是[]int 也有可能是[]string []float等等

map[string] interface{}

这个类型是map键值对,值可以是任意类型,因为在go中任意类型都实现了空接口interface{},而json数据也是key value的键值对,所以map[string] interface{}天然支持解析json类型数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go复制代码jsonStr := xxx
var data map[string]interface{}
err := json.Unmarshal([]byte(jsonStr),&data)

// 你想取的字段
fieldValue := data["field"]

// 类型断言
if value,ok := data["field"].(float64);ok {

} else if vluae,ok := data["field"].(int64); ok {

}

理论上所有的合法的JSON数据都可以被反序列化到map[string]interface{}中
但是实际应用中 可能会出现一些无法被map[string]interface{}解析的JSON数据
  • JSON 数据中包含了多层嵌套的数据结构。在这种情况下,如果没有使用递归或者其他方式对嵌套数据进行处理,可能会导致反序列化失败。
  • JSON 数据中包含了数组类型,但是数组元素类型不一致或者无法转换成相应的类型。在这种情况下,可能需要手动处理数组元素或者使用其他数据类型来保存数组数据。
  • JSON 数据中包含了自定义数据类型或者复杂的数据结构,无法使用 map[string]interface{} 类型来反序列化。在这种情况下,需要定义相应的结构体或者使用其他适合的数据类型来反序列化。

第三方库

除了encoding/json之外,还有很多第三方库可以用来解析不确定的JSON数据,例如gjson和jsonparser,这些库通常提供了更加灵活和高效的JSON解析方式,可以根据具体的需求选择合适的库来使用

json.RawMessage与json.Number

  • json.RawMessage 是一个非常高效的数据类型,因为她不需要进行任何解析和类型转换,直接保存了未经处理的原始JSON数据,在反序列化的时候只需要将json.RawMessage转化为对应的数据类型即可,无需重新解析JSON数据
  • json.Number 表示JSON中的数字类型,可以用来保存任意精度的数字。这个数字可以特别大,可能会无法用Go中的整数或者浮点数来表示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
go复制代码package main

import (
"encoding/json"
"fmt"
)

func main() {
jsonData := []byte(`{
"id": 12345,
"name": "John Doe",
"age": 30,
"score": 95.5,
"is_student": true,
"tags": ["tag1", "tag2", "tag3"],
"extra": {
"field1": "value1",
"field2": 123
}
}`)

var m map[string]json.RawMessage
err := json.Unmarshal(jsonData, &m)
if err != nil {
panic(err)
}

var id int
err = json.Unmarshal(m["id"], &id)
if err != nil {
panic(err)
}
fmt.Printf("id: %d\n", id)

var name string
err = json.Unmarshal(m["name"], &name)
if err != nil {
panic(err)
}
fmt.Printf("name: %s\n", name)

var age int
err = json.Unmarshal(m["age"], &age)
if err != nil {
panic(err)
}
fmt.Printf("age: %d\n", age)

var score float64
err = json.Unmarshal(m["score"], &score)
if err != nil {
panic(err)
}
fmt.Printf("score: %f\n", score)

var isStudent bool
err = json.Unmarshal(m["is_student"], &isStudent)
if err != nil {
panic(err)
}
fmt.Printf("is_student: %v\n", isStudent)

var tags []string
err = json.Unmarshal(m["tags"], &tags)
if err != nil {
panic(err)
}
fmt.Printf("tags: %v\n", tags)

var extra map[string]json.RawMessage
err = json.Unmarshal(m["extra"], &extra)
if err != nil {
panic(err)
}
var field1 string
err = json.Unmarshal(extra["field1"], &field1)
if err != nil {
panic(err)
}
fmt.Printf("extra.field1: %s\n", field1)

var field2 int
err = json.Unmarshal(extra["field2"], &field2)
if err != nil {
panic(err)
}
fmt.Printf("extra.field2: %d\n", field2)
}

// 不确定的类型
data := make(map[string]interface{})
if err := json.Unmarshal(rawData, &data); err != nil {
log.Fatal(err)
}

if value, ok := data["age"].(float64); ok {
// 处理年龄为浮点数的情况
} else if value, ok := data["age"].(int); ok {
// 处理年龄为整数的情况
} else {
// 处理年龄为其他类型或不存在的情况
}

需要注意的是:类型断言的底层为反射,因为在运行时需要判断一个接口值的具体类型,而这个类型是在编译时无法确定的,需要在运行时动态地获取。效率比正常的代码低一到两个数量级,而且需要消耗额外的时间和内存。

推荐阅读

当说到云原生时,我们究竟在谈论什么? - 掘金

不太熟悉Git? 不妨看看这篇文章 - 掘金

一文搞定常见分布式事务实现 - 掘金

你真的理解分布式理论吗? - 掘金

深入了解异地多活 - 掘金

02.K8S架构详解 - 掘金

01.你为什么需要学习K8S - 掘金

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

面试准备不充分,被Java守护线程干懵了,面试官主打一个东西

发表于 2024-03-09

写在开头

面试官:小伙子请聊一聊Java中的精灵线程?
我:什么?精灵线程?啥时候精灵线程?
面试官:精灵线程没听过?那守护线程呢?
我:守护线程知道,就是为普通线程服务的线程嘛。
面试官:没了?守护线程的特点,怎么使用,需要注意啥,Java中经典的守护线程都有啥?
我:不知道。。。

这的天,面试一个10K的工作,上来先整个精灵线程,直接把人整蒙了,难道提及Java多线程的时候,问的不应该是线程、线程池、并发冲突解决方案、如何加锁,以及各种锁的知识点吗?上来整个偏门的守护线程,这是出心的不想要啊。

何为守护线程

上面这段内容是在牛客上看到的,说实话这位面试官问的这内容确实主要一个:东西没用,但你得知道!可如果说他问的真是离谱吗?也算不上,精灵线程我们很少听到,但守护线程我们在学习Java线程的时候肯定有所耳闻!那么今天我们就一起来小酌一下这个 “ 守护线程 ”

Java中的线程分为2种:用户线程和守护线程

用户线程又叫普通线程,是我们驱动业务逻辑运转的核心;而守护线程,顾名思义,是守护用户线程的一种线程,运行在后台提供通用服务,因此也叫后台线程或者精灵线程。

守护线程的使用场景

那在Java中这个守护线程都有什么实际用处,或者说应用场景呢?

  1. GC垃圾回收线程:这是JVM中非常经典的一个守护线程,它始终以低级别状态运行,用于实时监控和管理系统中的可回收资源,一旦我们的系统没有任何运行的用户线程时,程序也就不会再产生垃圾,这时,无事可做的垃圾回收线程会自动结束。
  2. 应用指标统计:部分服务可以通过守护线程来采取应用指标,服务结束则停止采集。

怎么设置守护线程

那我们在代码中,如何将一个线程设置为守护线程呢?咱们可以通过在 start 线程之前调用线程的 setDaemon(true) 方法,将一个线程设置为守护线程,来看一下下面的这个demo。

【代码实例1】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
java复制代码public class Test {
public static void main(String[] args) {
Thread thread1 = new Thread("守护线程"){
@Override
public void run() {
int i = 0;
while (i <= 4){
i++;
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":"+i);
}
super.run();
}
};
Thread thread2 = new Thread("用户线程"){
@Override
public void run() {
int i = 0;
while (i < 2){
i++;
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":"+i);
}
super.run();
}
};
//setDaemon, 不设置则默认false
thread1.setDaemon(true);//设置thread1为守护线程
thread2.setDaemon(false);//设置thread2为普通线程
thread1.start();
thread2.start();
}
}

输出:

1
2
3
4
java复制代码守护线程:1
用户线程:1
用户线程:2
守护线程:2

这段测试代码中,我们通过thread1.setDaemon(true)将线程1设置成了一个守护线程(false为普通线程),用户线程的循环次数为2,用户线程的循环次数为4,但当程序中的用户线程运行完之后,守护线程并没有继续向下循环,而是随着用户线程的结束而自我终止了。

守护线程的优先级

看到网上很多博文提到了守护线程的优先级问题,都说守护线程的优先级比较低,那我们通过一段测试用例看一下真实情况。

【代码实例2】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
java复制代码public class Test {
public static void main(String[] args) {
Thread thread1 = new Thread("守护线程"){
@Override
public void run() {
int i = 0;
while (i <= 4){
i++;
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":"+i+"-优先级:" +Thread.currentThread().getPriority());
}
super.run();
}
};
Thread thread2 = new Thread("用户线程"){
@Override
public void run() {
int i = 0;
while (i < 2){
i++;
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":"+i+"-优先级:" +Thread.currentThread().getPriority());
}
super.run();
}
};

//setDaemon, 不设置则默认false
thread1.setDaemon(true);//设置thread1为守护线程
thread2.setDaemon(false);//设置thread2为普通线程

thread1.start();
thread2.start();

for (int i = 0; i <5 ; i++) {
System.out.println("主线程:"+i+"-优先级:" +Thread.currentThread().getPriority());
}
}
}

输出:

1
2
3
4
5
6
7
8
9
java复制代码主线程:0-优先级:5
主线程:1-优先级:5
主线程:2-优先级:5
主线程:3-优先级:5
主线程:4-优先级:5
用户线程:1-优先级:5
守护线程:1-优先级:5
用户线程:2-优先级:5
守护线程:2-优先级:5

这个测试结果是不是出乎意料?无论是主线程还是普通的用户线程,又或者说守护线程,他们的优先级都是5,优先级竟然都一样!

我们知道所谓的线程就是CPU 调度和分派的基本单位,根据优先级不同,来决定获取CPU时间片的先后顺序,因为主线程启动时,其他线程还没有启动,所以这时候它最先获得CPU调度权限;

又因为其他线程存在休眠时间,这个时间段上足够主线程执行完毕。主线程执行完后,用户线程和守护线程互相抢占CPU资源,交错执行,直至程序中没有普通线程为止!若没有休眠时间,且循环次数足够多时,我们可以看到主线程、守护线程、用户线程都竞争CPU时间片,呈现交错执行的结果!

注意事项

在设置线程为守护线程的时候要注意一个事情,那就是当 start(); 放到 setDaemon(true); 之前,程序抛出IllegalThreadStateException。如下图:

守护线程.png

原因是 setDaemon(true)源码中,有一个isAlive()的判断,判断当前线程的状态是否为活跃线程,若是则抛出异常,我们不能修改一个正在运行中的线程!

【源码解析1】

1
2
3
4
5
6
7
8
java复制代码  public final void setDaemon(boolean on) {
checkAccess();
//线程已经启动后,不可修改,否则抛出非法线程状态异常
if (isAlive()) {
throw new IllegalThreadStateException();
}
daemon = on;
}

总结

OK,写到这里,关于守护线程的内容就讲完了,我们从什么是守护线程,守护线程的使用场景,优先级,注意事项等方面,进行了全面的介绍。

其实说实话,在我们日后工作中,很少直接使用上守护线程,所以它看似没那么重要,但在很多Java多线程相关的书籍中绝对都有提及,很多小伙伴在学习的过程中认为这个点不重要,也就相当然的忽略了,但遇到变态的面试官,专门挑拣一些偏僻的知识点考你时,难免陷入尴尬,所以希望借助这个考题,大家能够在日后更细心的学习哈。

结尾彩蛋

如果本篇博客对您有一定的帮助,大家记得留言+点赞+收藏呀。原创不易,转载请联系Build哥!

点赞.png

如果您想与Build哥的关系更近一步,还可以关注“JavaBuild888”,在这里除了看到《Java成长计划》系列博文,还有提升工作效率的小笔记、读书心得、大厂面经、人生感悟等等,欢迎您的加入!

公众号.png

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

面试官问我String能存储多少个字符?

发表于 2024-03-09
  1. 首先String的length方法返回是int。所以理论上长度一定不会超过int的最大值。
  2. 编译器源码如下,限制了字符串长度大于等于65535就会编译不通过
1
2
3
4
5
6
java复制代码private void checkStringConstant(DiagnosticPosition var1, Object var2) {
if (this.nerrs == 0 && var2 != null && var2 instanceof String && ((String)var2).length() >= 65535) {
this.log.error(var1, "limit.string", new Object[0]);
++this.nerrs;
}
}

Java中的字符常量都是使用UTF8编码的,UTF8编码使用1~4个字节来表示具体的Unicode字符。所以有的字符占用一个字节,而我们平时所用的大部分中文都需要3个字节来存储。

1
2
3
4
5
6
7
8
ini复制代码//65534个字母,编译通过
String s1 = "dd..d";

//21845个中文”自“,编译通过
String s2 = "自自...自";

//一个英文字母d加上21845个中文”自“,编译失败
String s3 = "d自自...自";

对于s1,一个字母d的UTF8编码占用一个字节,65534字母占用65534个字节,长度是65534,长度和存储都没超过限制,所以可以编译通过。

对于s2,一个中文占用3个字节,21845个正好占用65535个字节,而且字符串长度是21845,长度和存储也都没超过限制,所以可以编译通过。

对于s3,一个英文字母d加上21845个中文”自“占用65536个字节,超过了存储最大限制,编译失败。
3. JVM规范对常量池有所限制。量池中的每一种数据项都有自己的类型。Java中的UTF-8编码的Unicode字符串在常量池中以CONSTANTUtf8类型表示。CONSTANTUtf8的数据结构如下:

1
2
3
4
5
ini复制代码CONSTANT_Utf8_info {
u1 tag;
u2 length;
u1 bytes[length];
}

我们重点关注下长度为 length 的那个bytes数组,这个数组就是真正存储常量数据的地方,而 length 就是数组可以存储的最大字节数。length 的类型是u2,u2是无符号的16位整数,因此理论上允许的的最大长度是2^16-1=65535。所以上面byte数组的最大长度可以是65535
4. 运行时限制

String 运行时的限制主要体现在 String 的构造函数上。下面是 String 的一个构造函数:

1
2
3
java复制代码public String(char value[], int offset, int count) {
...
}

上面的count值就是字符串的最大长度。在Java中,int的最大长度是2^31-1。所以在运行时,String 的最大长度是2^31-1。

但是这个也是理论上的长度,实际的长度还要看你JVM的内存。我们来看下,最大的字符串会占用多大的内存。

1
ini复制代码(2^31-1)*16/8/1024/1024/1024 = 2GB

所以在最坏的情况下,一个最大的字符串要占用4GB的内存。如果你的虚拟机不能分配这么多内存的话,会直接报错的。


补充 JDK9以后对String的存储进行了优化。底层不再使用char数组存储字符串,而是使用byte数组。对于LATIN1字符的字符串可以节省一倍的内存空间。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【规范】看看人家Git提交描述,那叫一个规矩

发表于 2024-03-08

前言

🍊缘由

没想到玩了多年git,竟然还有提交描述规范

🏀事情起因:
在工作迭代过程中,偶然发现同组小帅哥Git提交描述总是和自己的不大一样,秉承好奇至上的我特意去研究了下。竟然发现提交了这么多年的Git描述竟然不符合规范,遂总结一下大厂和一些开源项目的的Git提交规范,跟大家分享一下。


🍋实例展示

规范Git提交记录

本狗不规范Git提交记录

🔔 分析
在团队开发中,一般都会使用Git 版本控制工具来管理代码,每个组员提交代码时都会写 commit message。如果没有一个统一标准规范,每个人都有自己的风格,项目小成员少还好,如果团队成员多,项目复杂,十分不利于阅读管理和维护。

通过上方图中提交记录对比,明显感觉上方Git提交记录较为规范美观。虽然本狗写的提交记录也比较清晰,但是随着项目推进及人员的混杂,规范标准必须执行!

因此为了后期一劳永逸,需要制定统一标准,提交记录清晰明了,让团队一看就能知道此次提交的目的,减少管理时间成本。


🎯主要目标

实现3大重点

1. IDEA Git描述规范插件
2. Git提交描述格式规范
3. 实例Git提交描述解析

正文

🥦目标分析

1.IDEA Git描述规范插件?

【git commit message helper】介绍

一个可帮助您标准化提交内容的插件

【git commit message helper】 插件安装步骤
  • 点击【File】=>【Settings】
  • 【Plugins】=>【Marketplace】搜索 git commit message helper,点击【Install】
  • 安装后点击【Installed】查看是否成功
【git commit message helper】 使用
  • 代码提交时,点击如下图标

  • 补充提交记录

    有问题先别着急,耐心往下看,慢慢分析每个属性!!!

2. Git提交描述格式规范解析

Git提交描述规则可以映射到插件下图部分,Header, Body,Footer

一个规范的Git提交描述格式如下

1
2
3
4
5
6
7
8
less复制代码# Header头
<type>(<scope>): <subject>

# Body体
<body>

# Footer体
<footer>
1.Header头

Header头只有一行,包括3个字段: type(必需), scope(可选), subject(必需)

属性 描述
type(必填) commit提交类型
scope(选填) commint提交影响范围
subject(必填) commint提交简短描述
  • type 提交类型

type说明提交类型:只允许使用下面属性

属性 描述
feat 新功能
fix 修改bug
docs 文档修改
style 格式修改
refactor 重构
perf 性能提升
test 测试
build 构建系统
ci 对CI配置文件修改
chore 修改构建流程、或者增加依赖库、工具
revert 回滚版本
  • scope 作用范围

scope说明提交影响范围:一般是修改的什么模块或者是什么功能,如【xx模块】/【xx功能】

  • subject 提交主题

subject 说明提交简短描述:一般是5-10各自简单描述做的任务,如【xx模块加入消息队列】

2.Body体

body说明提交详细描述:对于功能详细的描述,解释为什么加入这段代码,为什么调整优化等,如因分布式锁问题,导致死锁问题,优化调整xxxx

3.Footer脚

.Footer脚包括2个字段: Breaking Changes、Closed Issues

属性 描述
Breaking Changes 中断性不兼容变动(不常用)
Closed Issues 关闭Issues问题
  • Breaking Changes

当前版本与之前版本不兼容,如迭代升级对之前版本不能做到兼容,就需要在Breaking Changes后面描述变动理由和迁移方法之类,此属性不常用

  • Closed Issues
    当前 commit提交针对某个issue问题或者是禅道bug编号等,如Closes # 234
4.完成填充示例

3. 实例Git提交解析

举几个常用git提交描述案例

短信模块新功能提交

用户模块禅道bug1026修复提交

迭代SQL脚本提交

总结

本文通过IDEA中Git描述规范插件【git commit message helper】为契机,介绍Git提交描述的规范流程步骤,最后以实际例子作为体验对象,融汇插件及规范流程,实操Git Commit提交描述。希望大家能体会到流程的好处,团队规范统一的益处。


🍈猜你想问

如何与狗哥联系进行探讨

关注公众号【JavaDog程序狗】

公众号回复【入群】或者【加入】,便可成为【程序员学习交流摸鱼群】的一员,问题随便问,牛逼随便吹。

此群优势:

  1. 技术交流随时沟通
  2. 任何私活资源免费分享
  3. 实时科技动态抢先知晓
  4. CSDN资源免费下载
  5. 本人一切源码均群内开源,可免费使用
2.踩踩狗哥博客

javadog.net

大家可以在里面留言,随意发挥,有问必答


🍯猜你喜欢

文章推荐

【工具】用nvm管理nodejs版本切换,真香!

【苹果】SpringBoot监听Iphone15邮件提醒,Selenium+Python自动化抢购脚本

【项目实战】SpringBoot+uniapp+uview2打造H5+小程序+APP入门学习的聊天小项目

【项目实战】SpringBoot+uniapp+uview2打造一个企业黑红名单吐槽小程序

【模块分层】还不会SpringBoot项目模块分层?来这手把手教你!

【ChatGPT】手摸手,带你玩转ChatGPT

【ChatGPT】SpringBoot+uniapp+uview2对接OpenAI,带你开发玩转ChatGPT


本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

什么?你设计接口什么都不考虑?

发表于 2024-03-08

后端接口设计

如果让你设计一个接口,你会考虑哪些问题?

image.png

1.接口参数校验

接口的入参和返回值都需要进行校验。

  • 入参是否不能为空,入参的长度限制是多少,入参的格式限制,如邮箱格式限制
  • 返回值是否为空,如果为空的时候是否返回默认值,这个默认值需要和前端协商

2.接口扩展性

举个例子,比如用户在进行某些操作之后,后端需要进行消息推送,那么是直接针对这个业务流程来开发一个专门为这个业务流程服务的消息推送功能呢?还是说将消息推送整合为一个通用的接口,其他流程都可以进行调用,并非针对特定业务。

这个场景可能光靠说不是很能理解,大家想想策略工厂设计模式,是不是可以根据不同的策略,来选择不同的实现方式呢?再结合上面的这个例子,是否对扩展性有了进一步的理解呢?

3.接口幂等设计

什么是幂等呢?幂等是指多次调用接口不会改变业务状态,可以保证重复调用的结果和单次调用的结果一致

举个例子,在购物商场里面你用手机下单,要买某个商品,你需要去支付,然后你点击了支付,但是因为网速问题,始终没有跳转到

支付界面,于是你又连点了几次支付,那在没有做接口幂等的时候,是不是你点击了多少次支付,我们就需要执行多少次支付操作?

所以接口幂等到的是什么?防止用户多次调用同一个接口

  • 对于查询和删除类型的接口,不论调用多少次,都是不会产生错误的业务逻辑和数据的,因此无需幂等处理
  • 对于新增和修改,例如转账等操作,重复提交就会导致多次转账,这是很严重的,影响业务的接口需要做接口幂等的处理,跟前端约定好一个固定的token接口,先通过用户的id获取全局的token,写入到Redis缓存,请求时带上Token,后端做处理

image.png

4.关键接口日志打印

关键的业务代码,是需要打印日志进行监测的,在入参和返回值或者如catch代码块中的位置进行日志打印

  • 方便排查和定位线上问题,划清责任
  • 生产环境是没有办法进行debug的,必须依靠日志查问题,看看到底是出现了什么异常情况

5.核心接口要进行线程池隔离

分类查询啊,首页数据等接口,都有可能使用到线程池,某些普通接口也可能会使用到线程池,如果不做线程池隔离,万一普通接口出现bug把线程池打满了,会导致你的主业务受到影响

image.png

6.第三方接口异常重试

如果有场景出现调用第三方接口,或者分布式远程服务的话,需要考虑的问题

  • 异常处理

比如你在调用别人提供的接口的时候,如果出现异常了,是要进行重试还是直接就是当做失败

  • 请求超时

有时候如果对方请求迟迟无响应,难道就一直等着吗?肯定不是这样的,需要设法预估对方接口响应时间,设置一个超时断开的机制,以保护接口,提高接口的可用性,举个例子,你去调用别人对外提供的一个接口,然后你去发http请求,始终响应不回来,此时你又没设置超时机制,最后响应方进程假死,请求一直占着线程不释放,拖垮线程池。

  • 重试机制

如果调用对外的接口失败了或者超时了,是否需要重新尝试调用呢?还是失败了就直接返回失败的数据?

7.接口是否需要采用异步处理

举个例子,比如你实现一个用户注册的接口。用户注册成功时,发个邮件或者短信去通知用户。这个邮件或者发短信,就更适合异步处理。总不能一个通知类的失败,导致注册失败吧。 那我们如何进行异步操作呢?可以使用消息队列,就是用户注册成功后,生产者产生一个注册成功的消息,消费者拉到注册成功的消息,就发送通知。

image.png

8.接口查询优化,串行优化为并行

假设我们要开发一个网站的首页,我们设计了一个首页数据查询的接口,这个接口需要查用户信息,需要查头部信息,需要查新闻信息

等等之类的,最简单的就是一个一个接口串行调用,那要是想要提高性能,那就采取并行调用的方式,同时查询,而不是阻塞

可以使用CompletableFuture(推荐)或者FutureTask(不推荐)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
swift复制代码        Map<Long, List<SubjectLabelBO>> map = new HashMap<>();
      List<CompletableFuture<Map<Long, List<SubjectLabelBO>>>> completableFutureList =
      categoryBOList.stream().map(category ->
              CompletableFuture.supplyAsync(() -> getLabelBOList(category), labelThreadPool)
      ).collect(Collectors.toList());

      completableFutureList.forEach(future -> {
          try {
              Map<Long, List<SubjectLabelBO>> resultMap = future.get(); //这里会阻塞
              map.putAll(resultMap);
          } catch (Exception e) {
              e.printStackTrace();
          }
      });
       
public Map<Long, List<SubjectLabelBO>> getLabelBOList(SubjectCategoryBO category) {...}

9.高频接口注意限流

自定义注解 + AOP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
java复制代码@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimiter {
   int value() default 1;
   int durationInSeconds() default 1;
}

@Aspect
@Component
public class RateLimiterAspect {

   private final ConcurrentHashMap<String, RateLimiter> rateLimiters = new ConcurrentHashMap<>();

   @Pointcut("@annotation(RateLimiter)")
   public void rateLimiterPointcut(RateLimiter rateLimiterAnnotation) {
  }

   @Around("rateLimiterPointcut(rateLimiterAnnotation)")
   public Object around(ProceedingJoinPoint joinPoint, RateLimiter rateLimiterAnnotation) throws Throwable {
       int permits = rateLimiterAnnotation.value();
       int durationInSeconds = rateLimiterAnnotation.durationInSeconds();

       // 使用方法签名作为 RateLimiter 的 key
       String key = joinPoint.getSignature().toLongString();
       com.google.common.util.concurrent.RateLimiter rateLimiter = rateLimiters.computeIfAbsent(key, k -> com.google.common.util.concurrent.RateLimiter.create((double) permits / durationInSeconds));

       // 尝试获取令牌,如果获取到则执行方法,否则抛出异常
       if (rateLimiter.tryAcquire()) {
           return joinPoint.proceed();
      } else {
           throw new RuntimeException("Rate limit exceeded.");
      }
  }
}

@RestController
public class ApiController {

   @GetMapping("/api/limited")
   @RateLimiter(value = 10, durationInSeconds = 60) //限制为每分钟 10 次请求
   public String limitedEndpoint() {
       return "This API has a rate limit of 10 requests per minute.";
  }

   @GetMapping("/api/unlimited")
   public String unlimitedEndpoint() {
       return "This API has no rate limit.";
  }
}

10.保障接口安全

配置黑白名单,用Bloom过滤器实现黑白名单的配置

具体代码不贴出来了,大家可以去看看布隆过滤器的具体使用

11.接口控制锁粒度

在高并发场景下,为了防止超卖等情况,我们会对共享资源进行加锁的操作来保证线程安全的问题,但是如果加锁的粒度过大,是会影响

到接口性能的。那什么是加锁粒度呢?举一个例子,你带了一封情书回家,但是不想被爸妈发现,然后你偷偷回到房间里放到一个可以锁

住的抽屉里面,而不用把房间的门锁给锁上。 无论是使用synchronized加锁还是redis分布式锁,只需要在共享临界资源加锁即可,不涉

及共享资源的,就不必要加锁。

  • 锁粒度过大:

把方法A和方法B全部进行加锁,但是实际上我只是想要对A加锁,这就是锁粒度过大

1
2
3
4
5
6
scss复制代码void test(){
   synchronized (this) {
      B();
      A();
  }
}
  • 缩小锁粒度
1
2
3
4
5
6
scss复制代码void test(){
      B();
   synchronized (this) {
      A();
  }
}

12.避免长事务问题

长事务期间可能伴随cpu、内存升高、严重时会导致服务端整体响应缓慢,导致在线应用无法使用

产生长事务的原因除了sql本身可能存在问题外,和应用层的事务控制逻辑也有很大的关系。

  • 如何尽可能的避免长事务问题呢?

1.RPC远程调用不要放到事务里面

2.一些查询相关的操作如果可用,尽量放到事务外面

3.并发场景下,尽量避免使用@Transactional注解来操作事务,使用TransactionTemplate的编排式事务来灵活控制事务的范围

在原先使用@Transactional来管理事务的时候是这样的

1
2
3
4
5
6
7
8
9
scss复制代码@Transactional
public int createUser(User user){
   //保存用户信息
   userDao.save(user);
   passCertDao.updateFlag(user.getPassId());
   // 该方法为远程RPC接口
   sendEmailRpc(user.getEmail());
   return user.getUserId();
}

使用TransactionTemplat进行编排式事务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
scss复制代码@Resource
private TransactionTemplate transactionTemplate;

public int createUser(User user){
   transactionTemplate.execute(transactionStatus -> {
     try {
        userDao.save(user);
        passCertDao.updateFlag(user.getPassId());
    } catch (Exception e) {
        // 异常手动设置回滚
        transactionStatus.setRollbackOnly();
    }
     return true;
  });
// 该方法为远程RPC接口
   sendEmailRpc(user.getEmail());
   return user.getUserId();
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

还在用又臭又长的多层 if 判断空值?那我推荐你使用 Opt

发表于 2024-03-08

版本

  • JDK 8

前言

  • Optional 是 Java 8 中引入的一个有趣类,用于表示一个值存在或不存在。它的设计目的是解决空指针异常(NullPointerException)问题,使得程序更加健壮、简洁。

先看一个小案例

  • 大家看一下下面的代码是否存在问题?聪明的小伙伴也许都看出来了,代码没有进行判空检验,异常的场景会抛出 NullPointerException 异常。
1
java复制代码String isocode = user.getAddress().getCountry().getIsocode().toUpperCase();
  • 但是如果加上判空校验,那么我们代码可能就会变成下面这样又臭又长的情况:
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码if (user != null) {
Address address = user.getAddress();
if (address != null) {
Country country = address.getCountry();
if (country != null) {
String isocode = country.getIsocode();
if (isocode != null) {
isocode = isocode.toUpperCase();
}
}
}
}
  • 那么我们方法可以优化上面这段代码呢?答案当然是肯定的,它就是我们今天要介绍的主角 Java8 引入的 Optional,接下来就让我们一起看看 Optional 的魅力以及如何优化上文中又臭又长的代码。

Optional

Optional 创建

  • Optional 提供三种方式来创建 Optional 对象:
1
2
3
4
5
6
7
8
java复制代码        // 创建包含值的 Optional 对象
Optional<String> nonEmptyOptional = Optional.of("Hello");

// 创建一个空的 Optional 对象
Optional<String> emptyOptional = Optional.empty();

// 创建包含可能为空的值的 Optional 对象
Optional<String> nullableOptional = Optional.ofNullable(null);

Optional 常用方法

  • isPresent():检查值是否存在。
  • get():获取值,如果值不存在会抛出 NoSuchElementException 异常。
  • orElse(T other):获取值,如果值不存在则返回指定的默认值。
  • orElseGet(Supplier<? extends T> other):获取值,如果值不存在则返回由 Supplier 提供的默认值。
  • orElseThrow(Supplier<? extends X> exceptionSupplier):获取值,如果值不存在则抛出由 Supplier 提供的异常。
1
2
3
4
5
6
7
8
9
10
java复制代码        // 检查值是否存在
System.out.println("nonEmptyOptional is present: " + nonEmptyOptional.isPresent());
System.out.println("emptyOptional is present: " + emptyOptional.isPresent());
System.out.println("nullableOptional is present: " + nullableOptional.isPresent());

// 获取值
System.out.println("nonEmptyOptional value: " + nonEmptyOptional.get());

// 值为空是返回指定异常
nullableOptional.orElseThrow(() -> new IllegalStateException("field is not present"));

orElse(T other) 和 orElseGet(Supplier<? extends T> other)

  • 两者都是在值不存在时返回默认值,但还是有一些差异:1、接受参数不同 2、某些场景写法中存在性能问题(注意点)

Optional 为 null

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码public class TestMain {

public static void main(String[] args) {
Optional<String> nullableOptional = Optional.ofNullable(null);
System.out.println(nullableOptional.orElse(defaultStr()));
System.out.println(nullableOptional.orElseGet(() -> {
System.out.println("执行 defaultStr 方法 orElseGet");
return "defaultStr";
}));
}

public static String defaultStr() {
System.out.println("执行 defaultStr 方法");
return "defaultStr";
}
}

// 输出
执行 defaultStr 方法
defaultStr
执行 defaultStr 方法 orElseGet
defaultStr

Optional 不为 null

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码public class TestMain {

public static void main(String[] args) {
Optional<String> nullableOptional = Optional.ofNullable("123");
System.out.println(nullableOptional.orElse(defaultStr()));
System.out.println(nullableOptional.orElseGet(() -> {
System.out.println("执行 defaultStr 方法 orElseGet");
return "defaultStr";
}));
}

public static String defaultStr() {
System.out.println("执行 defaultStr 方法");
return "defaultStr";
}
}

// 输出
执行 defaultStr 方法
123
123
  • 对比两种情况,我们发现 orElse(T other) 无论 Optional 是否 null 都会执行传入的函数获取结果值,在一些高并发的场景会造成额外的性能浪费,应尽可能选择使用 orElseGet(Supplier<? extends T> other)。

map 和 flatMap

  • map 和 flatMap 可以将当前值传入到参数函数中,并返回一个 Optional 对象,两者唯一的区别在于 flatMap 不会再次包装,即传入函数返回值为 Optional 类型,具体可以参考下面的例子:
1
2
3
4
5
6
7
8
9
java复制代码    public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent())
return empty();
else {
// flatMap 不会主动包装为 Optional
return Objects.requireNonNull(mapper.apply(value));
}
}
  • 示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码@Builder
@Getter
class User implements Serializable {
private String name;
private Integer age;

public Optional<Integer> getAge() {
return Optional.ofNullable(age);
}
}

public class TestMain {

public static void main(String[] args) {
User user = User.builder().name("ming").age(null).build();
Optional.ofNullable(user).map(User::getName).orElse("unknown");
Optional.ofNullable(user).flatMap(User::getAge).orElse(0);
}
}
  • 了解 map 和 flatMap 方法十分重要,因为这是我们后文实现链式调用的关键。
  • 注:get 方法中一部分返回了 Optional 类型,一方面是为了演示,另一个方面可以通过这种方式可以让阅读我们代码的人明确的感知到这个字段的值可能为 null 你需要谨慎处理。

filter 过滤

  • Optional 类也提供了按条件过滤值的方法,filter() 接受一个 Predicate 参数,返回测试结果为 true 的值。如果测试结果为 false,会返回一个空的 Optional。
1
java复制代码Optional.ofNullable(user).filter(t -> t.getName().contains("test")).orElse("unknown");

如何优化文章开头的代码

  • 核心点在于使用 Optional 实现链式调用,首先我们需要对 User 类做一些小小的改造。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
java复制代码@Builder
@Getter
class User implements Serializable {
private String name;
private Integer age;

private Address address;

public Optional<Integer> getAge() {
return Optional.ofNullable(age);
}

public Address getAddress() {
return address;
}
}

class Address {
private Country country;

public Optional<Country> getCountry() {
return Optional.ofNullable(country);
}
}

class Country {
private String isocode;

public Optional<String> getCountry() {
return Optional.ofNullable(isocode);
}
}

public class TestMain {

public static void main(String[] args) {
User user = User.builder().name("ming").age(null).build();
Optional.ofNullable(user).map(User::getAddress)
.flatMap(Address::getCountry)
.flatMap(Country::getIsocode)
.orElse("unknown");
}
}
  • 通过使用 Optional 重构,我们代码的可读性和健壮性都有了很大的提升。

Java9 中的增强

  • Java 9 为 Optional 类添加了三个方法:or()、ifPresentOrElse() 和 stream()。
  • or():与 orElse() 和 orElseGet() 类似,它们都在对象为空的时候提供了替代情况。or() 的返回值是由 Supplier 参数产生的另一个 Optional 对象。
  • ifPresentOrElse():需要两个参数:一个 Consumer 和一个 Runnable。如果对象包含值,会执行 Consumer 的动作,否则运行 Runnable。
  • stream():通过把实例转换为 Stream 对象,让我们从广大的 Stream API 中受益。如果没有值,它会得到空的 Stream;有值的情况下,Stream 则会包含单一值。

总结

  • Optional 是 Java 8 中引入的一个有趣类,它的设计目的是解决空指针异常(NullPointerException)问题,我们应该好好掌握它,从而让我们在程序代码中更加优雅的处理空指针异常(NullPointerException)问题,使我们的代码具备更好的可读性以及更加健壮。

个人简介

👋 你好,我是 Lorin 洛林,一位 Java 后端技术开发者!座右铭:Technology has the power to make the world a better place.

🚀 我对技术的热情是我不断学习和分享的动力。我的博客是一个关于Java生态系统、后端开发和最新技术趋势的地方。

🧠 作为一个 Java 后端技术爱好者,我不仅热衷于探索语言的新特性和技术的深度,还热衷于分享我的见解和最佳实践。我相信知识的分享和社区合作可以帮助我们共同成长。

💡 在我的博客上,你将找到关于Java核心概念、JVM 底层技术、常用框架如Spring和Mybatis 、MySQL等数据库管理、RabbitMQ、Rocketmq等消息中间件、性能优化等内容的深入文章。我也将分享一些编程技巧和解决问题的方法,以帮助你更好地掌握Java编程。

🌐 我鼓励互动和建立社区,因此请留下你的问题、建议或主题请求,让我知道你感兴趣的内容。此外,我将分享最新的互联网和技术资讯,以确保你与技术世界的最新发展保持联系。我期待与你一起在技术之路上前进,一起探讨技术世界的无限可能性。

📖 保持关注我的博客,让我们共同追求技术卓越。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

面试官问我:自己写String类,包名也是javalang

发表于 2024-03-07

之前某次面试,我说自己对Java比较熟,面试官问了我一个问题:假设你自己写一个String类,包名也是java.lang,代码里使用String的时候,这个String类能编译成功吗?能运行成功吗?

好了,我当时又是一脸懵逼o((⊙﹏⊙))o,因为我只是看了些Java的面试题目,而且并没有涉及类加载方面的内容(ps:我是怎么敢说我对Java比较熟的)。

结论

先说结论:
能编译成功,但是运行会报错。因为加载String的时候根据双亲委派机制会默认加载jdk里的String。

  • 在自己写的String类中写main方法并运行,会报错找不到main方法。
1
2
3
4
5
6
7
8
9
java复制代码public class String {
public int print(int a) {
int b = a;
return b;
}
public static void main(String[] args) {
new String().print(1);
}
}

上述代码运行报错如下:

1
2
3
arduino复制代码错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application
  • 如果在其他类中尝试调用这个String类的方法,也调用不到,实际的结果是调用jdk中的String类的方法。

题目分析

这里涉及3个知识点:

  • Java代码的编译过程
  • Java代码的运行过程
  • 类加载器(详见文章:JVM:类加载器)

image.png

以上3个内容基本上是涉及代码运行的整个流程了。接下来就结合实战操作一步步分析具体的过程。

Java代码的编译过程

平时我都是通过IDEA直接运行代码,都没注意过编译的过程。所以结合平时的操作说明一下编译的过程。

什么是Java的编译

Java的编译过程,是将.java源文件转换为.class字节码文件的过程。

如何将.java源文件编译成.class字节码文件

  1. IDEA工具中,点击BUILD按钮
    image.png
  2. 执行命令javac xx.java

如何查看字节码文件

  1. 如果我们直接用文本工具打开字节码文件,将会看到以下内容:

image.png
这是因为Class文件内部本质上是二进制的,用不同的工具打开看,展示的效果不一样。下图是用xx工具打开的class文件,展示的是十六进制格式,其实可以自己一点点翻译出来源码了。(class文件的这个二进制串,计算机是不能够直接读取并且执行的。也就是说,计算机看不懂,而我们的JVM解决了这个问题,JVM可以看作是一个翻译官,它可以看懂,而且它也知道计算机想要什么样子的二进制,所以它可以把Class文件的二进制翻译成计算机需要的样子)

image.png
2. 我们可以通过命令的方式将class文件反汇编成汇编代码。

javap是JDK自带的反汇编器,可以查看java编译器为我们生成的字节码。

javap -v xx.class,javap -c -l xx.class

字节码文件中包含哪些内容

这个有很多文章说了,可以自己搜索一下,也可以看我总结的文章:xxx(还没写)。

Java代码的运行过程

java类运行的过程大概可分为两个过程:1)类的加载;2)类的执行。

需要说明的是:JVM主要在程序第一次主动使用类的时候,才会去加载该类。也就是说,JVM并不是在一开始就把一个程序就所有的类都加载到内存中,而是到不得不用的时候才把它加载进来,而且只加载一次。

类加载过程

Class文件需要加载到虚拟机中之后才能运行和使用。系统加载Class文件主要有3步:加载->连接->初始化。连接过程又可分为3步:验证->准备->解析。

image.png
(图源:javaguide.cn)

加载

类加载过程的第一步,主要完成3件事情:

  • 通过全类名获取定义此类的二进制字节流。
  • 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
  • 在内存中生成一个代表该类的Class对象,作为方法区这些数据的访问入口。

加载这一步的操作主要是通过类加载器完成的。类加载器详情可参考文章:xxx。

每个Java类都有一个引用指向加载它的ClassLoader。不过数组类不是通过ClassLoader创建的,而是JVM在需要的时候自动创建的,数组类通过getClassLoader方法获取ClassLoader的时候和该数组的元素类型的ClassLoader是一致的。

一个非数组类的加载阶段(加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,这一步我们可以去完成还可以自定义类加载器去控制字节流的获取方式(重写一个类加载器的loadClass()方法)。

加载阶段与连接阶段的部分动作(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未结束,连接阶段可能就已经开始了。

连接

验证

验证是连接阶段的第一步,这步的目的是为了保证Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当做代码运行后不会危害虚拟机的安全。

验证阶段所要耗费的资源相对还是多的,但验证阶段也不是必要的。如果程序运行的全部代码已经被反复使用和验证过,那在生产环境的实施阶段可以考虑使用-Xverify:none参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

验证阶段主要由4个检验阶段组成:

  • 文件格式验证。要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。比如以下验证点:
+ 是否以魔数CAFEBABE开头
+ 主、次版本号是否在当前Java虚拟机接收范围内
+ 常量池的常量是否有不被支持的常量类型
+ 。。。**该阶段验证的主要目的是保证输入的字节流能够被正确地解析并存储于方法区。只有通过了这个阶段的验证之后,这段字节流才被允许进入Java虚拟机内存的方法区中存储。后面3个阶段的验证是在方法区的存储信息上进行的,不会再直接读取和操作字节流了。**
  • 元数据验证。对字节码描述的信息进行语义分析,保证其描述的信息符合《Java语言规范》的要求。这个阶段可能包括的验证点如下:
+ 这个类是否有父类(除了Object类之外,所有的类都应该有父类)
+ 这个类or其父类是否继承了不允许继承的类(比如final修饰的类)
+ 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法。
  • 字节码验证。是整个验证过程中最复杂的,主要目的是通过分析字节码,判断字节码能否被正确执行。比如会验证以下内容:
+ 在字节码的执行过程中,是否会跳转到一条不存在的指令
+ 函数的调用是否传递了正确类型的参数
+ 变量的赋值是不是给了正确的数据类型
+ 。。。如果一个方法体通过了字节码验证,也仍然不能保证它一定是安全的。
  • 符号引用验证。该动作发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段–解析阶段中发生(所以说符号引用验证是在解析阶段发生???)。

符号引用验证的主要目的是确保解析行为能正常执行。

符号引用验证简单来说就是验证当前类是否缺少或者被禁止访问它依赖的外部类、方法、变量等资源。该阶段通常要校验以下内容:

+ 符号引用中通过字符串描述的全限定名是否能找到对应的类。
+ 在指定类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段。(没太明白什么意思)
+ 符号引用中的类、变量、方法是否可被当前类访问。如果无法通过符号引用验证,Java 虚拟机将会抛出一个 java.lang.IncompatibleClassChangeError 的子类异常,典型的如:


+ java.lang.IllegalAccessError
+ java.lang.NoSuchFieldError
+ java.lang.NoSuchMethodError等。

准备

准备阶段是正式为类中的静态变量分配内存并设置类变量初始化值的阶段。从概念上来说,这些变量所使用的内存都应当在方法区中分配,但方法区本身是一个逻辑概念。在JDK7及以前,HotSpot使用永久代来实现方法区。在JDK8及以后,类变量会随着Class对象一起放入Java堆中(也是叫做方法区的概念?)

注意点:

  • 准备阶段仅为类变量分配内存并初始化。实例变量会在对象实例化时随着对象一起分配在堆内存中。
  • 非final修饰的类变量,在初始化之后,是赋值为0,而不是程序中的赋值。比如:
1
java复制代码public static int value = 123;

初始化之后的值是0,而不是10。因为这时候程序还未运行。把 value 赋值为 123 的 putstatic 指令是程序被编译后,存放于类构造器() 方法之中,所以把 value 赋值为 123 的动作要到类的初始化阶段才会被执行。

  • final修饰的类变量,初始化之后会赋值为代码中的值。因为:如果类字段被 final 修饰,那么类阻断的属性表中存在 ConstantValue 属性,那在准备阶段变量值就会被初始化为ConstantValue 属性所指定的初始值,假设上面类变量 value 的定义修改为 123 ,而不是 “零值”

解析

解析阶段是将符号引用转化为直接引用的过程。也就是得到类或者字段、方法在内存中的指针或者偏移量。

  • 符号引用(Symbolic References):用一组字符串来表示所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
  • 直接引用(Direct Reference):是可以直接指向目标的指针,相对偏移量、或者可以间接定位到目标的句柄?直接引用是和虚拟机实现的内存布局直接相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在虚拟机的内存中存在。

初始化

初始化阶段是执行初始化方法 <clinit> ()方法的过程,是类加载的最后一步,这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)。

说明:<clinit> ()方法是编译之后自动生成的。

对于初始化阶段,虚拟机严格规范了有且只有 6 种情况下,必须对类进行初始化(只有主动去使用类才会初始化类):

  1. 当遇到 new、 getstatic、putstatic 或 invokestatic 这 4 条字节码指令时,比如 new 一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。
* 当 jvm 执行 `new` 指令时会初始化类。即当程序创建一个类的实例对象。
* 当 jvm 执行 `getstatic` 指令时会初始化类。即程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池)。
* 当 jvm 执行 `putstatic` 指令时会初始化类。即程序给类的静态变量赋值。
* 当 jvm 执行 `invokestatic` 指令时会初始化类。即程序调用类的静态方法。
  1. 使用 java.lang.reflect 包的方法对类进行反射调用时如 Class.forname("..."), newInstance() 等等。如果类没初始化,需要触发其初始化。
  2. 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。
  3. 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会先初始化这个类。
  4. MethodHandle 和 VarHandle 可以看作是轻量级的反射调用机制,而要想使用这 2 个调用,

就必须先使用findStaticVarHandle 来初始化要调用的类。
6. 当一个接口中定义了 JDK8 新加入的默认方法(default) ,那么实现该接口的类需要提前初始化。

代码运行过程:案例

针对下面这段代码进行讲解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码//MainApp.java  
pblic class MainApp {
public static void main(String[] args) {
Animal animal = new Animal("Puppy");
animal.printName();
}
}
//Animal.java
public class Animal {
public String name;
public Animal(String name) {
this.name = name;
}
public void printName() {
System.out.println("Animal ["+name+"]");
}
}
  1. MainApp类加载:编译得到MainApp.class文件后,在命令行上敲java AppMain。系统就会启动一个jvm进程,jvm进程从classpath路径中找到一个名为AppMain.class的二进制文件,将MainApp的类信息加载到运行时数据区的方法区内,这个过程叫做MainApp类的加载。
  2. 然后JVM找到AppMain的主函数入口,开始执行main函数。
  3. Animal类加载:main函数的第一条命令是Animal animal = new Animal(“Puppy”);就是让JVM创建一个Animal对象,但是这时候方法区中没有Animal类的信息,所以JVM马上加载Animal类,把Animal类的类型信息放到方法区中。
  4. 加载完Animal类之后,Java虚拟机做的第一件事情就是在堆区中为一个新的Animal实例分配内存, 然后调用构造函数初始化Animal实例,这个Animal实例持有着指向方法区的Animal类的类型信息(其中包含有方法表,java动态绑定的底层实现)的引用。
  5. 当使用animal.printName()的时候,JVM根据animal引用找到Animal对象,然后根据Animal对象持有的引用定位到方法区中Animal类的类型信息的方法表,获得printName()函数的字节码的地址。
  6. 开始运行printName()函数。

参考文章

  1. Java代码编译和执行的整个过程-简述(一)
  2. JavaGuide面试题
  3. java类在什么时候加载和初始化
  4. Java类加载机制
  5. JVM成神之路(2): 类的加载

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…505152…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%