深入理解JVM

类的加载,连接与初始化

  • 加载:查找并加载类的二进制

  • 连接

    • 验证:确保被加载的类的正确性
    • 准备:为类的静态变量分配内存,并将其初始化为默认值
    • 解析:把类中的符号引用转为直接引用
  • 初始化:为类的静态变量赋于正确的初始值

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

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

  • 主动使用

    • 创建类的实例
    • 访问某个类或接口的静态变量,或者对该静态变量赋值‘
    • 调用类的静态方法
    • 反射(如Class.forName(“com.test.Test”))
    • 初始化一个类的子类
    • Java虚拟机启动时被标明为启动类的类(如执行 java Test)
    • JDK1.7开始提供的动态语言支持:java.lang.invoke.MethodHandle实例的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic句柄对应的类没有初始化,则初始化

    除了上述7种情形,其他使用Java类的方式都被看作被动使用,不会导致类的初始化。

类的加载

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时的数据区内,然后在内存中创建一个java.lang.Class对象(规范并未说明Class对象位于哪里,HotSpot虚拟机将其放在了方法区中)用来防撞类在方法区的数据结构。

类加载器

  • Java虚拟机自带的类加载器
    • 根类加载器(Bootstrap)从系统属性sun.boot.class.path指定的目录加载类(jar, class),它并没有继承ClassLoader
    • 扩展类加载器(Extension)它的父加载器为根类加载器,它从系统属性java.ext.dirs指定的目录加载类库,或者从JDK的安装目录的jre/lib/ext目录加载类库(即jar)
    • 系统(应用)类加载器(System/App)它的父加载器为扩展类加载器,它从环境变量classpath或系统属性java.class.path指定的目录加载类(jar, class),它是用户自定义加载器的默认父加载器,系统类加载器是纯Java类,是java.lang.ClassLoader类的子类。
  • 用户自定义的类加载器
    • java.lang.ClassLoader的子类
    • 用户可以定制类的加载方式

类加载器会把类加载到Java虚拟机中,从JDK 1.2版本开始,类的加载过程采用双亲委托机制,这种机制能更好的保证Java平台的安全,在此委托机制中,除了Java虚拟机自带的根类加载器外,其余的加载器都有且只有一个父加载器,当Java程序请求加载器loader1加载Sample类时,loader1首先委托自己的父加载器去加载Sample类,若如加载器能加载,则由父加载器完成加载任务,否则才由加载器loader1本身加载Sample类。

类的初始化

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

类的初始化时机

  • 当Java虚拟机初始化一个实例时,要求它所有的父类都已经初始化,但是这条规则并不适用于接口
  • 在初始化一个类时,并不会先初始化它所实现的接口
  • 在初始化一个接口时,并不会先初始化它的父接口

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

调用ClassLoader类的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化。

获取ClassLoader的途径

获取当前类的ClassLoader

clazz.getClassLoader()

获取当前线程上下文的ClassLoader

Thread.currentThread().getContextClassLoader()

获取系统的ClassLoader

ClassLoader.getSystemClassLoader()

获取调用者的ClassLoader

sun.reflect.Relection.getCallerClass().getClassLoader()

类加载器的双亲委托机制

1
2
3
4
ClassLoader loader1 = new MyClassLoader();

// 参数loader1将作为loader2的父加载器
ClassLoader loader2 = new MyClassLoader(loader1);

双亲委托机制的优点是能够提高软件系统的安全性。因此在此机制下,用户自定义的类加载器不可能加载应该由父加载器加载的可靠类,从而防止不可靠甚至恶意的代码替代父加载器加载的可靠代码,例如:java.lang.Object 类总是由根类加载器加载,其他任何用户自定义的类加载器都不可能加载包含有恶意代码的java.lang.Object类

命名空间

  • 每个类加载器都有自己的命名空间,命名空间由该加载器及所有父加载器所加载的类组成
  • 在同一个命名空间中,不会出现类的完成名字(包括类的包名)相同的两个类
  • 在不同的命名空间中,有可能会出现类的全限定名(包括类的包名)相同的两个类

java.lang.ClassCastException: com.test.Test cannot be cast to com.test.Test

类的卸载

由用户自定义的类加载器所加载的类是可以被卸载的

SPI (Service Interface Provider)

例子:JDBC

字节码

1
2
javap -c com.test.Test1
javap -verbose com.test.Test1
  • 使用javap -verbose命令分析一个字节码时,将会分析字节码文件的魔数,版本号,常量池,类信息,类的构造方法,类中的方法信息,类变量与成员变量等信息。

  • 魔数:所有的.class字节码文件的前4个字节都是魔数,魔数值为固定值:0xCAFEBABE

  • 魔数之后的4个字节时版本信息,前两个字节时minor version(次版本号),后两个字节时major version(主版本号)这里的版本号为00 00 00 34 换算成十进制,表示次版本号为0,主版本号为52. 所以该文件的版本号为1.8.0,可以通过java -version来验证。

  • 常量池(constant pool):紧接着主版本号之后就是常量池的入口,一个Java类中定义的很多信息都是常量池来维护和描述的,可以将常量池看作是Class文件的资源仓库,比如说Java类中定义的方法与变量信息,都是存在在常量池中,常量池中主要存储两类常量:字面量与符号引用,字面量如文本字符串,Java中声明为final的常量等,而符号引用和接口的全限定名,字段的名称和描述,方法的名称和描述符等。
  • 常量池的总体结构:Java类所对应的常量池主要由常量数量和常量池数组这两个部分组成。常量池数量紧跟在主版本号之后,占2个字节;常量池数组紧跟在常量池数量之后。常量池数组与一般的数组不同的是,常量池数组中不同的元素的类型,结构都是不同的。长度当然也就不同;但是,每一种元素的第一个数据都是一个u1类型,该字节是个标志位,占1个字节,JVM在解析常量池时,会根据这个u1类型来获取元素的具体类型。值得注意的是,常量池数组中元素的个数 = 常量池数 - 1(其中0暂时不实用)。目的是满足某些常量池索引值的数据在特定情况下需要表达「不引用任何一个常量池」;根本原因在于,索引值为0也是一个常量(保留常量),只不过它不位于常量表中,这个常量对应null值,常量池的索引从1而非0开始。
  • 在JVM规范中,每个变量/字段都有描述信息,描述信息主要的作用是描述字段的数据类型,方法的参数列表(包括数量,类型与顺序),根据描述符规则,基本数据类型和代表无返回值的void类型都用一个大写字符来表示,对象类型则使用字符L加对象全限定名称来表示,为了压缩字节码文件的体积,对于基本数据类型,JVM都只使用一个大写字母表示,如下所示:B - byte, C - char, D - double, F - float, I = int, J = long, S - short, Z - boolean, V - void, L - 对象类型,如Ljava/lang/String;
  • 对于数组类型来说,每一个维度使用一个前置[来表示,如int[]被记录为[I, String[][]被记录为[[java/lang/String;
  • 用描述符来描述方法的时候,按照先参数列表,后返回值的顺序来描述,参数列表按照参数的严格顺序放在一组()之内,如方法:String getPersonByIdAndName(int id, String name)的描述符为:([I,Ljava/lang/String;)Ljava/lang/String;

完整的Java字节码结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ClassFile {
u4 magic; // 魔数
u2 minor_version; // 次版本号
u2 major_version; // 主版本号
u2 constant_pool_count; // 常量池个数
constant_pool constant_pool_count - 1; // 常量池表
u2 access_flag; // 类的访问控制权限
u2 this_class; // 类名
u2 super_class; // 父类名
u2 interfaces_count; // 接口数量
u2 interfaces; // 接口个数
u2 fields_count; // 字段个数
field_info fields; // 字段表
u2 methods_count; // 方法个数
method_info methods; // 方法表
u2 attributes_count; // 附加属性的个数
attribute_info attributes; // 附加属性表
}

u1, u2, u4, u8 表示1个字节,2个字节,4个字节,8个字节

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
javap -verbose org.xxz.Test1 
Classfile /Users/tt/IdeaProjects/demo/target/classes/org/xxz/Test1.class
Last modified 2019-6-7; size 455 bytes
MD5 checksum cb0d025ecc8eb40b573008dd290ac668
Compiled from "Test1.java"
public class org.xxz.Test1
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#21 // org/xxz/Test1.a:I
#3 = Class #22 // org/xxz/Test1
#4 = Class #23 // java/lang/Object
#5 = Utf8 a
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lorg/xxz/Test1;
#14 = Utf8 getA
#15 = Utf8 ()I
#16 = Utf8 setA
#17 = Utf8 (I)V
#18 = Utf8 SourceFile
#19 = Utf8 Test1.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = NameAndType #5:#6 // a:I
#22 = Utf8 org/xxz/Test1
#23 = Utf8 java/lang/Object
{
public org.xxz.Test1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_1
6: putfield #2 // Field a:I
9: return
LineNumberTable:
line 6: 0
line 8: 4
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lorg/xxz/Test1;

public int getA();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field a:I
4: ireturn
LineNumberTable:
line 11: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lorg/xxz/Test1;

public void setA(int);
descriptor: (I)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: iload_1
2: putfield #2 // Field a:I
5: return
LineNumberTable:
line 15: 0
line 16: 5
LocalVariableTable:
Start Length Slot Name Signature
0 6 0 this Lorg/xxz/Test1;
0 6 1 a I
}
SourceFile: "Test1.java"

常量池表,访问控制权限表

jclasslib

基于栈的指令集与基于寄存器的指令集之间的关系:

  1. JVM执行指令时采取的方式是基于栈的指令集
  2. 基于栈的指令集主要的操作由入栈和出栈
  3. 基于栈的指令集的优势在于它可以在不同的平台之间移植,而基于寄存器的指令集是与硬件紧密关联的,无法做到可移植
  4. 基于栈的指令的缺点在于完成相同的操作,指令数量通常要比基于寄存器的指令数量要多,基于栈的指令集是在内存中完成操作的,而基于寄存器的指令集是直接由CPU来执行的,它是在高速缓冲区中执行的,速度要快很多,虽然虚拟机可以采用一些优化手段,但总体来说,基于栈的指令集的执行速度要慢一些。

JVM 模块

虚拟机栈:Stack Frame 栈帧

程序计数器:Program Counter

本地方法栈:主要用于处理本地方法

堆(Heap):JVM管理的最大一块内存空间

方法区(Method Area):存储元数据。永久代(Permanent Generation),从JDK1.8开始,已经彻底废弃了永久代,使用元空间(Meta Space)

运行时常量池:方法区的一部分内容

直接内存:Direct Memory