一、类加载过程 / 类的生命周期:
总共分为五步,依次为:加载、连接(验证、准备、解析)、初始化、使用、卸载
1.1 加载:
主要完成下面 3 件事情:
- 通过全类名获取定义此类的二进制字节流。
- 将字节流所代表的静态存储结构转换为方法区的运行时数据结构。
- 在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口。
加载是通过 类加载器 完成的。类加载器有很多种,当我们想要加载一个类的时候,具体是哪个类加载器加载由 双亲委派模型 决定(能打破该模型)。
1.2 连接:
1. 验证:
JVM规范校验:JVM 会对字节流进行文件格式校验,判断其是否符合 JVM 规范,是否能被当前版本的虚拟机处理。
代码逻辑校验: JVM 会对代码组成的数据流和控制流进行校验,确保没有语法错误
2. 准备:
内存分配的对象:JVM 为类中的静态成员变量分配内存空间
初始化的类型:为 静态成员变量 赋予 Java 语言中该数据类型的零值,而不是用户代码里初始化的值。比如:对于int类型将赋值为0,而不是用户定义的数值;但对static final修饰的变量则在该阶段赋用户定义的值。
3. 解析:
虚拟机将常量池内的符号引用替换为直接引用的过程,也就是得到类或者字段、方法在内存中的指针或者偏移量。
1.3 初始化:
是类加载的最后一步,这一步 JVM 才开始真正执行类中定义的 Java 程序代码。
当 JVM 遇到下面6种情况时,必须对类进行初始化(只有主动去使用类才会初始化类):
- 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会先初始化这个类。
- 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。
- 当遇 new 一个类,操作(读取、赋值)一个静态字段(未被 final 修饰)、或调用一个类的静态方法时,会进行初始化。
- 当 jvm 执行 new 指令时会初始化类。即当程序创建一个类的实例对象。
- 当 jvm 执行 getstatic 指令时会初始化类。即程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池)。
- 当 jvm 执行 putstatic 指令时会初始化类。即程序给类的静态变量赋值。
- 当 jvm 执行 invokestatic 指令时会初始化类。即程序调用类的静态方法。
- 使用 java.lang.reflect 包的方法对类进行反射调用时如 Class.forname(“…”), newInstance() 等等。如果类没初始化,需要触发其初始化。
- MethodHandle 和 VarHandle 可以看作是轻量级的反射调用机制,而要想使用这 2 个调用, 就必须先使用 findStaticVarHandle 来初始化要调用的类。
- 当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
初始化过程中,会对类变量赋用户指定的值,且是线程安全的,因此可实现线程安全的单例模式(单例类用例可参考)。
1.4 使用:
JVM 从入口方法开始执行用户的程序代码
1.5 卸载:
该类的 Class 对象被 GC。
卸载类需要满足 2 个要求:
- 该类没有在其他任何地方被引用,所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象。
- 该类的 Classloader 实例已被 GC
所以,在 JVM 生命周期内,由 JVM 自带的类加载器加载的类是不会被卸载的。但是由我们自定义的 Classloader 加载的类是可能被卸载的。
只要想通一点就好了,JDK 自带的 BootstrapClassLoader, ExtClassLoader, AppClassLoader 负责加载 JDK 提供的类,所以它们(类加载器的实例)肯定不会被回收。而我们自定义的 Classloader 的实例是可以被回收的,所以使用我们自定义加载器加载的类是可以被卸载掉的。
更多类加载可参考
思考:
在整个加载过程中,其实 初始化 时才是真正执行类中代码的一步,那么类中的代码是按顺序自上而下加载,还是按其他规则加载的呢?
二、加载阶段 - 类加载器:
特点:
- 类加载器是一个负责加载类的对象,用于实现类加载过程中的“加载”这一步。ClassLoader 可以在内存中生成一个代表该类的 Class 对象
- 每个 Java 类都有一个引用指向加载它的 ClassLoader。
- 数组类是由 JVM 直接生成的,其 ClassLoader 和该数组的元素类型是一致的。
JVM 中内置了三个重要的 ClassLoader:
1. Bootstrap ClassLoader
负责装载最核心的Java类,比如Object、System、 String等,用 C++ 来实现的,不存在于 JVM 中。
2. ExtensionClassLoader,JDK 9 及以后更换为Platform ClassLoader
主要负责加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类,以及被 java.ext.dirs 系统变量所指定的路径下的所有类,比如XML、加密、压缩相关的功能类等。
3. AppClassLoader
负责加载当前应用 classpath 下的所有 jar 包和类。
User ClassLoader
自定义的类加载器由使用方来实现,以满足自己的特殊需求。比如可以对 Java 类的字节码( .class 文件)进行加密,加载时再利用自定义的 ClassLoader 对其解密。
自定义类加载器继承至 ClassLoader 类,其中有两个关键的方法:
- protected Class loadClass(String name, boolean resolve):加载指定二进制名称的类,实现了双亲委派机制 。name 为类的二进制名称,resove 如果为 true,在加载时调用 resolveClass(Class<?> c) 方法解析该类。
- protected Class findClass(String name):根据类的二进制名称来查找类,默认实现是空方法。
当需要加载一个类时,递归的向上级父加载器查找类,然后逐层向下尝试加载类,确保了基础类的统一。
在查找类时,通过类的 全限定类名 和 其类加载器ID 作为唯一标识,这也是 JVM 中 判断两个类是否相等 的条件
代码用例:
public static void main(String[] args) {
ClassLoader c = ATest.class.getClassLoader();
ClassLoader cp = c.getParent();
ClassLoader cpp = cp.getParent();
System.out.println(c);//sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(cp);//sun.misc.Launcher$ExtClassLoader@1eb44e46
System.out.println(cpp);//null
}
更多介绍可参考 该文
委派模型下的类加载机制:
为什么不叫“双亲委派模型”?因为这里的“双亲”容易让人误解其中的流程,本文简化为“委派模型”
流程图:
委派模型的好处
委派模型保证了 Java 程序的稳定运行,可以保证一个类只被一个 ClassLoader 加载且只被加载一次,避免了类的重复加载(JVM 中类的唯一标识:类全限定名 + ClassLoader),也保证了 Java 的核心 API 不被篡改。
思考:
打破委派模型的场景
委派模型在 Java 中,被破坏的场景:
场景一:重写 ClassLoader#loadClass(java.lang.String, boolean)方法
Tomcat 作为 web 容器,当部署多个应用时,为了处理多个应用对同一个第三方类库的不同版本的依赖,在 WebappClassLoader (自定义的类加载器)中重写loadClass,优先使用 WebappClassLoader 进行加载,违背了委派原则中从父到子的逐级加载顺序。
场景二:Thread Context ClassLoader
基础类(由Bootstrap加载,如JNDI 服务)为了调用子类(由 Application / User 加载)的服务,加入了 Thread Context ClassLoader 设计,用来选择合适的类加载器去完成子类的加载, 解决了基础类型要回调到 用户自定义类的类加载问题。
JNDI(Java Naming and Directory Interface)是Java提供的一种API,它提供了一种标准的方式,使Java 应用程序能够通过统一的接口访问各种命名和目录服务。
在 JNDI 中,Thread Context ClassLoader 的实际应用主要是为了解决类加载器的上下文切换问题,避免类加载冲突和类找不到的问题。
由于 JNDI 服务在不同的类加载器环境中执行,它不一定能够直接访问目标类所在的类加载器。这可能导致类加载冲突或无法找到类的问题。为了解决这个问题,JNDI 服务可以使用线程上下文类加载器(Thread Context ClassLoader)来切换到合适的类加载器上下文。用例如下:
在JNDI中,可以通过以下步骤使用线程上下文类加载器:
1.保存当前线程的上下文类加载器:
ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader();
2.在需要加载类的地方,将线程上下文类加载器设置为目标类加载器:
Thread.currentThread().setContextClassLoader(targetClassLoader);
3.执行需要使用目标类加载器加载的操作,如通过JNDI查找和获取资源。
4.恢复线程的上下文类加载器为原来的类加载器:
Thread.currentThread().setContextClassLoader(currentClassLoader);
场景三:Hot Swap
为了实现热更新,替换一个模块时,把这个模块连同类加载器一起换掉就实现了代码的热替换。
在OSGI环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为网状结构。
SpringBoot中,ClassLoader的实现与功能解析
SpringBoot 的 FAT JAR 机制简化了应用的打包、部署、启动能力。为了实现 FatJar 机制,SpringBoot 对类加载机制做了对应的调整。
打包后的目录结构:
├── BOOT-INF
│ ├── classes # 应用配置和class文件
│ └── lib # 应用依赖jar文件
├── META-INF
│ ├── MANIFEST.MF # jar 描述信息文件。应用启动类,boot启动入口类等
│ ├── maven # maven 信息文件
└── org
└── springframework
└── boot
└── loader # spring boot 启动相关类库
| ├── archive
| │ ├── Archive.class
|....
Boot中 ClassLoader 特点以及它解决的问题
类加载流程图:
pom中引入 groupId:org.springframework.boot ; artifactId:spring-boot-loader ; version: 2.2.9
SpringBoot 将创建的 LaunchedURLClassLoader 添加到 Thread 中,当一些基础的ClassLoader 无法加载到 应用所依赖的其他 jar 中所包含的类 时,使用 LaunchedURLClassLoader 完成它们的加载动作。
思考&问题:
1. 类中不同区域代码加载顺序测试
类变量(静态变量)、静态代码块:
-
随着类加载而被加载,在加载过程中,是线程安全的(可用于单例模式的创建)
-
是最先执行的
成员变量、代码块:
-
在类初始化时被调用
-
是在构造函数前执行的
构造函数:
- 在初始化时被调用
实列化一个对象的执行顺序用例:
-
父类静态变量和静态代码块(顺序执行);
-
子类静态变量和静态代码块(顺序执行);
-
父类的成员变量和代码块(顺序执行);
-
父类的构造函数;
-
子类的变量和代码块(顺序执行);
-
子类的构造函数。
package TestOrder;
public class Order
{
public static void main(String[] args) {
new Child();
}
}
class Child extends Parent{
static Foo FOO = new Foo("44444Child's static parameter");//4
Foo foo = new Foo("99Child's parameter");//9
static {
System.out.println("5555Child's static code block");//5
}
{
System.out.println("10 10 Child's code block");//10
}
public Child() {
System.out.println("11 11 Child.Child()");//11
}
}
class Parent {
static Foo FOO = new Foo("22222Parent's static parameter");//2
Foo foo = new Foo("6666Parent's parameter");//6
static {
System.out.println("333333Parent's static code block");//3
}
{
System.out.println("7777Parent's code block");//7
}
public Parent() {
System.out.println("888Parent.Parent()");//8
}
}
class Foo {
static String str="this str in Foo";
static {
System.out.println("1111print in Foo static :"+str);//1
}
public Foo(String word) {
System.out.println(word);
}
}
2. 数组的 Classloader 是什么?
数组的 Classloader与数组的元素类型的 Classloader相同。换句话说,数组的 Classloader 用的是数组中,元素类型的类 Classloader。
当创建一个数组对象时,它的元素类型的 Classloader 将负责加载数组元素的类。如果数组的元素类型是Java提供的类型(例如int、boolean、String等,由Java虚拟机直接提供的),则缺少类加载器,由引导类加载器(Bootstrap ClassLoader)完成加载。
但如果数组的元素类型是引用类型,那么数组的 Classloader 将由加载这些引用类型的类加载器确定。
@Test
public void classloaderTest(){
// 加载Java核心类
String[] str = new String[2];
ClassLoader stringClassLoader = str.getClass().getClassLoader();
System.out.println("String ClassLoader: " + stringClassLoader);
// 加载应用程序类
ExtensionClass[] extensionClass = new ExtensionClass[2];
ClassLoader extensionClassLoader = extensionClass.getClass().getClassLoader();
System.out.println("ExtensionClass ClassLoader: " + extensionClassLoader);
// 加载应用程序类
ClassLoader appClassLoader = this.getClass().getClassLoader();
System.out.println("Application ClassLoader: " + appClassLoader);
}
class ExtensionClass {
// Java扩展类
}
win-jdk 11.0.2-输出:
String ClassLoader: null
ExtensionClass ClassLoader: jdk.internal.loader.ClassLoaders$AppClassLoader@61064425
Application ClassLoader: jdk.internal.loader.ClassLoaders$AppClassLoader@61064425
参考文档:
《深入理解 Java 虚拟机》