类加载流程
提示
Java虚拟机把描述类的数据从Class文件加载到内存
,并对数据进行校验、转换解析和初始化
,最终形成可以被虚拟机直接使用
的Java类型。
类加载时机
类加载的生命周期
类型被加载到虚拟机内存中开始,到卸载出内存为止,整个生命周期经历如上七个阶段。
其中
验证、准备、解析
统称为连接
。需要注意的是:
解析阶段顺序是不确定的
,它可以在初始化阶段之后再开始
。
类初始化的六种情况
《Java虚拟机规范》中规定了六种
要立即对类进行”初始化“(加载、验证、准备自然需要在此之前执行)
的情况:
遇到
new
、getstatic
、putstatic
、invokestatic
这四条字节码指令时,类型没有过初始化,生成这四条字节码指令的场景有:- 使用
new
关键字实例化对象。
java// 会初始化A A a = new A();
- 读取或设置一个类型的
静态字段(final修饰、编译器进入常量池的静态字段除外)
。
javaclass B { // 会导致A类被初始化 static A a = new A(); public static void main(String[] args) { // 不会导致A类被初始化 System.out.print(A.HELLO_WORLD); } } class A { static final String HELLO_WORLD = "hello_world"; }
引用
静态字符串常量
不会导致持有该常量的类初始化。- 调用一个类型的静态方法
javaclass A { static void print() { System.out.print("hello"); } public static void main(String[] args) { A.print(); } }
我们可以用过
-XX:+TraceClassLoading
来查看类是否被加载。- 使用
通过
java.lang.Reflect
对方法进行反射调用时初始化类时发现其
父类
还没有初始化。
public class SuperClass {
static {
System.out.println("Super class init");
}
public static int value = 123;
}
class SubClass extends SuperClass {
static {
System.out.println("Sub class init");
}
}
class Test {
public static void main(String[] args) {
// 只会初始化父类,不会初始化子类
System.out.println(SubClass.value);
}
}
- 接口中定义的
default
方法,若该接口的实现类发生初始化,default
方法在此之前要被初始化。
public interface Father {
default void print() {
System.out.print("hello");
}
}
public class Son implements Father {
}
class Test {
public static void main(String[] args) {
// 会初始化Father接口中的default方法
Father f = new Son();
}
}
如果是接口初始化,那么不会要求父接口也全部初始化,
真正使用到的父接口
才会初始化。
java.lang.invoke.MethodHandle
实例的解析结果为REF_getstatic
、REF_putstatic
、REF_invokestatic
、REF_newInvokeSpecial
四种类型的方法句柄还没有初始化时。
类加载流程
加载
加载
阶段是整个类加载生命周期的第一个阶段,Java虚拟机需要完成以下三件事情:
- 通过一个类的全限定名获取定义此类的二进制字节流。
《Java虚拟机规范》没有规定
二进制字节流
的具体获取方式,目前已知获取方式包括:从zip包读取、运行时生成、加密文件获取等
。既可以通过Java虚拟机内置的类加载器
,也可以通过用户自定义的类加载器
来实现类的加载动作(类来源的多样性需要自定义类加载器)。数组本身不通过类加载器创建,但
数组的类型
需要通过类加载器来完成加载。
将字节流代表的静态存储结构(ex:
常量池 -> 运行时常量池
)转换为方法区运行时数据结构
。堆中生成
java.lang.Class
对象,并作为这个类方法区各各种数据的访问入口。
验证
- 文件格式验证
验证字节流是否符号Class文件格式
的规范,并能够被当前版本的虚拟机处理。包括:常量池中的常量是否有不被支持的常量类型
、是否以魔数0XCAFEBABE开头
等验证点。
- 元数据验证
对字节码描述的信息进行予以分析,确保符合规范,包括:此类是否有父类
、是否继承了不被允许继承的类
、是否缺少字段、方法
等验证点。
- 字节码验证
通过数据流和控制流分析
,确保程序语义是合法、符合逻辑的。包括:保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作
、保证方法中类型转换都是有效的
等验证点。
- 符号引用验证
目的是确保将符号引用转为直接引用
的解析
阶段能够顺利执行,对类自身以外的类信息进行匹配性校验。包括:符号引用中通过字符串描述的权限定名能够找到对应的类
、符号引用中的类、字段、方法的可访问性
等验证点。
# 关闭大部分的类验证以缩短虚拟机加载类时间
-Xverify:none
准备
为类中定义的静态变量
分配内存并设置类变量初始值
的阶段。这些变量所使用的内存都会在方法区
进行分配。
class Test {
// 类变量(静态变量),初始值为0
public static int value1 = 123;
// 类变量,初始值为123
public static final int value2 = 123;
// 实例变量
public Object obj;
}
类变量value1在
准备
阶段过后的初始值为0
,赋值为123的操作要等到初始化
阶段才会执行。类变量value2在编译时会生成
ConstantValue
属性,在准备
阶段虚拟机就会将value2设置为123。实例变量会随着
对象Test的实例化
而一起分配在堆中
。
解析
Java虚拟机将常量池内的符号引用替换为直接引用
的过程。《Java虚拟机规范》没有规定解析
阶段发生的具体时间,只要求了执行ldc、getfield、getstatic等17个指令前
,先对它们所使用的符号引用
进行解析。所以虚拟机可以自行决定解析的触发时机是类被加载器加载时或符号引用将被使用前。
符号引用:以
一组符号
来描述所引用的目标。可以引用没有加载到内存中的内容。直接引用:可以直接指向目标的指针、相对偏移量或者能够定位到目标的句柄池。
- 类或接口解析
基于以下代码,将从未解析过的符号引用x
解析为类或接口的直接引用
。包括三个步骤:
class Test {
A x = new A();
}
若A不是数组类型,虚拟机将
x作为权限定名
交给Test类的加载器
去加载类A
,类A
按照类加载流程
执行类加载,若发生错误,那么解析失败。若A是数组类型(如Integer[]),那么虚拟机会将
x即[Ljava/lang/Integer
中的Integer
类型交给Test类加载器
去加载,再由虚拟机生成对应的数组对象
。若前两步没有问题,在解析完成前还要进行
符号引用
验证,确保Test对A
的访问权限。
- 字段解析
通过类的常量池表查找字段所属的
类或接口的符号引用(用A表示)
,并执行类或接口的解析
。按照如下步骤执行:
- 若A本身包含了
简单名称
和字段描述
都与目标匹配的字段,返回该字段的直接引用。 - 否则,若A实现了接口,会按照继承关系从下往上查找,重复步骤1。
- 否则,若A不是
java.lang.Object
,会按照继承关系从下往上查找,重复步骤1。 - 否则,查找失败,抛出
NoSuchFieldError
异常。 - 最后对该字段进行
权限验证
,若不具备权限,抛出java.lang.illegalAccessError
异常。
- 方法解析
与字段解析
类似,都是要找到方法所属的
类或接口的符号引用(用A表示)
。按照如下步骤执行:
- 如果A是个接口,那么直接抛出
java.lang.IncompatibleClasssChangeError
异常。 - 否则,查找类A中是否存在
简单名称
和字段描述
都与目标匹配的方法,返回该方法的直接引用。 - 否则,查找类A
实现的接口及它们的父接口
中递归查找是否存在简单名称
和字段描述
都与目标匹配的方法,若存在匹配的方法,说明类A是个抽象类
,抛出java.lang.AbstractMethodError
异常。 - 否则宣告查找失败,抛出
java.lang.NoSuchMethodError
异常。 - 若返回了方法的直接引用,则需要进行
权限验证
,若不具备抛出java.lang.illegalAccessError
异常。
- 接口方法解析
- 如果A是个类,那么直接抛出
java.lang.IncompatibleClasssChangeError
异常。 - 否则,查找接口A中是否存在
简单名称
和字段描述
都与目标匹配的方法,返回该方法的直接引用。 - 否则,在接口A的父接口中递归查找,直到
java.lang.Object类
为止,若存在简单名称
和字段描述
都与目标匹配的方法,返回该方法的直接引用。 - 否则宣告查找失败,抛出
java.lang.NoSuchMethodError
异常。
初始化
直接来说:初始化
阶段就是执行类构造器<clinit>
方法的过程。
<clinit>
方法是由编译器自动收集类中的所有类变量的赋值动作
与静态语句块(static {})
中的语句合并产生的。编译器的收集顺序由语句在源文件
中出现的顺序决定的。<clinit>
不包含静态方法
,静态方法
在被调用的时候才会加载。
class Test {
static {
// i=1执行先于i=0,但并不能修改成功
i = 1;
// 输出i,提示Illegal forward reference编译错误
// System.out.println(i);
}
static int i = 0;
public static void main(String[] args) {
System.out.println(i); // 0
}
}
static静态代码块
只能访问到定义在其之前的变量,定义在其之后的变量能赋值但不能访问
。
clinit
与
类的构造函数<init>方法
不同,<clinit>
不需要显示的调用父类构造器,Java虚拟机会保证子类的<clinit>
方法执行前,父类的<clinit>
已执行完毕,因此java.lang.Object
类是Java虚拟机中第一个被执行的<clinit>
方法的类型。由于父类的
<clinit>
先执行,所以父类中的而静态语句块要先于子类的变量赋值操作。<clinit>
方法对于接口或类来说不是必须的,如果没有静态语句块、也没有变量赋值操作,那么编译器不会为该类生成<clinit>
方法。接口中不能使用
静态语句块
,但能有静态变量赋值操作,所以接口也能生成<clinit>
方法。但与类不同的是,执行接口的<clinit>
方法不需要执行父接口的<clinit>
方法,只有当父接口中定义的变量被使用时,父接口才会初始化。此外接口的实现类在初始化时也不会执行接口的<clinit>
方法。Java虚拟机必须要保证一个类的
<clinit>
方法在多线程环境中,如果多个线程同时初始化一个类,那么只会有一个线程去执行<clinit>
方法,其他线程都要阻塞等待,直到活动线程执行完<clinit>
。若一个类的<clinit>
方法存在耗时很长的操作,那么可能造成多线程阻塞(活动线程执行完,其他线程不会再进入<clinit>
方法)。
class Parent {
static int A = 0;
static {
A = 2;
}
}
class Son extends Parent {
public static int B = A;
}
public class Test1 {
public static void main(String[] args) {
System.out.println(Son.B);// 2
}
}
根据规则:父类的
<clinit>
先执行,所以父类的静态语句块先于子类而执行。Son.B = 2
。
类加载器
类加载阶段的通过一个类的全限定名来获取描述该类的二进制字节流
这个动作交给Java虚拟机外部去实现,让应用程序决定如何去获取所需的类,实现这个动作的代码被称为类加载器(Class Loader)
。类加载器用于实现类的加载动作
。
判断两个类是否相等,前提是由
同一个类加载器
加载。不同类加载器加载同一个.class文件也是不相等的。
public class ClassLoaderTest {
public static void main(String[] args) throws Exception {
// 自定义类加载器,匿名内部类
ClassLoader classLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String fileName =
name.substring(name.lastIndexOf(".") + 1) + ".class";
// 使用当前自定义类加载来加载fileName的类为二进制字节流
InputStream inputStream =
getClass().getResourceAsStream(fileName);
// 如果找不到就让父类加载器去执行加载
if (null == inputStream) {
return super.loadClass(name);
}
byte[] bytes = new byte[inputStream.available()];
inputStream.read(bytes);
// 创建name的Class对象
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
};
// 通过自定义类加载器实现类的加载,并实例化该对象
Object obj =
classLoader.loadClass("top.leejay.jvm.load.ClassLoaderTest")
.newInstance();
// 查看加载的Class对象
System.out.println(obj.getClass());
// 验证不同的类加载器加载同一个.class文件是否相同
System.out.println(obj instanceof ClassLoaderTest);// false
}
}
此时在Java虚拟机中共存在两个
ClassLoaderTest
类,一个是应用程序类加载器
加载的,一个是自定义类加载
加载的。
双亲委派模型
上图各种类加载器之间的层次关系被称为
类加载器的双亲委派模型
。双亲委派模型要求:除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器(继承自java.lang.ClassLoader
类)。如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载该类,而是把这个请求委派给父加载器去执行,每个层次都是如此,最终所有的请求都传递到
最顶层的启动类加载器
中。只有当父加载器无法完成这个加载请求
时,子加载才会尝试自己去完成加载。
- 启动类加载器
负责加载JAVA_HOME/lib
目录下,能被Java虚拟机识别的类库。由C++
实现。Java中用null
来表示。
- 扩展类加载器
负责加载JAVA_HOME/lib/ext
目录下的类库。由Java
实现。
- 应用程序加载器
因为是ClassLoader.getSystemClassLoader
的返回值,又被称为系统类加载器。负责加载用户类路径(ClassPath)上所有的类库。
不同加载器加载同一文件
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 判断类是否被加载
Class<?> c = findLoadedClass(name);
// 如果没有被加载
if (c == null) {
long t0 = System.nanoTime();
try {
// 如果当前类加载器的父类不为null,说明存在父加载器
// 如果为null,说明加载到顶层的启动类加载器了(Java中为null)
if (parent != null) {
// 调用父类的类加载器加载类,往上查找
c = parent.loadClass(name, false);
} else {
// 如果为null,那么调用顶层的启动类加载器来加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果父加载器没找到,那么会抛出该异常,默认不处理
}
// 如果c=null,说明启动类加载器也没找到这个类
// 那么会直接调用本身的findClass方法
if (c == null) {
long t1 = System.nanoTime();
// 调用findClass,从上往下查找
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
判断请求加载的类型是否被加载过,如果没有则调用父加载器的
loadClass()
,若父加载器为null
则默认使用启动类加载器
作为自己的父加载器。若父加载器加载失败,抛出ClassNotFound
异常后,就会调用自身的findClass
方法尝试进行加载。
双亲委派的优点
- 避免
类重复加载
。加载类都会先判断这个是否被加载过。 - 避免
核心类
被篡改。如果用户自定义了java.lang.Object
类,就无法保证最基本的行为。
破坏双亲委派
- 历史遗留原因
类加载器和ClassLoader抽象类在JDK1
中就存在,而双亲委派模型在JDK1.2
才出现,为了面对已经存在的用户自定义类加载器的代码而做出妥协,加入了protected Class<?> findClass()
方法,结合ClassLoader
的源码,当loadClass
加载失败,就会调用自身的findClass
方法。
- JNDI
我们知道JNDI
的代码由启动类加载器加载,但JNDI
需要对调用部署在应用程序的ClassPath
下的JNDI
服务提供者接口,简单来说就是顶层启动类加载器需要加载应用程序类加载器
,此问题会破坏双亲委派模型。
解决办法:引入线程上下文类加载器,如果创建线程时还未设置,它会从父线程中继承一个,如果在应用程序的全局范围内都没有设置的话,那么这个类加载器默认是应用程序类加载器
。
正确编写自定义类加载器
在前面我们编写了"糟糕"
的自定义类加载器代码来验证不同的类加载器加载同一个.class文件是不相等的
。那么这节我们按照JDK1.2
后建议我们使用的findClass
来编写自定义类加载器。
public class MyClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) {
// 先判断类是否已被加载
Class<?> c = findLoadedClass(name);
if (null == c) {
try {
// 加载本地磁盘上指定name的class文件(只要不在ClassPath下即可)
FileInputStream inputStream =
new FileInputStream(new File("D://" + name + ".class"));
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
byte[] bytes = new byte[inputStream.available()];
int index;
while ((index = inputStream.read(bytes)) != -1) {
outputStream.write(bytes, 0, index);
}
// 获取字节流
byte[] byteArray = outputStream.toByteArray();
// 创建name的Class对象
c = defineClass(name, byteArray, 0, byteArray.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
return c;
}
}
public class ClassLoaderTest {
public static void main(String[] args) throws ClassNotFoundException {
MyClassLoader loader = new MyClassLoader();
Class<?> hello = loader.loadClass("Hello");
// top.leejay.jvm.load.MyClassLoader@4fccd51b
System.out.println(hello.getClassLoader());
}
}
- D盘下创建一个名为
Hello
的java文件,javac -encoding utf-8 Hello.java
生成class文件。- 切记
Hello.class
文件不能在ClassPath
下,否则会导致AppClassLoader
加载该类。- 自定义类加载器的
loadClass
方法为入口,在三个默认类加载器都找不到时会调用findClass
。
Class.forName与ClassLoader.loadClass
结论
forName
除了加载.class文件
外,还会执行该类的初始化
,即执行类的<clinit>
方法,所以类的静态代码块
和静态变量赋值操作
都会执行。loadClass
只会将.class文件
加载到虚拟机中,不会执行初始化操作。
@CallerSensitive
public static Class<?> forName(String className)
throws ClassNotFoundException {
Class<?> caller = Reflection.getCallerClass();
/**
* @param 要加载的类名
* @param 是否需要初始化 true/false 初始化/不初始化
* @param 调用者类加载器
* @param 调用者Class对象
*/
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
public class ClassDemo {
static int value = 0;
static {
System.out.println("static ...");
value = 1;
System.out.println(value);
}
}
class Test {
public static void main(String[] args) throws ClassNotFoundException {
// 初始化
Class<?> aClass1 =
Class.forName("top.leejay.jvm.load.ClassDemo");
// 不会初始化
Class<?> aClass2 = ClassLoader.getSystemClassLoader()
.loadClass("top.leejay.jvm.load.ClassDemo");
}
}