性能调优-基础-6、JVM-类加载器
1. 类加载器分类
1.1. 启动类加载器⭐️🔴
1.2. 扩展类加载器
1.3. 系统类加载器
1.4. 线程上下文类加载器⭐️🔴
1.4.1. 是什么
ContextClassLoader
是一种与线程相关的类加载器,类似 ThreadLocal
,每个线程对应一个上下文类加载器,主要是用了打破类加载器中的委托机制的。在 SPI 中用来加载实现类。
线程上下文类加载器(Thread Context ClassLoader)可以通过 java.lang.Thread 类的 setContextClassLoader() 方法人为设置,创建线程时候未指定的话,则默认从父线程中继承。
那父线程中也没指定呢?那么会默认为应用程序的类加载器。例如:main 方法的线程上下文类加载器就是 sun.misc.Launcher$AppClassLoader。
1.4.2. 用途
SPI 的接口是 Java 核心库的一部分,是由 引导类加载器(Bootstrap Classloader) 来加载的;SPI 的实现类是由 系统类加载器 来加载的。
1.4.3. 设置时机⭐️🔴
为什么默认的线程上下文类加载器就是系统类加载器呢?肯定是在某个地方给设置了,其实它是在 Launcher 中进行设置的
可以看到 Launcher 类初始化时,先初始化了 ExtClassLoader,然后又初始化了 AppClassLoader,然后把 ExtClassLoader 作为 AppClassLoader 的父 loader,以及设置线程上下文类加载器。
Launcher 类是由 启动类加载器 加载的,即启动类加载器加载 sun.misc.Launcher 类,Launcher 加载扩展类加载器、系统类加载器以及设置线程上下文类加载器;
#todo
- 🚩 - 虚拟机的启动流程 - 🏡 2023-01-24 14:22 SpringBoot 的 Fat jar 启动也有一个 Launcher 类 [[3、SpringBoot-基础#^lf3ecs]]
1.5. 自定义类加载器
1.5.1. 获取 classloader 方法
1.5.2. Demo
1.5.3. API 区别⭐️🔴
^w4qf6s
class.forName() 与 ClassLoader.getSystemClassLoader().loadClass() 的区别
Class.forName 加载类时将类进了初始化 (第二个参数表示是否初始化,默认为 true),而 ClassLoader 的 loadClass 并没有对类进行初始化,只是把类加载到了虚拟机中。loadClass 的参数中默认为 false,表示不进行链接过程中的解析动作,即链接阶段不确定完成,所以初始化肯定不会完成。
1.5.3.1. Class.forName
单个 className 入参的 forName 方法,调用 forName0 时传入的 initialize 参数值默认为 true
1.5.3.1.1. resolveBeanClass
由 BeanDefinition 种的 String 类型的 beanName 变为 Class 时,并未进行初始化动作
1.5.3.2. ClassLoader.loadClass()
而 Spring 中传入的 initialize 参数值默认为 false,即默认先不要初始化,达到懒加载的目的
1.5.3.3. find-load-define 区别
findClass 类加载逻辑
loadClass 如果父类加载器加载失败则会调用自定义的 findClass 方法
defineClass 把类字节数组变成类
如果不打破双亲委派机制,只需重写 findClass 方法即可
如果打破双亲委派机制,需要重写整个 loadClass 方法
URLClassLoader 实现了 findClass 方法
1.5.3.4. 应用场景
在我们熟悉的 Spring 框架中的 IOC 的实现就是使用的 ClassLoader。
而在我们使用 JDBC 时通常是使用 Class.forName() 方法来加载数据库连接驱动。这是因为在 JDBC 规范中明确要求 Driver(数据库驱动) 类必须向 DriverManager 注册自己。
https://www.cnblogs.com/jimoer/p/9185662.html
https://juejin.cn/post/6996669656190681119
https://kilric.com/post/classloader/
http://codefun007.xyz/a/article_detail/779.htm
2. 类加载器初始化流程
sun.misc.Launcher 类是 java 的入口,在启动 java 应用的时候会首先创建 Launcher 类,创建 Launcher 类的时候回准备应用程序运行中需要的类加载器。
3. 类加载机制
3.1. 双亲委派机制⭐️🔴
3.1.1. 全盘委托和双亲委派
[[../../../../cubox/006-ChromeCapture/(209条消息) 类加载机制:全盘负责和双亲委托_zhangzeyuaaa的博客-CSDN博客_beanfactory父子关系与全盘委托]]
3.1.1.1. 全盘委托
“全盘负责”是指当一个 ClassLoader 装载一个类时,除非显示地使用另一个 ClassLoader,则该类所依赖及引用的类也由这个 CladdLoader 载入。
例如,系统 类加载 器 AppClassLoader 加载入口类(含有 main 方法的类)时,会把 main 方法所依赖的类及引用的类也载入,依此类推。“全盘负责”机制也可称为当前类加载器负责机制。显然,入口类所依赖的类及引用的类的当前类加载器就是入口类的类加载器。
3.1.1.2. 双亲委派
以上步骤只是调用了 ClassLoader.loadClass(name) 方法,并没有真正定义类。真正加载 class 字节码文件生成 Class 对象由“双亲委派”机制完成。
3.1.2. 源码分析
[[../../../../cubox/006-ChromeCapture/ClassLoader双亲委派机制源码分析 - 简书]]
如果 parent 不为 null,则由 parent 来进行类加载,依次递归到 ExtClassLoader
ExtClassLoader 的 parent 在 Launcher 构造过程中被设置为了 null,所以会执行 else 的逻辑,调用 findBootstrapClassOrNull()
,而该方法最终为 native 方法 private native Class findBootstrapClass(String name)
,实际上就是调用 openjdk 中 BootStrap ClassLoader 的实现去加载该类。如果不是启动类加载器负责的路径,那么 c 为 null,递归出来又进到 loadClass 方法中,此时 parent 不为 null,就由 parent 进行加载,依次类推。
3.2. 沙箱安全机制
3.3. 类相同判断⭐️🔴
3.4. 指定加载器
添加 jar 包到扩展类加载器路径
打完包后放到 ext 目录下,验证双亲委托加载机制,ext 会加载,应用类加载器就不会再加载
3.5. 类加载过程⭐️🔴
3.5.1. 加载
3.5.1.1. 加载过程
3.5.1.2. 加载结果⭐️🔴
3.5.1.2.1. InstanceKlass
类的字节码被加载到元空间的方法区中,生成 InstanceKlass类
(包含了类型信息、域信息、方法信息、运行时常量池),
InstanceKlass 存着Java 类型的名字、继承关系、实现接口关系,字段信息,方法信息,运行时常量池的指针,还有内嵌的虚方法表(vtable)、接口方法表(itable)和记录对象里什么位置上有 GC 会关心的指针(oop map)等等。
与方法调用紧密相关 性能调优专题-基础-5、JVM-虚拟机栈
3.5.1.2.2. InstanceMirrorKlass
同时在 heap 中生成一个 Class 对象 (即InstanceMirrorKlass对象
),它持有方法区中 instanceKlass 的内存地址。同时 instanceKlass 中的 _java_mirror 也持有堆中 Class 对象的内存地址
3.5.1.3. Class 与 Klass 关系
3.5.1.4. 总结⭐️🔴
- JVM 能执行的就是 Class 文件,所有计算机语言只要最后生成了 Class 文件,都可以交给 JVM 执行。Kotlin、Groovy、JRuby、Jython、Scala 等语言就是如此。
- 由于 JVM 是由 C/C++ 编写的,所以每一个 Java 类加载到 JVM 时都会生成一个对应的 C++ 类,即 InstanceKlass,存放在方法区 (元空间)。同时生成一个 InstanceKlass 的实例对象 (是 InstanceKlass 的子类,也是 C++),即 InstanceMirrorKlass,放在堆区。 ❕
- JVM 类加载机制分为,加载、验证、准备、解析、初始化五个阶段。
- 通过类的全限定名获取存储该类的 class 文件,并对其进行解析 解析后生成对应的 C++ 模板类,即InstanceKlass 实例,存放在元空间,用于 JVM 内部使用 在堆区生成该类的 Class 对象实例,即 InstanceMirrorKlass,用于其他系统或程序进行调用。
静态变量跟随 Class 放在了堆里,还有字符串常量池也放在了堆里,具体演变请看👇🏻
在 JDK 6 及之前的 HotSpot VM 里,静态字段依附在 InstanceKlass 对象的末尾;而在 JDK 7 开始的 HotSpot VM 里,静态字段依附在 java.lang.Class 对象的末尾,所以 7 之后包装成了 InstanceMirrorKlass
性能调优专题-基础-3、JVM-方法区和3个池子3.5.2. 链接
#todo
NEW-TOOL:BinaryViewer idea 有插件
3.5.2.1. 链接 - 验证
验证类是否符合 JVM 规范,是安全性检查
3.5.2.2. 链接 - 准备⭐️🔴
❕
3.5.2.2.1. 静态变量
注意final 的静态引用类型变量这个特例
3.5.2.2.2. 常量区别
静态变量的赋值动作,是在这个 static 块,即 clinit 方法中被执行的;
而 final 修饰的常量是在编译阶段已经完成零值初始化,在准备阶段进行赋值动作。
3.5.2.2.3. 其他变量
实例变量:随着对象的创建,会在堆空间中分配实例变量空间,并进行默认赋值
局部变量:在使用前,必须进行显示赋值,否则编译不通过
3.5.2.2.4. 零值
3.5.2.3. 链接 - 解析
new 方法会触发解析动作
对于一个方法的调用,编译器会生成一个包含目标方法所在的类、目标方法名、接收参数类型以及返回值类型的符号引用,来指代要调用的方法。
解析阶段的目的,就是将这些符号引用解析为实际引用。
如果符号引用指向一个未被加载的类,或者未被加载类的字段或方法,那么解析将触发这个类的加载(但未必会触发解析与初始化)。
将常量池中的符号引用转为直接引用,能够确切的知道引用类或方法的具体位置和内容。除了解析外,其他阶段是顺序发生的,而解析可以与这些阶段交叉进行,因为 Java 支持动态绑定(晚期绑定),需要运行时才能确定具体类型
3.5.2.3.1. 解析案例
https://www.bilibili.com/video/BV1yE411Z7AP?p=146&vd_source=c5b2d0d7bc377c0c35dbc251d95cf204
代码:[[Load2.java]]
new 方法会触发解析动作,loadclass 方法只是加载 class,并没有触发解析动作
3.5.3. 初始化
3.5.3.1. 作用
初始化阶段是执行初始化方法 <clinit> ()
方法的过程,是类加载的最后一步,这一步 JVM 才开始真正执行类中定义的 Java 程序代码 (字节码)。
说明:
<clinit> ()
方法是编译之后自动生成的。
对于 <clinit> ()
方法的调用,虚拟机会自己确保其在多线程环境中的安全性。因为 <clinit> ()
方法是带锁线程安全,所以在多线程环境下进行类初始化的话可能会引起多个线程阻塞,并且这种阻塞很难被发现。
3.5.3.2. clinit
虚拟机会收集类及父类中的类变量及类方法组合为<clinit>
方法,根据定义的顺序进行初始化。虚拟机会保证子类的< clinit>执行之前,父类的<clinit>
方法先执行完毕。
因此,虚拟机中第一个被执行完毕的< clinit>方法肯定是 java.lang.Object 方法。
如果类或者父类中都没有静态变量及方法,虚拟机不会为其生成<clinit>
方法。
接口与类不同的是,执行接口的<clinit>
方法不需要先执行父接口的<clinit>
方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>
方法。
虚拟机会保证一个类的<clinit>
方法在多线程环境中被正确地加锁和同步,如果多个线程同时去初始化一个类,那么只有一个线程去执行这个类的<clinit>
方法,其他线程都需要阻塞等待,直到活动线程执行< clinit>方法完毕。
3.5.3.3. 发生时机
PS: 注意多线程下加载时,如果加载出现问题,会导致其他线程都进入阻塞状态。
3.5.3.4. 主动被动使用⭐️🔴
❕
主动使用:意味着会调用类的 <clinit>()
,即执行了类的初始化阶段
被动使用:就是指没有调用类的 <clinit>()
,没有执行初始化阶段
https://www.sukaidev.top/2021/03/30/d90e9b80/#%E7%B1%BB%E5%8A%A0%E8%BD%BD%E7%9A%84%E8%BF%87%E7%A8%8B
[[ActiveUse1.java]]
需要注意的是,在上述 7 个阶段中,只有加载、验证、准备、初始化和卸载这 5 个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,并且是互相交叉地混合式进行。解析阶段在某些情况下可以在初始化阶段之后再开始,直接是为了支持 Java 语言的运行时绑定(也称为动态绑定或晚期绑定)。
那么什么时候会触发这个流程呢?《Java 虚拟机规范》中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把握。不过,规范中严格规定了有且只有以下 8 种情况必须立即对类进行“初始化”:
- 当创建一个类的实例时,比如使用new 关键字,或者通过反射、克隆、反序列化
- 当调用类的静态方法时,即当使用了字节码 invokestatic 指令。
- 当使用类、接口的静态字段时 (final 修饰需分情况考虑),比如,使用 getstatic 或者 putstatic 指令。(对应访问变量、赋值变量操作)❕
下面例子中变量 c 虽然使用了 final 修饰,但是会潜在一个 Integer 自动装箱的动作,需要初始化的赋值动作,所以触发了 E 的初始化。而 a 和 b 因为是基本数据类型和字符串字面量,在链接准备阶段就完成初始化赋值,所以不会再触发后面的初始化动作
static 与 final 的搭配使用->[[001-基础知识专题-关键字和接口-2、final]]
- 使用 java.lang.reflect 包的方法对类型的方法进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化。比如:Class.forName(“com.atguigu.java.Test”)
- 当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。但是这条规则并不适用于接口。但如果接口中定义了 default 方法,那么实现了该接口的子类初始化就会触发接口的初始化 ,即下面第 7 条所述内容 ❕
- 当一个接口中定义了 JDK 8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类。![[JVM源码分析之JVM启动流程_猿灯塔_InfoQ写作社区#^75pqt1]]
- 当使用 JDK 7 新加入的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial 四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
这 8 种场景中的行为称为对一个类型进行 主动引用,那么对应的就存在 被动引用 了,被动引用时将不会˝触发初始化过程。例如有 4 个被动引用的场景:
- 通过子类引用父类的静态字段,父子类都加载了,但父类会初始化,而不会导致子类初始化。❕
-XX:+TraceClassLoading
查看类加载情况 可以通过启动参数 - 通过数组定义引用的类,不会触发此类的初始化。
对于数组类的话,数组类的元素类型本身也需要进行类加载 (不会初始化),不过数组类本身并不通过类加载器创建,而是直接由 Java 虚拟机在内存中动态构造出来。
❕
因为对象内存模型中,array 是另一个子类,[[Java的对象模型——Oop-Klass模型(二) - 掘金#Klass的继承体系]]
SpringIOC 中类加载也有体现数组类型的创建是由虚拟机调用了本地方法:https://www.processon.com/diagraming/63bb6db902b9ad68cd932a68
- 常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
- 类对象.class
- ClassLoader 的 loadclass,如下图所示
loadclass 默认是不解析的,即链接阶段未完成,所以肯定不会是初始化完成的
❕ - class.forName 第二个参数为 false,表示不进行初始化
3.5.3.5. 其他补充
当 Java 虚拟机初始化一个类时,要求它的所有父类都已经被初始化,但是这条规则并不适用于接口。
在初始化一个类时,并不会先初始化它所实现的接口
在初始化一个接口时,并不会先初始化它的父接口
因此,一个父接口并不会因为它的子接口或者实现类的初始化而初始化。只有当程序首次使用特定接口的静态字段时,才会导致该接口的初始化。
4. 初始化与实例化区别⭐️🔴
实例化内容:对象创建-7、JVM-堆
4.1. 实例化总体流程
- 父类普通成员变量和普通代码块;
- 父类的构造函数;
- 子类普通成员变量和普通代码块;
- 子类的构造函数。
[[../../../../cubox/006-ChromeCapture/Java类的 初始化 和 实例化区别_Fox_bert的博客-CSDN博客_java实例化和初始化的区别]]
[[../../../../cubox/006-ChromeCapture/类加载、对象实例化知识点一网打尽 - jimuzz - 博客园]]
5. 实战经验
5.1. SPI 是否破坏了双亲委派机制
5.1.1. 双亲委派
https://www.jianshu.com/p/3a3edbcd8f24
https://www.zhihu.com/question/49667892
首先双亲委派原则本身并非 JVM 强制模型。
5.1.2. SPI⭐️🔴
SPI ,全称为 Service Provider Interface,是一种服务发现机制。它通过在 ClassPath 路径下的 META-INF/services 文件夹查找文件,文件内容为类的全限定名,自动加载文件里所定义的类。
这一机制为很多框架扩展提供了可能,比如在 Dubbo、JDBC 中都使用到了 SPI 机制。
5.1.2.1. 原理剖析
SPI 的调用方和接口定义方很可能都在 Java 的核心类库之中,而实现类交由开发者实现,然而实现类并不会被启动类加载器所加载,基于双亲委派的可见性原则,SPI 调用方无法拿到实现类。
❕
SPI Serviceloader 通过线程上下文获取能够加载实现类的 classloader,一般情况下是 application classloader,绕过了这层限制,逻辑上打破了双亲委派原则。
https://blog.csdn.net/m0_37135421/article/details/103770096
JVM 规范:全盘委托和双亲委派
全盘委托:加载某类时,其引用的类的加载也由同一个加载器加载
双亲委派:加载类时,向上查找 (先查找父加载器的缓存,然后负责的路径),父加载器找不到,再往下查找加载
SPI 的情况是bootstrap 类加载器负责的 rt.jar 里,定义了接口规范,比如数据库连接的接口,由 bootstrap 类加载器加载,但接口中引用了各个厂商实现类,根据全盘委托机制,也应该由 bootstrap 类加载器加载,但是这些实现类所在目录不在 bootstrap 类加载器的负责范围之内,此时使用了在启动类中设置为上下文类加载器的应用类加载器,所以打破了双亲委派机制。
5.1.2.2. 触发逻辑
❕ ^3e5oac
因为类加载器收到加载范围的限制,在某些情况下父加载器无法加载到需要的文件,就需要委托给子类加载器去加载 class 文件。
JDBC 的 Driver 接口定义在 JDK 中,其实现有数据库的各个厂商提供。DriverManager类中要加载各个实现了Driver接口的类统一进行管理,Driver类位于JAVA_HOME中jre/lib/rt.jar中
,应该由 Bootstrap 类加载器进行加载,而各个 Driver 的实现类位于各个服务商提供的 jar 包中。根据类加载机制,当被加载的类引用了另外一个类时,虚拟机就会使用装载第一个类的类加载器装在被引用的类,也就是说应该使用 Bootstrap 类加载器去加载各个厂商提供的 Driver 类。但是,Bootstrap 类加载器只负责加载 JAVA_HOME 中 jre/lib/rt.jar 中所有的 class,所以需要由子类加载器去加载 Driver 的实现类,这就破坏了双亲委派模型。
在 Java 应用中存在着很多服务提供者接口(Service Provider Interface,SPI),这些接口允许第三方为它们提供实现,如常见的 SPI 有 JDBC、JNDI 等,这些 SPI 的接口属于 Java 核心库,一般存在 rt.jar 包中,由 Bootstrap 类加载器加载。而 Bootstrap 类加载器无法直接加载 SPI 的实现类,同时由于双亲委派模式的存在,Bootstrap 类加载器也无法反向委托 AppClassLoader 加载器 SPI 的实现类。在这种情况下,我们就需要一种特殊的类加载器来加载第三方的类库,而线程上下文类加载器(双亲委派模型的破坏者)就是很好的选择。
从图可知 rt.jar 核心包是有 Bootstrap 类加载器加载的,其内包含 SPI 核心接口类,由于 SPI 中的类经常需要调用外部实现类的方法,而 jdbc.jar 包含外部实现类 (jdbc.jar 存在于 classpath 路径) 无法通过 Bootstrap 类加载器加载,因此只能委派线程上下文类加载器把 jdbc.jar 中的实现类加载到内存以便 SPI 相关类使用。显然这种线程上下文类加载器的加载方式破坏了“双亲委派模型”,它在执行过程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器,当然这也使得 Java 类加载器变得更加灵活。
6. 参考与感谢
6.1. 尚硅谷 - 宋红康
[[../../../002-schdule/001-Arch/001-Subject/007-性能调优专题/001-JVM/黑马程序员JVM完整教程/讲义/3_类加载与字节码技术.pdf]]
https://javaguide.cn/java/jvm/class-loading-process.html
https://blog.csdn.net/m0_37135421/article/details/103770096
[[../../../../cubox/006-ChromeCapture/【没啥用的知识】一砖一瓦皆根基 —— Java类加载之Class对象到Klass模型 - 掘金]]
[[../../../../cubox/006-ChromeCapture/【JVM】类加载_zxfhahaha的博客-CSDN博客]]
[[../../../../cubox/006-ChromeCapture/Java的对象模型——Oop-Klass模型(一) - 掘金]]
OOP-Klass 模型:https://juejin.cn/post/6998389585927487495
性能调优专题-基础-5、JVM-虚拟机栈[[shishenm]]
示例代码:/Users/taylor/Nutstore Files/Obsidian_data/pages/002-schdule/001-Arch/001-Subject/013-DemoCode/JVMDemo
对象创建-7、JVM-堆6.2. 黑马程序员
https://www.bilibili.com/video/BV1yE411Z7AP?p=146&vd_source=c5b2d0d7bc377c0c35dbc251d95cf204
示例代码:/Users/taylor/Nutstore Files/Obsidian_data/pages/002-schdule/001-Arch/001-Subject/013-DemoCode/jvm