El Psy Congroo

JVM Internals学习笔记

原文: JVM Interals

Thread

Hotspot JVM 将Java线程一对一映射到操作系统原生线程上

  • Java线程状态初始化,如thread-local sotrage, allocation buffers, synchronization objects, stacks, program counter
  • 原生线程创建,调用run()方法
  • run()方法返回,处理未捕获异常
  • 线程终止,释放Java线程及原生线程资源,并判断JVM是否需要退出(如此线程是最后一个非守护线程)

JVM System Thread

由main方法创建的系统后台线程

  • VM thread
    执行需要JVM在safe-point时才能执行的操作,如GC, thread stack dump, thread suspension, biased locking revocation
  • Periodic task thread
  • GC threads
  • Compiler threads
    运行时编译字节码到native code
  • Signal dispatch thread
    接收操作系统signal并处理

Per thread

与线程一一对应的资源

Program Counter (PC)

下一条指令或操作数的地址,如果是native方法,PC值为undefined

VM Stack

虚拟机栈,存储栈帧(Frame),每次方法调用时入栈,方法返回或异常时出栈。当栈容量超过最大限制时,会抛出StackOverflowError异常,当新线程没有足够内存创建栈时,会抛出OutOfMemoryError异常

Native Method Stack

本地方法栈,支持native方法执行,类似虚拟机栈,有的实现将两者合并

Stack Frame

栈帧(Stack Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接 (Dynamic Linking)、方法返回值和异常分派(Dispatch Exception)。

栈帧随着方法调用而创建,随着方法结束(返回或抛出异常)而销毁。栈帧包含局部变量表(Local Variables Array)、操作数栈(Operand Stack)和指向当前方法所属的类的运行时常量池的引用。

  • Local Variable Array
    方法调用时所有的局部变量,包括参数及对this的引用,局部变量可以是基本类型,引用及返回地址,其中long和double占用两个局部变量。当方法调用时,参数会传递至从0开始的连续位置中,如果是实例方法,0存储方法所在对象的应用(即this)。局部变量表是可重用的,超出作用域的变量的位置可以被新变量使用。局部变量表长度在编译器决定。
  • Oprand Stack
    用于字节码操作,性质类似CPU寄存器,例如调用iadd字节码指令会将栈顶两个int值出栈相加后再入栈
  • Dynamic Linking (Reference to runtime constant pool)
    C/C++代码在编译链接阶段会将符号引用(symbolic reference)转化为实际内存地址,对于Java来说,链接是在运行时动态进行的。Java class编译时,所有的变量及方法引用都以符号引用形式保存在class的常量池中,由JVM选择解析时机,eager/static 或者lazy/later resolution。解析只发生一次,同时会触发必要的类加载,解析后的direct reference以相对于方法或变量运行时位置存储结构的偏移量存储

Shared Between Threads

线程间共享的资源

Heap

存储对象实例及数组,栈帧被设计为创建后大小不可变,因此可变对象都存放在堆中,栈帧中仅保存基础类型及引用

Interned Strings

String常量池,在类加载时,所有的String常量都会被保存到常量池中。运行时也可以显式调用String.intern(),如果常量池中已经存在该string,则直接返回引用,否则将该string加入常量池中并返回引用。
Java 6中String常量池放在Permgen中,容易引发OOM问题,Java 7开始已经将String常量池放到堆中,默认大小为60013,如果重度使用intern的话需要配置的更大避免hash冲突导致性能下降,详细信息参考String.intern in Java 6, 7 and 8

Non-Heap Memory

Method Area

方法区,保存每个类的结构信息,如运行时常量池,字段和方法数据,构造函数和普通方法的字节码等等,包括:

  • Classloader Reference
    • 相对应的classloader也会维护一份所有已加载类定义的引用
  • Run Time Constant Pool
  • Field data
  • Method data
  • Method code

Run Time Constant Pool
JVM为每个类维护一个运行时常量池,保存运行时所需的数据,数据类型如下:

  • numeric literals
  • string literals
  • class references
  • field references
  • method references

字节码操作时会直接引用常量池中的数据,例如:

1
2
3
4
5
6
7
# 源码
# Object foo = new Object();
# 字节码
0: new #2 // Class java/lang/Object
1: dup
2: invokespecial #3 // Method java/lang/Object."<init>"()V

Code Cache

保存JIT编译后的本地代码,以前遇到过JDK的bug,Code Cache占用过高触发回收后JIT失效,因此可以考虑相对设置的大一些避免触发回收-XX:ReservedCodeCacheSize=256m

Class File Structure

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;
cp_info contant_pool[constant_pool_count – 1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// TestClass.java
package com.test;
public class TestClass {
public static final String HELLO = "Hello";
private int m;
public int inc(int x) {
return m + x;
}
public int inc_r(int x) {
if (x <= 0) return m;
return 1 + inc_r(x - 1);
}
public static int inc_one(int x) {
return ++x;
}
}

编译结果

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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
$ javap -v -p TestClass
public class com.test.TestClass
minor version: 0
## 版本号,1.1为45,后续每个版本加1,52为1.8
major version: 52
## JDK 1.0.2之后编译出的Class文件都带有ACC_SUPER标志,用于兼容invokespecial的语义变更
flags: ACC_PUBLIC, ACC_SUPER
## 常量池,包含Class中所有字符串常量,类或接口名,字段名,方法名和其他常量
Constant pool:
## 方法和字段表示方式类似,由class_index和name_and_type_index组成
#1 = Methodref #5.#23 // java/lang/Object."<init>":()V
#2 = Fieldref #4.#24 // com/test/TestClass.m:I
#3 = Methodref #4.#25 // com/test/TestClass.inc_r:(I)I
## 类和接口,值为name_index
#4 = Class #26 // com/test/TestClass
#5 = Class #27 // java/lang/Object
#6 = Utf8 HELLO
#7 = Utf8 Ljava/lang/String;
#8 = Utf8 ConstantValue
#9 = String #28 // Hello
#10 = Utf8 m
#11 = Utf8 I
#12 = Utf8 <init>
#13 = Utf8 ()V
#14 = Utf8 Code
#15 = Utf8 LineNumberTable
#16 = Utf8 inc
#17 = Utf8 (I)I
#18 = Utf8 inc_r
#19 = Utf8 StackMapTable
#20 = Utf8 inc_one
#21 = Utf8 SourceFile
#22 = Utf8 TestClass.java
## 用于表示字段和方法,由name_index和descriptor_index组成
#23 = NameAndType #12:#13 // "<init>":()V
#24 = NameAndType #10:#11 // m:I
#25 = NameAndType #18:#17 // inc_r:(I)I
#26 = Utf8 com/test/TestClass
#27 = Utf8 java/lang/Object
#28 = Utf8 Hello
{
## field_info表,字段信息
## 字段名,name_index,引用自常量池
public static final java.lang.String HELLO;
## 字段描述,descriptor_index,引用自常量池
descriptor: Ljavabiao/lang/String;
## 定义字段被访问权限和基础属性的掩码标志
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
## 属性,attribute_info表,ConstantValue属性表示一个常量字段的值
## 只有ACC_STATIC, ACC_FINAL的基本类型和String才会使用ConstantValue方式赋值,其他字段都在执行构造函数时赋值
ConstantValue: String Hello
private int m;
## int类型
descriptor: I
flags: ACC_PRIVATE
## method_info表,方法信息,包括字节码
## 初始化函数
public com.test.TestClass();
## 返回值为void,无参数
descriptor: ()V
flags: ACC_PUBLIC
## 属性,attribute_info表,Code属性保存字节码
Code:
## 栈最大深度,本地变量数量,参数数量(实例方法第一个参数默认为this,因此size为1)
stack=1, locals=1, args_size=1
## 推送this到栈顶(将第一个引用类型局部变量推送至栈顶)
0: aload_0
## 调用初始化方法(调用特殊的实例方法,例如私有方法,父类方法和初始化方法)
1: invokespecial #1 // Method java/lang/Object."<init>":()V
## 返回void
4: return
## 对应源代码行号,用于调试
LineNumberTable:
line 3: 0
public int inc(int);
## 参数为int,返回值为int
descriptor: (I)I
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
## this入栈
0: aload_0
## m入栈(获取指定类的实例域,并将其值压入栈顶)
1: getfield #2 // Field m:I
## int参数入栈
4: iload_1
## 将栈顶2个int相加并将结果入栈
5: iadd
## 返回栈顶int
6: ireturn
LineNumberTable:
line 15: 0
public int inc_r(int);
descriptor: (I)I
flags: ACC_PUBLIC
Code:
## stack记录单次调用的最大栈深度
stack=4, locals=2, args_size=2
0: iload_1
1: ifgt 9
4: aload_0
5: getfield #2 // Field m:I
8: ireturn
9: iconst_1
10: aload_0
11: iload_1
12: iconst_1
13: isub
## 调用对象的实例方法
14: invokevirtual #3 // Method inc_r:(I)I
17: iadd
18: ireturn
LineNumberTable:
line 18: 0
line 19: 9
## 帮助在编译期进行字节码验证,代替加载阶段性能开销较高的基于数据流的类型推导验证器
## 按照控制流(if/goto)将代码分为多个frame
## StackMapTable记录每个frame的字节码偏移量及局部变量和操作数栈的变化
## 每个方法的第一个frame都是隐式生成的,这边显示的从第二个开始
## 参考:http://hllvm.group.iteye.com/group/topic/26545
StackMapTable: number_of_entries = 1
## 偏移量为9(if结束后),局部变量和操作数栈没有变化(same)
frame_type = 9 /* same */
public static int inc_one(int);
descriptor: (I)I
flags: ACC_PUBLIC, ACC_STATIC
Code:
## static方法,因此没有this参数
stack=1, locals=1, args_size=1
0: iinc 0, 1
3: iload_0
4: ireturn
LineNumberTable:
line 22: 0
}
SourceFile: "TestClass.java"

Classloader