1. 类加载机制

在如下几种情况下,Java 虚拟机将结束生命周期

  1. 执行了 System.exit() 方法
  2. 程序正常执行结束
  3. 程序在执行的过程中遇到了异常或则错误而异常终止
  4. 由于操作系统出现了错误,导致 Java 虚拟机进程结束

Java 程序对类的使用方式可以分为两种:

主动使用

  1. 创建类的实例
  2. 访问某个类或接口的静态变量,或则对该静态变量赋值
  3. 调用类的静态方法
  4. 反射(如 Class.forName(“com.cuzz.Test”))
  5. 初始化一个子类
  6. Java 虚拟机启动时被标明为启动类的类

被动使用

  • 所有的 Java 虚拟机实现必须在每个类或接口被 Java 程序首次主动使用时才初始化他们

我们来看一段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MyTest01 {

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

class Parent1 {
public static String str = "hello world";
static {
System.out.println("Parent1 static block");
}
}

class Child1 extends Parent1 {
static {
System.out.println("Child1 static block");
}
}

输出

1
2
Parent1 static block
hello world

对于静态代码块,只有定义该字段的类才会被初始化,这个 Child1.str 是子类调用父类的静态字段,所以子类不会被初始化,父类才会被初始化,这是对 Parent1 的主动使用,对于这个例子只是用了 Child1 的名字,并没有主动使用 Child1 这个类

我们在来看看有没有被加载到虚拟机中,在 VM options : -XX:+TraceClassLoading 在运行

1
2
3
4
5
6
7
...
[Loaded com.cuzz.jvm.classloader.Parent1 from file:/E:/project/learn-demo/demo-10-jvm-lecture/out/production/classes/]
[Loaded com.cuzz.jvm.classloader.Child1 from file:/E:/project/learn-demo/demo-10-jvm-lecture/out/production/classes/]
Parent1 static block
hello world
[Loaded java.lang.Shutdown from E:\deployer\jdk8\jre\lib\rt.jar]
[Loaded java.lang.Shutdown$Lock from E:\deployer\jdk8\jre\lib\rt.jar]

发现这两个类已经被加载到虚拟中

再看一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MyTest01 {

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

class Parent1 {
public static String str = "hello world";
static {
System.out.println("Parent1 static block");
}
}

class Child1 extends Parent1 {
public static String str2 = "welcome";
static {
System.out.println("Child1 static block");
}
}

输出

1
2
3
Parent1 static block
Child1 static block
welcome

当我们初始一个子类,我们会先初始化父类,所以会线输出父类的静态代码块

如果我们加上 final 变为常量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @Author: zzxx
* @Date: 2020/4/10 19:16
* @Description:
*/
public class MyTest02 {
public static void main(String[] args) {
System.out.println(Parent2.str);
}
}
class Parent2 {
public static final String str = "hello world";
static {
System.out.println("Parent2 static block");
}
}

输出

1
hello world

常量在编译阶段会存入到调用这个常量的方法所在的类的常量池中(也就是说会存入MyTest02这个类中),本质上,调用类并没有直接引用到定义常量的类,因此并不会触发定义常量的类的初始化

注意:这里指的是将常量存放到了 MyTest02 的常量池中,之后 MyTest02 与 Parent2 就没有任何关系了,甚至我们可以将 Parent 的 class 文件删除

我们进入 classes 目录下使用:javap -c com.cuzz.jvm.classloader.MyTest02 命令反编译一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Compiled from "MyTest02.java"
public class com.cuzz.jvm.classloader.MyTest02 {
public com.cuzz.jvm.classloader.MyTest02();// (1)
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return

public static void main(java.lang.String[]);
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 (2) // String hello world (3)
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}

1.是构造方法

2.ldc 助记符表示将 int,float 或 String 类型的值从常量池中推送至栈顶

3.可以看出Parent2.str 已经转化为 hello world

注:当 int 取值-15采用iconst指令,取值-128127采用 bipush 指令,取值-3276832767采用 sipush 指令,取值-21474836482147483647采用 ldc 指令

我们在看一个例子

1
2
3
4
5
6
7
8
9
10
11
public class MyTest03 {
public static void main(String[] args) {
System.out.println(Parent3.str);
}
}
class Parent3 {
public static final String str = UUID.randomUUID().toString();
static {
System.out.println("Parent3 static block");
}
}

输出

1
2
Parent3 static block
bee2f54d-8960-46d0-b5d7-02666fcf4a14

相比于上一个例子,我们发现输出了静态代码块,说明 Parent3 这个类被初始化了,当一个常量的值并非编译期间可以确定的,那么器值就不会放到调用类的常量池中,这是在程序运行时,会导致主动使用这个常量所在的类,会导致这给类初始化

再看一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MyTest04 {
public static void main(String[] args) {
Parent4[] parent4s = new Parent4[1];
System.out.println("---------");
System.out.println(parent4s.getClass());
System.out.println(parent4s.getClass().getSuperclass());
System.out.println("---------");
int[] ints = new int[1];
System.out.println(ints.getClass());
System.out.println(ints.getClass().getSuperclass());
}
}

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

输出

1
2
3
4
5
6
7
8
---------
class [Lcom.cuzz.jvm.classloader.Parent4;
class java.lang.Object
---------
class [I
class java.lang.Object

Process finished with exit code 0

对于数组实例来说,其类型是由 JVM 在运行期动态生成的,表示为 [Lcom.cuzz.jvm.classloader.Parent4 这种形式,动态生成的类型,其父类型就是 Object

对于数组来说,JavaDoc 经常将构成的数组元素称为 Component,实际上就是将数组降低一个维度的类型

我们使用 javap -c com.cuzz.jvm.classloader.MyTest04 进行反编译

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
39
40
41
public class com.cuzz.jvm.classloader.MyTest04 {
public com.cuzz.jvm.classloader.MyTest04();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return

public static void main(java.lang.String[]);
Code:
0: iconst_1
1: anewarray #2 // class com/cuzz/jvm/classloader/Parent4
4: astore_1
5: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
8: ldc #4 // String ---------
10: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
13: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
16: aload_1
17: invokevirtual #6 // Method java/lang/Object.getClass:()Ljava/lang/Class;
20: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
23: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
26: aload_1
27: invokevirtual #6 // Method java/lang/Object.getClass:()Ljava/lang/Class;
30: invokevirtual #8 // Method java/lang/Class.getSuperclass:()Ljava/lang/Class;
33: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
36: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
39: ldc #4 // String ---------
41: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
44: iconst_1
45: newarray int
47: astore_2
48: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
51: aload_2
52: invokevirtual #6 // Method java/lang/Object.getClass:()Ljava/lang/Class;
55: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
58: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
61: aload_2
62: invokevirtual #6 // Method java/lang/Object.getClass:()Ljava/lang/Class;
65: invokevirtual #8 // Method java/lang/Class.getSuperclass:()Ljava/lang/Class;
68: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
71: return
}

里面有两个助记符

  • anewarray :表示创建一个引用类型的(如类、接口、数组)数组,并将其值压入栈顶
  • newarray:表示创建一个指定的原始类型(如int、float、char等)数组,并将其引用值压入栈顶

下一个例子

1
2
3
4
5
6
7
8
9
10
11
12
public class MyTest05 {
public static void main(String[] args) {
System.out.println(Child5.j);
}
}
interface Parent5 {
int i = 5;
}

interface Child5 extends Parent5 {
int j = 55;
}

编译之后我们把 Parent5.class 文件删掉,还能打印出 55,说明当一个接口在初始化时,并不要求其父接口都完成初始化,如果我们把 Child5.class 文件也删掉,也能打印出 55,原来接口中的修饰符默认为 public static final 说明接口中的值是一个常量,不需要加载到 JVM 中,也就没有初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MyTest05 {
public static void main(String[] args) {
System.out.println(Child5.j);
}
}
interface Parent5 {
public static Thread thread = new Thread() {
{
System.out.println("Parent5 static block");
}
};
}

class Child5 implements Parent5 {
public static int j = 55;
}

此时也也是输出 55 ,也没有初始化 Child5 接口 Parent5

下一例子

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
public class MyTest06 {
public static void main(String[] args) {
Singleton singleton = Singleton.newSingleton();
System.out.println(Singleton.counter1);
System.out.println(Singleton.counter2);
}
}

class Singleton {

public static int counter1;

private static Singleton singleton = new Singleton();

private Singleton() {
counter1++; // counter1 = 1
counter2++; // counter2 = 1
}

public static int counter2 = 0; // 此时又把值赋值为 0

public static Singleton newSingleton() {
return singleton;
}
}

此时输出

1
2
3
1
0

为什么会这样呢,准备阶段 counter1 和 counter2 的初始值都是 0 ,初始化阶段从上往下赋值,后面 counter2 又赋值为 0

我们再来回顾一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MyTest09 {
static {
System.out.println("MyTest09 static block");
}
public static void main(String[] args) {
System.out.println(Child9.j);
}
}

class Parent9 {
public static int i = 9;
static {
System.out.println("Parent9 static block");
}
}

class Child9 extends Parent9 {
public static int j = 99;
static {
System.out.println("Child9 static block");
}
}

输出

1
2
3
4
MyTest09 static block
Parent9 static block
Child9 static block
99

我们多输出点信息

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
public class MyTest09 {
static {
System.out.println("MyTest09 static block");
}
public static void main(String[] args) {
Parent9 parent9; // 不会初始化
System.out.println("-------------");
parent9 = new Parent9();
System.out.println("-------------");
System.out.println(Parent9.i);
System.out.println("-------------");
System.out.println(Child9.j);

}
}

class Parent9 {
public static int i = 9;
static {
System.out.println("Parent9 static block");
}
}

class Child9 extends Parent9 {
public static int j = 99;
static {
System.out.println("Child9 static block");
}
}

输出结果

1
2
3
4
5
6
7
8
MyTest09 static block
-------------
Parent9 static block
-------------
9
-------------
Child9 static block
99

在看一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MyTest12 {
public static void main(String[] args) throws Exception{
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
Class<?> clazz = classLoader.loadClass("com.cuzz.jvm.classloader.CL");
System.out.println("--------------");
clazz = Class.forName("com.cuzz.jvm.classloader.CL");
}
}

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

输出

1
2
--------------
CL static block

说明调用 ClassLoader 类的 loadClass 方法加载一个类,并不是对类的主动使用,不会导致类的初始化,而通过 Class.forName 方法是通过反射机制,会对类初始化