深入理解JVM(一)-类加载器

一、类加载

  1. 在Java代码中,类型的加载、连接与初始化过程都是在程序运行期间完成的。
    • 类型:class、interface、enum等,这其中不涉及任何对象的内容,仅涉及类型。
    • 加载:多数情况(不是唯一)是将已经存在的字节码文件(class文件)加载到内存中。
    • 连接:将类与类之间的调用关系确定,对于字节码的验证校验也在加载连接阶段完成。—–符号引用转换成直接引用
    • 初始化:类的静态变量赋值。
  2. 加载、连接、初始化并不是严格按照顺序执行。

二、类加载器

  1. 加载类的工具,将字节码文件加载到内存中,每个类都要有类加载器加载到内存。
  2. JVM与程序生命周期,如下几种情况将导致JVM结束生命周期
    • 执行System.exit()方法。
    • 程序正常执行结束。
    • 程序执行过程中遇到了异常或者错误而异常终止。
    • 由于操作系统出现错误而导致虚拟机进程终止。—无法人为控制
  3. 类的加载、连接、初始化、使用、卸载
    • 加载:查找并加载类的二进制数据,类的class文件加载到内存。
    • 连接:
      • 验证:确保被加载的类的二进制文件正确性。
      • 准备:为类的静态变量分配内存,并将其初始化为默认值。(无论静态变量的实际值是多少,此时只会初始化为默认值)
      • 解析:把类中的符号引用转换为直接引用。
      • 初始化:为类的静态变量赋予正确的初始值。
      • 使用
      • 卸载:从内存清理类数据。
  4. Java程序对类的使用方式分为两种:
    • 主动使用
    • 被动使用
  5. 所有的Java虚拟机实现必须要在每个类或接口被Java程序首次主动使用才初始化。
    • 主动使用(7种)
      • 创建类的实例new。
      • 访问类或接口的静态变量或对静态变量赋值
      • 调用类的静态方法
      • 反射
      • 初始化类的子类
      • Java虚拟机启动时被标明为启动类的类(Java Test)
      • JDK1.7动态调用支持
  6. 被动使用不会导致类的初始化,并不意味着不会加载连接类。
    1
    2
    3
    class Test{
    public static int a = 1; //在准备阶段,先为a分配内存,并将a初始化为0,并不是1,1的值是初始化阶段赋值
    }

三、类的加载

  1. 类的加载是将将类的class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在内存中创建一个java.lang.Class对象(虚拟机规范为定义Class对象存放在哪里,HotSpot虚拟机将其放在方法区中)勇烈封装类在方法区内的数据结构。
  2. 加载class文件的方式(虚拟机规范并没有规定class文件从哪里加载)
    • 从本地系统中加载
    • 通过网络下载class文件
    • 从zip,jar等归档文件中加载class文件
    • 从专有数据库中提取class文件
    • 将Java源代码文件动态编译为class文件

四、实例

实例一

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
/**
* 对于静态字段,只有直接定义了该字段的类才会被初始化
* -XX:+TraceClassLoading,用于追踪类的加载信息并打印出来
*
* -XX:+<option>开启option选项
* -XX:-<option>关闭option选项
* -XX:<option>=<value>将option的值设置为value
*/
public class Test {
public static void main(String[] args) {
System.out.println(Child.strParent); //对strParent的主动使用,但没有主动使用Child,所以Child静态代码块不执行,Parent静态代码块执行
System.out.println(Child.strChild); //对于Child同上,但是初始化Child时要初始化父类,对子类的初始化要先初始化所有父类
}
}

class Parent {
public static String strParent = "parent hello world";

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

class Child extends Parent{
public static String strChild = "child hello world";

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

实例二 常量池和助记符相关

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
public class Test {

public static void main(String[] args) {
sqlString();
}

public static void sqlString(){
//助记符:ldc表示将int,float,String类型的常量值从常量池中推送到栈顶
//bipush表示将单字节(-128~127)的常量值从常量池推送到栈顶
//sipush将一个短整型(-32768~32767)的常量值从常量池推送至栈顶
//iconst_1表示将int类型的1常量值从常量池推送值栈顶,(只有iconst_m1, iconst_0, iconst_1~iconst_5,6之后用bipush)
System.out.println(Child.strParent); //对strParent的主动使用,但没有主动使用Child,所以Child静态代码块不执行,Parent静态代码块执行
}
}

//编译期的值是可以确定final常量的值
class Parent {
//final修饰的变量表示一个常量,在编译时,常量的值就会被放到调用常量的方法所在的类的常量池中,即常量被存到Test类的常量池中
//编译之后Test类和Parent类没有任何关系,甚至可以删除Parent的class文件,程序照样执行
//本质上调用类并没用直接引用到定义常量的类,因此并不会初始化定义常量的类
//所以静态代码块不会被执行
public static final String strParent = "parent hello world";

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

实例三

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//当一个常量值并非在编译期间可以确定,那么其值就不会被放到调用类的常量池中,这时在程序运行时,会导致主动使用这个常量所在的类,导致类的初始化
public class Test{
public static void main(String[] args) {
System.out.println(Parent.str);
}
}

class Parent {
//不是编译期常量,编译时不能确定str的值
public static final String str = UUID.randomUUID().toString();

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

实例四

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//对于数组实例来说,其类型是有JVM在运行时动态生表示为[Lcom.xxx.xxx.Parent
//动态生成的类型其父类型就是Object,对于数组来说,JavaDoc将数组的元素Component,实际上就是将数组降低一个维度后的类型
//助记符:
// anewarray:表示创建一个引用类型(如接口,类,数组)的数组,并将其引用值压入栈顶
// newarray:表示创建一个原始数据类型(如int、float、char等)的数组,并将其引用值压入栈顶
public class Test{
public static void main(String[] args) {
Parent parent1 = new Parent(); //会导致类的初始化,调用静态代码块

Parent parent2 = new Parent(); //不会导致类的初始化,不是“首次使用”

//创建一个数组并不表示对元素类型的主动使用,只是创建一个引用,这个引用类型时JVM运行时创建的一个类型
Parent[] parentArray = new Parent[1]; //不会导致类的初始化

int[] ints = new int[1]; //不会导致初始化,类型是[I
}
}

class Parent {

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

实例五

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//当一个接口初始化时,并不要求其父接口都完成初始化
//对于接口来说成员变量都是final的
//只有在真正使用到父接口的时候(如引用接口中定义的常量时),才会初始化
public class Test{
public static void main(String[] args) {
System.out.println(Child.b);
}
}

interface Parent {
int a = 5;
}

interface Child extends Parent {
int b = 6;
int c = new Random().nextInt(4);
}

实例六

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Test{
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
System.out.println("counter1:" + Singleton.counter1); //1
System.out.println("counter2:" + Singleton.counter2); //0
}
}

//从上到下初始化
class Singleton {
public static int counter1;

private static Singleton singleton = new Singleton();
public Singleton() {
counter1++;
counter2++; //此时在初始化阶段counter2变成1;
}

public static int counter2 = 0; //到初始化阶段显式给counter2赋值为0

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