深入理解JVM-类加载

一、类加载过程

Java中类的加载过程主要分为:加载、连接、初始化三个大阶段,这三个阶段都是在程序运行时完成的。

类加载各阶段解析:

  1. 加载:查找类的二进制文件并将.class文件从磁盘加载到内存中
  2. 连接:连接又分为三个小阶段
    • 验证:验证被加载的.class文件的正确性。(确保.class文件没有被篡改并且符合JVM规范)
    • 准备:为类的静态变量分配内存空间并将静态变量初始化为「默认值」。(此时还没有对象只有类,只涉及静态变量的操作)
    • 解析:在类的常量池中寻找类、接口、字段和方法的符号引用,把类中的符号引用转换为直接引用。
  3. 初始化:为类的静态变量赋值正确的初始值。(人为初始化的值)
  4. 使用
  5. 卸载

![image.png](https://cdn.nlark.com/yuque/0/2019/png/186518/1575708884725-a9a6089e-b1eb-492f-996f-6c534a0b34db.png#align=left&display=inline&height=271&name=image.png&originHeight=398&originWidth=1059&size=31027&status=done&style=none&width=722)

二、类的加载

类加载是指将类的.class文件中的二进制数读入到内存中,并将其放在运行时数据区的方法区中,然后在内存中创建一个java.lang.Class对象(HotSpot将这个对象放在了方法区中),用于封装类在方法区内的数据结构。

加载.class方式:

  1. 从本地文件系统直接加载。
  2. 从网络下载.class文件。
  3. 从zip、jar归档文件中读取.class文件。
  4. 从专有数据库中读取。
  5. 将java源文件动态编译成.class文件。(动态代理)

类加载器并不需要等到类的被首次主动使用才加载类。

JVM允许类加载器在预测某个类将要被首次使用时预先加载,如果预先加载过程中遇到class文件缺失或者错误,类加载器必须在类首次主动使用时才报告错误。

如果类一直没有被首次主动使用那么类加载器不会报告错误。

三、类的连接

类被加载后,就进入连接阶段,连接是将已读入到内存的类的二进制文件合并到虚拟机运行时环境中

  1. 类的验证:

    1. 类文件的结构检查
    2. 语义检查
    3. 字节码验证
    4. 二进制兼容性验证
  2. 类的准备:

在准备阶段Java虚拟机为类的静态变量分配内存,并设置默认的初始值。

四、类的初始化

在类的初始化阶段,Java虚拟机会执行类的初始化语句,为类的静态变量赋予初始值,静态变量的初始化途径有两种:(1)在静态变量声明处进行初始化,(2)在静态代码块中初始化。

静态变量的声明语句和静态代码块都是类的初始化语句,Java虚拟机会按照初始化语句在类文件中的先后顺序依次执行初始化。

Java虚拟机必须在类或接口被「首次主动使用」时,才会初始化,被动使用不会初始化,并且初始化只有「首次」主动使用时初始化一次,且仅一次。

主动使用情况:(以下情况会使类初始化)

  1. 创建类的对象。
  2. 访问类或接口的静态变量,或者对类或接口的静态变量赋值。(getstatic或putstatic)
  3. 调用类的静态方法。(invokestatic)
  4. 反射。(Class.forName(“”))
  5. 初始化一个类的子类。
  6. Java虚拟机启动时被标注为启动类的类。(包含main函数的类)

除以上情况外,其他使用类的情况都不会导致类的初始化,但可能会加载类。

Java虚拟机初始化一个类时,要求它的所有父类都已经被初始化,但是这条规则对接口不适用

  1. 在初始化一个类时,并不会初始化它实现的接口。
  2. 在初始化一个接口时,并不会初始化它的父接口。

因此一个父接口并不会因为它的子接口或者实现类的初始化而初始化,只有当程序首次使用特定接口的静态变量时,才会导致接口的初始化。

只有当程序访问的静态变量或静态方法确实在当前类或接口中定义时,才可以认为是对类或接口的主动使用。另外,调用ClassLoader类中的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化。

类的初始化步骤:

  1. 假如这个类还没有被加载和连接,那先进行加载和连接。
  2. 假如存在直接父类,并且这个父类还没有被初始化,那就先初始化直接父类。
  3. 假如类中存在初始化语句,那就依次执行这些初始化语句

五、示例

示例一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* 运行结果:
* Parent static block
* Hello World
*
* 解析:
* Child中的静态代码块没有执行,说明子类Child没有初始化。
* Parent中的静态代码块被执行,说明父类Parent被初始化。
* 对于静态字段来说,只有直接定义了该字段的类才会被初始化,示例中仅仅是通过子类的名字调用父类中定义的
* 静态变量,相当于对父类Parent中的静态变量str的主动使用
*/
public class MyTest {
public static void main(String[] args) {
System.out.println(Child.str);
}
}

class Parent {
public static String str = "Hello World";

static {
System.out.println("Parent static block");
}
}

class Child extends Parent {

static {
System.out.println("Child static block");
}
}

示例二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* 运行结果:
* Parent static block
* Child static block
* Welcome
*
* 解析:
* 对Child中的静态变量主动使用导致Child的初始化,根据初始化子类时必须先初始化父类的原则
* 所以Parent会被初始化
*/
public class MyTest {
public static void main(String[] args) {
System.out.println(Child.str2);
}
}

class Parent {
static {
System.out.println("Parent static block");
}
}

class Child extends Parent {
public static String str2 = "Welcome";

static {
System.out.println("Child static block");
}
}

示例三:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* 运行结果:
* Hello World
*
* 解析:
* 对于final常量的调用并没有导致Parent的初始化,
* 原因是:在编译期final常量会被存入到调用这个常量的方法所在的类的常量池中。
* 本质上调用类并没有直接引用到定义常量的类,因此并不会触发定义常量的类的初始化。
* 本例中str常量在编译期就被存到MyTest中的常量池中
*
* 特别情况,对于str2会有不同情况:
* Parent static block
* 622983f9-f9d4-40d2-ab17-8848c143f5e7
* 此处,常量str2在编译器并没有确定值,所以编译期虚拟机无法确定str2的值,所以无法将str2
* 存入到MyTest的常量池中,所以会主动使用Parent,导致Parent初始化。
*/
public class MyTest {

public static void main(String[] args) {
System.out.println(Parent.str2);
}
}

class Parent {
public static final String str = "Hello World";

public static final String str2 = UUID.randomUUID().toString();
static {
System.out.println("Parent static block");
}
}

示例四:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 运行结果:
* Parent static block
* +++++++++++++++++++++
*
* 解析:
* 这里是创建一个类的对象,是对类的主动使用会导致类的初始化。
* 并且要首次主动使用,且只初始化一次
*/
public class MyTest1 {

public static void main(String[] args) {
Parent parent = new Parent();
System.out.println("+++++++++++++++++++++");
Parent parent2 = new Parent();
}
}

class Parent {
static {
System.out.println("Parent static block");
}
}

示例五:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* 运行结果:
* 没有输出
*
* 解析:
* 对于数组来说,其类型是由虚拟机在运行时动态生成的,其类型表示为[Lcom.xxx.xxx
* 动态生成的类型其父类为Object。
* 原生类型的数组类型分别为:
* int -- [I
* short -- [S
* boolean -- [Z
* byte -- [B
* float -- [F
* char -- [C
*/
public class MyTest1 {

public static void main(String[] args) {
Parent[] parent = new Parent[0];
}
}

class Parent {
static {
System.out.println("Parent static block");
}
}

示例六:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 此例删除Parent的class文件后程序执行正常。
* 即使删除Child的class也正常运行。
*
* 当一接口初始化时,并不要求其父接口都完成初始化,只有在真正使用到父接口时才会初始化。
* 接口中的成员变量都是默认是public static final的所以会被存到MyTest的常量池中
*
*/
public class MyTest {
public static void main(String[] args) {
System.out.println(Child.b);
}
}

interface Parent {

public static int a = 1;
}

interface Child extends Parent {

public static int b = 2;
}

示例七:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* 运行结果:
* a: 1
* b: 0
*
* 解析:
* Singleton.getInstance();调用静态方法会导致类的初始化,类初始化是由上到下初始化静态变量
* 在准备阶段:a初始化为默认值0,b初始化为默认值0,singleton被初始化为默认值null
* 在初始化阶段:a依旧是默认值0,singleton初始化会执行构造方法,a和b都变成1,然后b初始化
* 赋值为0,所以最终结果a=1,b=0
*
*/
public class MyTest1 {
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();

System.out.println("a: " + Singleton.a);
System.out.println("b: " + Singleton.b);
}
}

class Singleton {

public static int a;

private static Singleton singleton = new Singleton();

private Singleton() {
a++;
b++;
}

public static int b = 0;

public static Singleton getInstance() {
return singleton;
}
}
坚持原创技术分享,您的支持将鼓励我继续创作!