- class字节码文件加载到内存过程:装载,链接,初始化
- class文件加载时机
- 链接又分为三个阶段:验证,准备,解析
- 初始化时机
- 对象创建时代码执行顺序
- ClassLoader的主要作用就是用来将class字节码加载到内存中,那JVM加载class文件的具体过程是怎样的呢?
- 一个class文件被加载到内存中需要经过3大步:装载,链接,初始化
- 其中链接又细分为:验证,准备,解析3小步
1.class文件加载到内存过程.png
1.装载
- 装载是指Java虚拟机查找.class文件并生成字节流,然后根据字节流创建java.lang.Class对象的过程
- 装载过程主要完成3件事
- ClassLoader通过一个类的全限定名(包名+类名)来查找.class文件,并生成二进制字节流
- 其中class字节码文件的来源不一定是.class文件,也可以市jar包,zip包,甚至市来源于网络的字节流
- 把.class文件的各个部分分别解析(parse)为JVM内部特定的数据结构,并存储在方法区
- 在内存中创建一个java.lang.Class类型的对象:
- 接下来程序在运行过程中所有对该类的访问都通过这个类对象,也就是这个Class类型的类对象是提供给外界访问该类的接口
- ClassLoader通过一个类的全限定名(包名+类名)来查找.class文件,并生成二进制字节流
加载时机
- 一个项目经过编译之后,往往会生成大量的.class文件。当程序运行时,jvm并不会一次性的将所有.class文件全部加载到内存中。以下两种情况一般会对class进行装载操作
- 隐式装载:在程序运行过程中,当碰到通过new等方式生成对象时,系统会隐式调用ClassLoader去装载对应的class到内存中
- 显示装载:在编写源代码时,主动调用Class.forName()等方法也会进行class装载操作,这种方式通常称为显示装载
2.链接
- 链接过程分为3步:验证,准备,解析
2.1.验证
- 验证的目的是为了确保.class文件的字节流中包含的信息符合虚拟机的要求,并且不会危及虚拟机本身的安全。主要包含以下几个方面的检验
- 文件格式检验:检验字节流是否符合class文件格式的规范,并且能被当前版本的虚拟机处理
- 元数据检验:对字节码描述的信息进行语义分析,以保证其描述的内容符合Java语言规范的要求
- 字节码检验:通过数据流和控制流分析,确定程序语义是合法,符合逻辑的
- 符号引用检验:符号引用检验可以看作是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验
实例:
public class Foo {
public static void main(String[] args) {
new Foo().print();
}
public void print() {
int superCode = super.hashCode();
System.out.println("superCode is:" + superCode);
int thisCode = hashCode();
System.out.println("thisCode is:" + thisCode);
}
@Override
public int hashCode() {
return 123;
}
}
- 对应javap 字节码内容:
.../Foo.class
Last modified 2020-12-10; size 853 bytes
MD5 checksum cfb1269514e4bd9bb8b8818267ce1152
Compiled from "Foo.java"
public class com.timmy.javalib._3class.Foo
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #16.#28 // java/lang/Object."<init>":()V
#2 = Class #29 // com/timmy/ks_tree/test/Foo
#3 = Methodref #2.#28 // com/timmy/ks_tree/test/Foo."<init>":()V
#4 = Methodref #2.#30 // com/timmy/ks_tree/test/Foo.print:()V
#5 = Methodref #16.#31 // java/lang/Object.hashCode:()I
#6 = Fieldref #32.#33 // java/lang/System.out:Ljava/io/PrintStream;
#7 = Class #34 // java/lang/StringBuilder
#8 = Methodref #7.#28 // java/lang/StringBuilder."<init>":()V
#9 = String #35 // superCode is:
#10 = Methodref #7.#36 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#11 = Methodref #7.#37 // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
#12 = Methodref #7.#38 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#13 = Methodref #39.#40 // java/io/PrintStream.println:(Ljava/lang/String;)V
#14 = Methodref #2.#31 // com/timmy/ks_tree/test/Foo.hashCode:()I
#15 = String #41 // thisCode is:
#16 = Class #42 // java/lang/Object
#17 = Utf8 <init>
#18 = Utf8 ()V
#19 = Utf8 Code
#20 = Utf8 LineNumberTable
#21 = Utf8 main
#22 = Utf8 ([Ljava/lang/String;)V
#23 = Utf8 print
#24 = Utf8 hashCode
#25 = Utf8 ()I
#26 = Utf8 SourceFile
#27 = Utf8 Foo.java
#28 = NameAndType #17:#18 // "<init>":()V
#29 = Utf8 com/timmy/ks_tree/test/Foo
#30 = NameAndType #23:#18 // print:()V
#31 = NameAndType #24:#25 // hashCode:()I
#32 = Class #43 // java/lang/System
#33 = NameAndType #44:#45 // out:Ljava/io/PrintStream;
#34 = Utf8 java/lang/StringBuilder
#35 = Utf8 superCode is:
#36 = NameAndType #46:#47 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#37 = NameAndType #46:#48 // append:(I)Ljava/lang/StringBuilder;
#38 = NameAndType #49:#50 // toString:()Ljava/lang/String;
#39 = Class #51 // java/io/PrintStream
#40 = NameAndType #52:#53 // println:(Ljava/lang/String;)V
#41 = Utf8 thisCode is:
#42 = Utf8 java/lang/Object
#43 = Utf8 java/lang/System
#44 = Utf8 out
#45 = Utf8 Ljava/io/PrintStream;
#46 = Utf8 append
#47 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#48 = Utf8 (I)Ljava/lang/StringBuilder;
#49 = Utf8 toString
#50 = Utf8 ()Ljava/lang/String;
#51 = Utf8 java/io/PrintStream
#52 = Utf8 println
#53 = Utf8 (Ljava/lang/String;)V
{
public com.timmy.javalib._3class.Foo();
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 3: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: new #2 // class com/timmy/ks_tree/test/Foo
3: dup
4: invokespecial #3 // Method "<init>":()V
7: invokevirtual #4 // Method print:()V
10: return
LineNumberTable:
line 6: 0
line 7: 10
public void print();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=3, args_size=1
0: aload_0
1: invokespecial #5 // Method java/lang/Object.hashCode:()I
4: istore_1
5: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
8: new #7 // class java/lang/StringBuilder
11: dup
12: invokespecial #8 // Method java/lang/StringBuilder."<init>":()V
15: ldc #9 // String superCode is:
17: invokevirtual #10 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
20: iload_1
21: invokevirtual #11 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
24: invokevirtual #12 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
30: aload_0
31: invokevirtual #14 // Method hashCode:()I
34: istore_2
35: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
38: new #7 // class java/lang/StringBuilder
41: dup
42: invokespecial #8 // Method java/lang/StringBuilder."<init>":()V
45: ldc #15 // String thisCode is:
47: invokevirtual #10 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
50: iload_2
51: invokevirtual #11 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
54: invokevirtual #12 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
57: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
60: return
LineNumberTable:
line 10: 0
line 11: 5
line 13: 30
line 14: 35
line 15: 60
public int hashCode();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: bipush 123
2: ireturn
LineNumberTable:
line 19: 0
}
SourceFile: "Foo.java"
2.2.准备
准备是链接的第2步,这一阶段的主要目的是为类中的静态变量(类变量)分配内存,并为其设置“0值”,比如:
public static int value = 100;
- 在准备阶段,jvm会为value分配内存,并将其设置为0.
- 而真正的值100是在初始化阶段设置,并且此阶段进行内存分配的仅包括类变量,而不包括实例变量
- 实例变量将会在对象实例化时随着对象一起分配在java堆中
- 静态常量
public static final int value = 100;
静态常量在准备阶段就为value分配内存,并设置为100.
2.3.解析
- 解析是链接的最后一步,这一阶段的任务是把常量池中的符号引用转换为直接引用,也就是具体的内存地址。
- 在这一阶段,jvm会将常量池中的类,接口名,字段名,方法名等转换为具体的内存地址
如在Foo类中的main方法中调用Foo.print()方法
#4 = Methodref #2.#30 // com/timmy/ks_tree/test/Foo.print:()V
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: new #2 // class com/timmy/ks_tree/test/Foo
3: dup
4: invokespecial #3 // Method "<init>":()V
7: invokevirtual #4 // Method print:()V
10: return
LineNumberTable:
line 6: 0
line 7: 10
- 在main方法中通过invokevirtual指令调用了print方法
- “Foo.print:()V”就是一个符号引用,当main方法执行到此处时,会将符号引用“Foo.print:()V”解析(resolve)成直接引用,可以将直接引用理解为方法真正的内存地址。
3.初始化
- 这时class加载的最后一步,这一阶段是执行类构造器<clinit>方法的过程,并真正初始化类变量,比如:
public static int value = 100;
在准备阶段value被分配内存并设置为0,在初始化阶段value就会被设置为100
3.1.初始化时机
- 对于装载阶段,jvm没有规范何时具体执行,但是对于初始化,jvm规范中严格规定了class初始化的时机,主要又以下几种情况会触发class的初始化:
- 虚拟机启动时,初始化包含main方法的主类;
- 遇到new指令创建对象实例时,如果目标对象类没有被初始化则进行初始化操作
- 当遇到访问静态方法或者静态字段的指令时,如果目标对象类没有被初始化则进行初始化操作
- 子类的初始化过程如果方法其父类还没有进行过初始化,则需要先触发其父类的初始化
- 使用反射API进行反射调用时,如果类没有进行过初始化则需要先触发器初始化
- 第一次调用java.lang.invoke.MethodHandle实例时,需要初始化MethodHandler指向方法所在的
3.2.初始化类变量(static)
- 在初始化阶段,只会初始化与类相关的静态赋值语句和静态语句,也就是又static关键字修饰的信息,而没有static修饰的语句块在实例化对象的时候才会执行
public class ClassInit {
public static int value = 100;
//静态代码块在初始化阶段执行
static {
System.out.println("ClassInit static block ");
}
//非静态代码块只在创建对象实例时被执行
{
System.out.println("ClassInit not static block ");
}
}
1.调用静态变量只执行静态代码块:
public class Foo {
public static void main(String[] args) {
ClassInit.value = 2;
}
}
> Task :testlib:Foo.main()
ClassInit static block
2.new创建对象实例时执行非静态代码块:
public static void main(String[] args) {
ClassInit.value = 2;
ClassInit classInit = new ClassInit();
}
> Task :testlib:Foo.main()
ClassInit static block
ClassInit not static block
3.3.被动引用
- 上诉的6中情况在jvm中被称为主动引用,除此6种情况外所有引用类的方法都被称为被动引用。被动引用并不会触发class的初始化
- 最典型的例子就是子类中调用父类的静态变量
public class Parent {
public static int value = 1;
static {
System.out.println("this is parent static block ");
}
}
public class Child extends Parent {
static {
System.out.println("this is child static block ");
}
}
- Child继承自Parent类,直接使用Child来访问Parent中的value值,不会初始化Child类
public class Foo {
public static void main(String[] args) {
Child.value = 3;
}
}
打印结果:
> Task :testlib:Foo.main()
this is parent static block
- 可以看出,Child中的静态代码块并没有被执行,jvm没有对Child执行初始化操作。
- 对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过子类Child来引用父类Parent中定义的静态字段,只会触发父类Parent的初始化而不会触发子类Child的初始化。
3.4.class初始化和对象的创建顺序
- 当在代码中使用new关键字创建一个类的实例对象时,类中的静态代码块,非静态代码块,构造函数之间的执行顺序如何呢?
public class ProcessOrder {
public static void main(String[] args) {
Parent p = new Child();
System.out.println("=======================");
p = new Child();
}
static class Child extends Parent {
static {
System.out.println("Child static block");
}
{
System.out.println("Child not-static block");
}
public Child() {
System.out.println("Child constructor");
}
}
static class Parent {
static {
System.out.println("parent static block");
}
{
System.out.println("parent not-static block");
}
public Parent() {
System.out.println("parent constructor");
}
}
}
日志打印:
> Task :testlib:ProcessOrder.main()
parent static block
Child static block
parent not-static block
parent constructor
Child not-static block
Child constructor
=======================
parent not-static block
parent constructor
Child not-static block
Child constructor
- 在main方法中执行了2此new Child()的操作,对象的初始化顺序如下:
静态变量/静态代码块 -》普通代码块 -》 构造函数
- 父类静态变量和静态代码块
- 子类静态变量和静态代码块
- 父类普通成员变量和普通代码块
- 父类构造函数
- 子类普通成员变量和普通代码块
- 子类构造函数











网友评论