Java类加载过程
概述
Java类加载过程包括以下五个阶段:
- 加载
- 验证
- 准备
- 解析
- 初始化
验证、准备和解析三个阶段统称连接阶段。
加载、验证、准备和初始化这几个阶段的开始顺序是确定的,解析阶段不一定,可能会在初始化之后才开始,也因此使得Java支持动态绑定。
详细了解下各个阶段具体的动作。
加载
加载阶段完成的是class文件的字节流载入虚拟机,虚拟机在此阶段需要完成以下三个任务:
- 通过全限定类名获取类的二进制字节流(不管文件的来源,网络也好,磁盘文件或压缩包也罢,只要能读入二进制流就可以);
- 将读取的字节流代表的静态存储结构转化为方法区的运行时数据结构;
- 在内存中(并没有明确规定是在堆中,HotSpot中Class对象虽然也是对象,但是却是存储在方法区中的)生成一个代表此类的java.lang.Class对象,作为方法区这个类的各种信息的入口。
在加载过程中,会用到类加载器,类加载器负责加载类文件到虚拟机中,并返回Class对象。
需要注意的是,数组类本身是不通过类加载器加载的,它是由java虚拟机直接创建的。数组类也是Class对象,是比较特殊的类:
package jdk.test.classLoader;
public class ArrayLoadTest {
public static void main(String[] args) {
Test[] test = new Test[10];
int[] i = { 0 };
System.out.println(test.getClass());
System.out.println(test.getClass().getClassLoader());
System.out.println(i.getClass());
System.out.println(i.getClass().getClassLoader());
}
}
// class [Ljdk.test.classLoader.Test;
// sun.misc.Launcher$AppClassLoader@73d16e93
// class [I
// null
从代码执行结果看,class [Ljdk.test.classLoader.Test;
类是由AppClassLoader加载的啊,int数组类的加载器是null,那么这个null是不是BootStrapClassLoader呢?
一个数组类的创建过程遵循以下原则:
- 数组元素类型是引用类型,如上面的test,会使用元素类的类加载器去加载这个Test类,但是数组会在这个类加载器的类名称空间上被标识。
- 如果数组元素是基本类型jvm会将此数组标记为何引导类加载器关联。
可以使用java -verbose命令行选项查看类加载的轨迹。
验证
验证阶段主要对输入的字节码进行检查,确保其内容符合规范。
实际上在加载期间就已经开始了验证过程,在读入字节流之后需要经过验证字节流是否符合Class文件格式的规范,且能被当前虚拟机执行,验证通过后才会存储到方法区中,在之后的其他验证都是对方法区的数据进行验证的,所以验证和加载时交叉的。
准备
准备阶段的任务是给静态变量分配内存和设置初始值(零值),final static变量(实际上就是常量)会直接赋代码中实际要赋的值,这些变量都将在方法区中分配内存。
解析
解析阶段是虚拟机将常量池中的符号引用替换成直接引用的过程。
通俗地说就是把占位符换成真正的地址(内存偏移等)
使用javap -verbose反编译class文件可以看到常量池。
$ javac -verbose Test.java
[解析开始时间 RegularFileObject[Test.java]]
[解析已完成, 用时 78 毫秒]
[源文件的搜索路径: .,G:\Program Files\Java\jdk1.8.0_45\lib]
[类文件的搜索路径: G:\Program Files\Java\jdk1.8.0_45\jre\lib\resources.jar,G:\Program Files\Java\jdk1.8.0_45\jre\lib\rt.jar,G:\Program Files\Java\jdk1.8.0_45\jre\lib\sunrsasign.jar,G:\Program Files\Java\jdk1.8.0_45\jre\lib\jsse.jar,G:\Program Files\Java\jdk1.8.0_45\jre\lib\jce.jar,G:\Program Files\Java\jdk1.8.0_45\jre\lib\charsets.jar,G:\Program Files\Java\jdk1.8.0_45\jre\lib\jfr.jar,G:\Program Files\Java\jdk1.8.0_45\jre\classes,G:\Program Files\Java\jdk1.8.0_45\jre\lib\ext\access-bridge-64.jar,G:\Program Files\Java\jdk1.8.0_45\jre\lib\ext\cldrdata.jar,G:\Program Files\Java\jdk1.8.0_45\jre\lib\ext\dnsns.jar,G:\Program Files\Java\jdk1.8.0_45\jre\lib\ext\jaccess.jar,G:\Program Files\Java\jdk1.8.0_45\jre\lib\ext\jfxrt.jar,G:\Program Files\Java\jdk1.8.0_45\jre\lib\ext\localedata.jar,G:\Program Files\Java\jdk1.8.0_45\jre\lib\ext\nashorn.jar,G:\Program Files\Java\jdk1.8.0_45\jre\lib\ext\sunec.jar,G:\Program Files\Java\jdk1.8.0_45\jre\lib\ext\sunjce_provider.jar,G:\Program Files\Java\jdk1.8.0_45\jre\lib\ext\sunmscapi.jar,G:\Program Files\Java\jdk1.8.0_45\jre\lib\ext\sunpkcs11.jar,G:\Program Files\Java\jdk1.8.0_45\jre\lib\ext\zipfs.jar,.,G:\Program Files\Java\jdk1.8.0_45\lib]
[正在加载ZipFileIndexFileObject[G:\Program Files\Java\jdk1.8.0_45\lib\ct.sym(META-INF/sym/rt.jar/java/lang/Object.class)]]
[正在加载ZipFileIndexFileObject[G:\Program Files\Java\jdk1.8.0_45\lib\ct.sym(META-INF/sym/rt.jar/java/lang/String.class)]]
[正在检查jdk.test.classLoader.Test]
[正在加载ZipFileIndexFileObject[G:\Program Files\Java\jdk1.8.0_45\lib\ct.sym(META-INF/sym/rt.jar/java/io/Serializable.class)]]
[正在加载ZipFileIndexFileObject[G:\Program Files\Java\jdk1.8.0_45\lib\ct.sym(META-INF/sym/rt.jar/java/lang/AutoCloseable.class)]]
[正在加载ZipFileIndexFileObject[G:\Program Files\Java\jdk1.8.0_45\lib\ct.sym(META-INF/sym/rt.jar/java/lang/Byte.class)]]
[正在加载ZipFileIndexFileObject[G:\Program Files\Java\jdk1.8.0_45\lib\ct.sym(META-INF/sym/rt.jar/java/lang/Character.class)]]
[正在加载ZipFileIndexFileObject[G:\Program Files\Java\jdk1.8.0_45\lib\ct.sym(META-INF/sym/rt.jar/java/lang/Short.class)]]
[正在加载ZipFileIndexFileObject[G:\Program Files\Java\jdk1.8.0_45\lib\ct.sym(META-INF/sym/rt.jar/java/lang/Long.class)]]
[正在加载ZipFileIndexFileObject[G:\Program Files\Java\jdk1.8.0_45\lib\ct.sym(META-INF/sym/rt.jar/java/lang/Float.class)]]
[正在加载ZipFileIndexFileObject[G:\Program Files\Java\jdk1.8.0_45\lib\ct.sym(META-INF/sym/rt.jar/java/lang/Integer.class)]]
[正在加载ZipFileIndexFileObject[G:\Program Files\Java\jdk1.8.0_45\lib\ct.sym(META-INF/sym/rt.jar/java/lang/Double.class)]]
[正在加载ZipFileIndexFileObject[G:\Program Files\Java\jdk1.8.0_45\lib\ct.sym(META-INF/sym/rt.jar/java/lang/Boolean.class)]]
[正在加载ZipFileIndexFileObject[G:\Program Files\Java\jdk1.8.0_45\lib\ct.sym(META-INF/sym/rt.jar/java/lang/Void.class)]]
[正在加载ZipFileIndexFileObject[G:\Program Files\Java\jdk1.8.0_45\lib\ct.sym(META-INF/sym/rt.jar/java/lang/System.class)]]
[正在加载ZipFileIndexFileObject[G:\Program Files\Java\jdk1.8.0_45\lib\ct.sym(META-INF/sym/rt.jar/java/io/PrintStream.class)]]
[正在加载ZipFileIndexFileObject[G:\Program Files\Java\jdk1.8.0_45\lib\ct.sym(META-INF/sym/rt.jar/java/io/FilterOutputStream.class)]]
[正在加载ZipFileIndexFileObject[G:\Program Files\Java\jdk1.8.0_45\lib\ct.sym(META-INF/sym/rt.jar/java/io/OutputStream.class)]]
[正在加载ZipFileIndexFileObject[G:\Program Files\Java\jdk1.8.0_45\lib\ct.sym(META-INF/sym/rt.jar/java/lang/Comparable.class)]]
[正在加载ZipFileIndexFileObject[G:\Program Files\Java\jdk1.8.0_45\lib\ct.sym(META-INF/sym/rt.jar/java/lang/CharSequence.class)]]
[正在加载ZipFileIndexFileObject[G:\Program Files\Java\jdk1.8.0_45\lib\ct.sym(META-INF/sym/rt.jar/java/lang/Appendable.class)]]
[正在加载ZipFileIndexFileObject[G:\Program Files\Java\jdk1.8.0_45\lib\ct.sym(META-INF/sym/rt.jar/java/io/Closeable.class)]]
[正在加载ZipFileIndexFileObject[G:\Program Files\Java\jdk1.8.0_45\lib\ct.sym(META-INF/sym/rt.jar/java/io/Flushable.class)]]
[已写入RegularFileObject[Test.class]]
[共 1743 毫秒]
$ javap -verbose Test
警告: 二进制文件Test包含jdk.test.classLoader.Test
Classfile /D:/workspace/Test/src/jdk/test/classLoader/Test.class
Last modified 2018-4-14; size 520 bytes
MD5 checksum 601c8f82d6b3a2ac4ce7c5b04e013d92
Compiled from "Test.java"
public class jdk.test.classLoader.Test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #8.#18 // java/lang/Object."<init>":()V
#2 = Fieldref #19.#20 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #21 // hello world
#4 = Methodref #22.#23 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #24 // jdk/test/classLoader/Test
#6 = Methodref #5.#18 // jdk/test/classLoader/Test."<init>":()V
#7 = Methodref #5.#25 // jdk/test/classLoader/Test.hello:()V
#8 = Class #26 // java/lang/Object
#9 = Utf8 <init>
#10 = Utf8 ()V
#11 = Utf8 Code
#12 = Utf8 LineNumberTable
#13 = Utf8 hello
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 SourceFile
#17 = Utf8 Test.java
#18 = NameAndType #9:#10 // "<init>":()V
#19 = Class #27 // java/lang/System
#20 = NameAndType #28:#29 // out:Ljava/io/PrintStream;
#21 = Utf8 hello world
#22 = Class #30 // java/io/PrintStream
#23 = NameAndType #31:#32 // println:(Ljava/lang/String;)V
#24 = Utf8 jdk/test/classLoader/Test
#25 = NameAndType #13:#10 // hello:()V
#26 = Utf8 java/lang/Object
#27 = Utf8 java/lang/System
#28 = Utf8 out
#29 = Utf8 Ljava/io/PrintStream;
#30 = Utf8 java/io/PrintStream
#31 = Utf8 println
#32 = Utf8 (Ljava/lang/String;)V
{
public jdk.test.classLoader.Test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 4: 0
line 5: 4
public void hello();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String hello world
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 8: 0
line 9: 8
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #5 // class jdk/test/classLoader/Test
3: dup
4: invokespecial #6 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #7 // Method hello:()V
12: return
LineNumberTable:
line 12: 0
line 13: 8
line 14: 12
}
SourceFile: "Test.java"
初始化
初始化时类加载的最后阶段,初始化阶段是执行类构造器<clinit>()
方法的过程。
初始化的执行时机,有5种情况会触发初始化:
- 遇到new、getstatic、putstatic或invokestatic指令时,如果相关的类没有初始化,会触发初始化。场景有new创建对象,读写类的静态变量或调用类的静态方法。注意如果是编译时期加入常量池的静态变量(final static 常量),那么这个静态变量与定义它的类已经剥离了关系,这种调用不会触发该类的初始化。
例如:
public class InitOrder {
static {
System.out.println("initOrder init!");
}
public static void main(String[] args) {
System.out.println("before get final static var");
// Demo.A在编译时加入了常量池,是共享的数据,访问不会触发Demo类的初始化
int a = Demo.A;
System.out.println("get final static var end");
System.out.println("before get static var");
// 非常量的访问 会触发初始化
int b = Demo.a;
System.out.println("get static var end");
}
}
class Demo {
static int a = 100;
final static int A = 1000;
static {
System.out.println("Demo init");
}
}
// 结果
// initOrder init!
// before get final static var
// get final static var end
// before get static var
// Demo init
// get static var end
- 使用Java反射机制的时候,如果类没有初始化,会触发初始化。
- 初始化一个类时,会先初始化其父类(如果父类没有初始化的话)
- JVM启动时,程序的入口类会先初始化
- 使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,且这个句柄对应的类没有进行过初始化,则需要先进行初始化。
有且仅有以上5种场景会触发初始化。
<clinit>() <init>()
<clinit>()
方法是由编译器自动收集类中静态变量的赋值语句以及静态代码块中定义的语句合并产生的,且内部的语句的顺序是由定义的顺序决定的,后面的语句可以访问前面定义的变量,反过来可以赋值,但是不能访问。
static{
i=100;//可以,但是没有意义
System.out.print(i); //不可以
}
static int i = 2 ;
public static void main(String[] args) {
System.out.println(i); // 2
}
<clinit>()
和<init>()
是不同的,前者对应的是类,后者对应的是实例,即前一个是Class的构造器(是编译器生成的),后者是实例对象的构造器(也就是我们定义或继承的构造函数)。且虚拟机会保证子类的<clinit>()
执行之前,其父类的<clinit>()
一定执行完成,无需显式指定。所以第一个执行<clinit>()
的类是java.lang.Object
;- 因为父类比子类先执行
<clinit>()
,所以父类的静态变量和静态代码块是先于子类执行的。- 如果一个类或者接口中没有静态变量或静态代码块,编译器可以不生成
<clinit>()
- 接口中没有静态代码块,但是可以有静态变量。所以可以有
<clinit>()
的初始化动作,但是接口和类不同之处在于接口不需要先执行父接口的<clinit>()
方法。- 虚拟机会保证一个类的
<clinit>()
方法在多线程环境中能被正确的同步,利用这一点可以实现线程安全的单例模式。
初始化结束后,一个类就可以被正常的使用了。
参考文献
《深入理解Java虚拟机》周志明
The end.