看到一篇关于类加载机制的博客,觉得讲的很不错,顺带整理个笔记。文章末尾附链接。
类的加载顺序
先看一个例子:想一下这个会输出的结果是什么
public class ClassLoaderTest {
public static ClassLoaderTest classLoaderTest = new ClassLoaderTest();
public static int a;
public static int b = 0;
public static void main(String[] args) {
ClassLoaderTest instance = getInstance();
System.out.println(ClassLoaderTest.a);
System.out.println(ClassLoaderTest.b);
}
public static ClassLoaderTest getInstance(){
return classLoaderTest;
}
public ClassLoaderTest() {
a++;
b++;
}
}
为什么会这样呢?
一个 java 类的完整的生命周期会经历加载、连接、初始化、使用、和卸载五大阶段。
加载:将.class字节码加载到jvm中,并生成一个class对象。
连接:进行验证(是否能被jvm执行,主要包括文件格式验证、元数据验证、字节码验证和符号引用验证)、准备(为静态变量分配内存)、解析(把类中的符号引用转换为直接引用)
初始化:为类的静态变量赋予用户定义的默认值和执行静态代码块
在例子中,在加载阶段将ClassLoaderTest加载到jvm中;在连接阶段对静态变量分配内存空间并初始化默认值,此时classLoaderTest = null;a =0;b=0;
main方法中通过
ClassLoaderTest instance = getInstance();
调用该类的静态方法,触发了类的初始化,在初始化阶段对静态变量进行赋值。
1 初始化classLoaderTest : classLoaderTest = new ClassLoaderTest();调用类的构造方法,a=1;b=1。
2 初始化a: a没有赋值操作,所以a=1;
3 初始化b:b=0;进行赋值。
所以最后的结果是1,0。
注:引用类的静态变量,不会导致子类初始化
再看一个例子:
下面是程序的输出结果:
为什么是这样呢?这儿引入一个“主动使用”的概念:
类只有在“主动使用”的时候才会进行初始化
1. 创建类的实例
2. 调用类的静态方法
3. 使用类的非常量静态字段
4. 调用Java API中的某些反射方法:如Class.forName()
5. 初始化某个类的子类
6. 含有main()方法的类启动时
返回来看上面的例子:
调用Teacher.age 此时会加载Teacher.java,由于Teacher.java继承了Person类,需要对Person.java进行加载成Person.class,并对父类进行初始化,初始化时会对类的静态变量赋值,以及执行静态代码块,此时输出Person static block。父类加载及初始化完成之后进行加载Teacher.java。加载成为Teacher.class之后,调用Teacher.age。该情况不属于上述主动使用的情况,所以不需要对Teacher.class进行初始化,也就不会有Teacher static block的输出。
类加载的时候可以通过class.forName来进行加载,也可以通过classloader.loadClass 来进行加载(loadClass加载的时候不会触发类的初始化)。
ClassLoader 类加载器
根据一个类的全限定名来读取此类的二进制字节流到 JVM 中,然后转换为一个与目标类对应的 java.lang.Class 对象,在 JVM 虚拟机提供了 3 种类加载器,启动(Bootstrap)类加载器(也称根类加载器)、扩展(Extension)类加载器、系统(System)类加载器(也称应用类加载器)
- Bootstrap ClassLoader
Bootstrap ClassLoader 称为启动类加载器(也称根类加载器),是由 C++ 语言写,是 Java 虚拟机自身的一部分,它是在 Java 虚拟机启动后初始化的,用来装载核心类库,如 java.lang.*等,它主要负责加载 %JAVA_HOME%/jre/lib, %JAVA_HOME%/jre/classes,或者被-Xbootclasspath参数指定的路径的类。
- Extension ClassLoader
ExtClassLoader 是用 Java 写的,由 Bootstrap ClassLoader 加载,并且将 ExtClassLoader 的父加载器设置为 Bootstrap ClassLoader,它 Launcher 类中定义的静态内部类,具体就是 sun.misc.Launcher$ExtClassLoader,主要负责加载 Java 的扩展类库,默认加载 JAVA_HOME/jre/lib/ext/ 目下的所有 jar 包以及 java.ext.dirs 系统变量指定的路径中类库。在Java9之后,更名为PlatformClassLoader
。
- Application ClassLoader
Bootstrap ClassLoader 加载完 ExtClassLoader 后,就会加载 AppClassLoader,并且将 AppClassLoader 的父加载器指定为 ExtClassLoader。AppClassLoader 也是用 Java 写成的,它的实现类是 sun.misc.Launcher$AppClassLoader,另外在 ClassLoader 中提供了一个getSystemClassLoader()
方法,此方法可用于获取 AppclassLoader。AppClassLoader 主要负责加载 classpath 所指定的位置的类或者是 jar 包,我们编写的 Java 程序也是由它加载。它也是我们实现自定义类加载器时默认的父加载器。
bootstrapClassLoader 是C++ 语言写的,这对 Java 是不可见的,所以最后一行的classLodaer为null。
所以如果某个 Class 对象的 classLoader 属性值是 null 时,那么就表示这个类也是根加载器加载的。
双亲委派机制
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,若父类加载器无法完成此加载任务,子类加载器才会尝试自己去加载,这就是双亲委派模式。
可以通过Classloader.loadClass()的源码看到此过程。
如果parent不为空时,通过parent的类加载器进行加载。如果为空,则使用启动类加载器进行加载。如果自带的类加载器无法加载该类的话,则通过 findClass()使用自定义加载器进行加载。
来说一下双亲委派的优点
1 可以避免重复加载
2 安全性:Java 核心基础 API 中定义类型不能被随意替换,假设通过网络传递一个名为 java.lang.Integer 的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心 Java API 发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的 java.lang.Integer,而直接返回已加载过的 Integer.class,这样便可以防止核心 API 库被随意篡改。
https://gitchat.csdn.net/activity/5cafebd20ebc85648e322910?utm_source=so