Java 如今已经成为了世界上最流行的编程语言之一,而 Java 的虚拟机 JVM 也是 Java 语言的核心。JVM 是 Java 的运行环境,它负责将 Java 代码编译成机器码并运行。本文将初步介绍 JVM 的基本概念。
从源代码到机器码
编程语言根据如何执行,可以分为两类:
-
编译型语言
源代码在运行前先编译成机器码,然后再运行机器码。例如 C、C++。编译型语言的代码在编写时,会使用到和硬件、操作系统相关的特性,编译后的机器码通常只能在特定的硬件和操作系统上运行。
尽管交叉编译可以让一个程序编译为多个平台的可执行文件,但编译型语言编写的程序也往往是不同的。例如对于 C 语言,我们在 Windows 上会使用
windows.h
来调用 Windows API,而在 Linux 上则会使用unistd.h
来调用 Linux API。 -
解释型语言
源代码在运行时逐行解释执行。例如 Python、JavaScript。解释型语言的代码在编写时,通常不会使用到和硬件、操作系统相关的特性,通常所有平台使用的代码是一模一样的。
Java 是一种特殊的编译型语言,它的编译运行分为两步:
- 由源代码编译为字节码
- JVM 翻译字节码并执行,有时也会将部分字节码编译为机器码
前端编译器
源代码到字节码这步由前端编译器完成。我们通常使用的 javac
命令就是一个前端编译器,它将 Java 源代码编译为字节码文件(.class
文件)。
我们新建一个 Hello.java
文件:
public class Hello {
public static void main(String[] args) {
System.out.println("Hello, world!");
}
}
然后使用 javac
编译:
javac Hello.java
编译后会生成一个 Hello.class
文件,这个文件就是字节码文件。我们使用任意二进制编辑器打开这个文件,可以看到:
CA FE BA BE 00 00 00 41 00 1D 0A 00 02 00 03 07
00 04 0C 00 05 00 06 01 00 10 6A 61 76 61 2F 6C
61 6E 67 2F 4F 62 6A 65 63 74 01 00 06 3C 69 6E
69 74 3E 01 00 03 28 29 56 09 00 08 00 09 07 00
0A 0C 00 0B 00 0C 01 00 10 6A 61 76 61 2F 6C 61
6E 67 2F 53 79 73 74 65 6D 01 00 03 6F 75 74 01
00 15 4C 6A 61 76 61 2F 69 6F 2F 50 72 69 6E 74
53 74 72 65 61 6D 3B 08 00 0E 01 00 0D 48 65 6C
6C 6F 2C 20 77 6F 72 6C 64 21 0A 00 10 00 11 07
00 12 0C 00 13 00 14 01 00 13 6A 61 76 61 2F 69
6F 2F 50 72 69 6E 74 53 74 72 65 61 6D 01 00 07
70 72 69 6E 74 6C 6E 01 00 15 28 4C 6A 61 76 61
2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 29 56 07
00 16 01 00 05 48 65 6C 6C 6F 01 00 04 43 6F 64
65 01 00 0F 4C 69 6E 65 4E 75 6D 62 65 72 54 61
62 6C 65 01 00 04 6D 61 69 6E 01 00 16 28 5B 4C
6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67
3B 29 56 01 00 0A 53 6F 75 72 63 65 46 69 6C 65
01 00 0A 48 65 6C 6C 6F 2E 6A 61 76 61 00 21 00
15 00 02 00 00 00 00 00 02 00 01 00 05 00 06 00
01 00 17 00 00 00 1D 00 01 00 01 00 00 00 05 2A
B7 00 01 B1 00 00 00 01 00 18 00 00 00 06 00 01
00 00 00 01 00 09 00 19 00 1A 00 01 00 17 00 00
00 25 00 02 00 01 00 00 00 09 B2 00 07 12 0D B6
00 0F B1 00 00 00 01 00 18 00 00 00 0A 00 02 00
00 00 03 00 08 00 04 00 01 00 1B 00 00 00 02 00
1C
这个字节码文件的格式遵循了 Java Virtual Machine Specification 的规范。接下来我们按顺序解读这个字节码文件:
点击展开具体的分析过程
-
CA FE BA BE
前 4 个字节为 Magic Number,标识这是一个 Java 字节码文件
-
00 00 00 41
接下来 4 个字节为版本号,其中
00 00
为次版本号,00 41
为主版本号。主版本号的数字是顺序排列的,例如 JDK 1.8 是0x34
、JDK 17 是0x3D
、JDK 21 是0x41
。由此可知,这个字节码文件是 Java 21。 -
00 1D
接下来 2 个字节为常量池大小,这里有 \(29-1=28\) 个常量。
规范规定,常量共有 17 种类型,每种都有一个 1 字节的 tag 标识。具体对应关系可以参考 Java Virtual Machine Specification 的表 4.4-A。每种常量的结构也不同,我们接下来会通过例子解读。
-
0A 00 02 00 03
(第 1 个常量)首先,
0A
是这个常量的 tag,表示这是一个CONSTANT_Methodref_info
类型的常量。查阅规范的第 4.4.2 节,我们可以知道这个常量的结构如下:CONSTANT_Methodref_info { u1 tag; u2 class_index; u2 name_and_type_index; }
因此,
00 02
是class_index
,即该常量的类型,其指向常量池中的第 2 个常量;00 03
是name_and_type_index
,即该常量的名字和类型,其指向常量池中的第 3 个常量。通过后文我们可以知道,这个常量表示一个方法引用,其类名是
java/lang/Object
,方法名是<init>
,方法类型是()V
。即调用了java/lang/Object
类的无参构造函数。我们继续查看常量池中的第 2 个常量。
-
07 00 04
(第 2 个常量)这个常量的 tag 是
CONSTANT_Class_info
,表示这是一个类名。查阅规范的第 4.4.1 节,我们可以知道这个常量的结构如下:CONSTANT_Class_info { u1 tag; u2 name_index; }
因此,
00 04
是name_index
,即类的名字,其指向常量池中的第 4 个常量。根据后文我们可以知道,这个类名是
java/lang/Object
。 -
0C 00 05 00 06
(第 3 个常量)这个常量的 tag 是
CONSTANT_NameAndType_info
,表示这是一个名字和类型。查阅规范的第 4.4.6 节,我们可以知道这个常量的结构如下:CONSTANT_NameAndType_info { u1 tag; u2 name_index; u2 descriptor_index; }
因此,
00 05
是name_index
,即名字,其指向常量池中的第 5 个常量;00 06
是descriptor_index
,即类型,其指向常量池中的第 6 个常量。根据后文我们可以知道,这个名字是
<init>
,这个类型是()V
。即调用了一个无参构造函数。怎么一环套一环,跟递归似的!
-
01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74
(第 4 个常量)这个常量的 tag 是
CONSTANT_Utf8_info
,表示这是一个 UTF-8 编码的字符串。查阅规范的第 4.4.7 节,我们可以知道这个常量的结构如下:CONSTANT_Utf8_info { u1 tag; u2 length; u1 bytes[length]; }
因此,
00 10
是length
,即字符串的长度,其长度为 16;因此,我们往后数 16 个字节,内容为6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74
。这一段就是bytes
,即字符串的内容,其内容为java/lang/Object
。 -
01 00 06 3C 69 6E 69 74 3E
(第 5 个常量)这个常量的 tag 是
CONSTANT_Utf8_info
,内容为<init>
。<init>
是 Java 中的构造函数的名字。 -
01 00 03 28 29 56
(第 6 个常量)这个常量的 tag 是
CONSTANT_Utf8_info
,内容为28 29 56
()V
。()
表示这个方法没有参数,V
表示这个方法没有返回值。 -
09 00 08 00 09
(第 7 个常量)这个常量的 tag 是
CONSTANT_Fieldref_info
,表示这是一个字段引用。查阅规范的第 4.4.2 节,我们可以知道这个常量的结构如下:CONSTANT_Fieldref_info { u1 tag; u2 class_index; u2 name_and_type_index; }
因此,
00 08
是class_index
,即该常量的类型,其指向常量池中的第 8 个常量;00 09
是name_and_type_index
,即该常量的名字和类型,其指向常量池中的第 9 个常量。通过后文我们可以知道,这个常量表示一个字段引用,其类名是
java/lang/System
,方法名是out
,方法返回值类型是Ljava/io/PrintStream;
。因此,这个常量表示了一个java.lang.System.out
的引用。 -
07 00 0A
(第 8 个常量)这个常量的 tag 是
CONSTANT_Class_info
,其指向常量池中的第 10 个常量。根据后文我们可以知道,这个类名是
java/lang/System
。 -
0C 00 0B 00 0C
(第 9 个常量)这个常量的 tag 是
CONSTANT_NameAndType_info
,其指向常量池中的第 12 个常量。根据后文我们可以知道,这个名字是
out
,这个类型是Ljava/io/PrintStream;
。即方法名为out
,方法返回类型类型为PrintStream
。 -
01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 53 79 73 74 65 6D
(第 10 个常量)这个常量的 tag 是
CONSTANT_Utf8_info
,其内容为java/lang/System
。 -
01 00 03 6F 75 74
(第 11 个常量)这个常量的 tag 是
CONSTANT_Utf8_info
,内容为out
。 -
01 00 15 4C 6A 61 76 61 2F 69 6F 2F 50 72 69 6E 74 53 74 72 65 61 6D 3B
(第 12 个常量)这个常量的 tag 是
CONSTANT_Utf8_info
,其内容为Ljava/io/PrintStream;
。 -
08 00 0E
(第 13 个常量)这个常量的 tag 是
CONSTANT_String_info
,表示这是一个字符串。查阅规范的第 4.4.3 节,我们可以知道这个常量的结构如下:CONSTANT_String_info { u1 tag; u2 string_index; }
因此,
00 0E
是string_index
,即字符串的索引,其指向常量池中的第 14 个常量。根据后文我们可以知道,这个常量表示了一个字符串,其内容为
Hello, world!
。 -
01 00 0D 48 65 6C 6C 6F 2C 20 77 6F 72 6C 64 21
(第 14 个常量)这个常量的 tag 是
CONSTANT_Utf8_info
,内容为Hello, world!
。 -
0A 00 10 00 11
(第 15 个常量)这个常量的 tag 是
CONSTANT_Methodref_info
。class_index
指向常量池中的第 16 个常量;name_and_type_index
指向常量池中的第 17 个常量。通过后文我们可以知道,这个常量表示一个方法引用,其类名是
java/io/PrintStream
,方法名是println
,方法类型是(Ljava/lang/String;)V
。即调用了java.io.PrintStream.println
方法。 -
07 00 12
(第 16 个常量)这个常量的 tag 是
CONSTANT_Class_info
,其指向常量池中的第 18 个常量。根据后文我们可以知道,这个类名是
java/io/PrintStream
。 -
0C 00 13 00 14
(第 17 个常量)这个常量的 tag 是
CONSTANT_NameAndType_info
。name_index
指向常量池中的第 19 个常量;descriptor_index
指向常量池中的第 20 个常量。根据后文我们可以知道,这个名字是
println
,这个类型是(Ljava/lang/String;)V
。即方法名为println
,方法返回类型为void
。 -
01 00 13 6A 61 76 61 2F 69 6F 2F 50 72 69 6E 74 53 74 72 65 61 6D
(第 18 个常量)这个常量的 tag 是
CONSTANT_Utf8_info
,其内容为java/io/PrintStream
。 -
01 00 07 70 72 69 6E 74 6C 6E
(第 19 个常量)这个常量的 tag 是
CONSTANT_Utf8_info
,内容为println
。 -
01 00 15 28 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 29 56
(第 20 个常量)这个常量的 tag 是
CONSTANT_Utf8_info
,其内容为(Ljava/lang/String;)V
。 -
07 00 16
(第 21 个常量)这个常量的 tag 是
CONSTANT_Class_info
,其指向常量池中的第 22 个常量。根据后文我们可以知道,这个类名是
Hello
。 -
01 00 05 48 65 6C 6C 6F
(第 22 个常量)这个常量的 tag 是
CONSTANT_Utf8_info
,内容为Hello
。 -
01 00 04 43 6F 64 65
(第 23 个常量)这个常量的 tag 是
CONSTANT_Utf8_info
,内容为Code
。 -
01 00 0F 4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65
(第 24 个常量)这个常量的 tag 是
CONSTANT_Utf8_info
,内容为4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65
,即LineNumberTable
。 -
01 00 04 6D 61 69 6E
(第 25 个常量)这个常量的 tag 是
CONSTANT_Utf8_info
,内容为main
。 -
01 00 16 28 5B 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 29 56
(第 26 个常量)这个常量的 tag 是
CONSTANT_Utf8_info
,内容为([Ljava/lang/String;)V
。 -
01 00 0A 53 6F 75 72 63 65 46 69 6C 65
(第 27 个常量)这个常量的 tag 是
CONSTANT_Utf8_info
,内容为53 6F 75 72 63 65 46 69 6C 65
,即SourceFile
。 -
01 00 0A 48 65 6C 6C 6F 2E 6A 61 76 61
(第 28 个常量)这个常量的 tag 是
CONSTANT_Utf8_info
,内容为Hello.java
。
现在我们回过去看,整个常量池的结构就清晰了:
#1 = java/lang/Object.<init>:()V #7 = java/lang/System.out:Ljava/io/PrintStream; #13 = Hello, world! #15 = java/io/PrintStream.println:(Ljava/lang/String;)V #21 = Hello #23 = Code #24 = LineNumberTable #25 = main #26 = ([Ljava/lang/String;)V #27 = SourceFile #28 = Hello.java
我们可以使用
javap
命令来查看这个字节码文件的内容:javap -v Hello.class
得到的结果是一样的。
-
-
00 21
接下来 2 个字节为访问标志,它的取值在规范的表 4.1-B 中定义。这里的
00 21
由0x0001
和0x0020
相加得到,分别表示ACC_PUBLIC
和ACC_SUPER
。JDK 1.2 之后的版本中
ACC_SUPER
一定会被设置,因此我们认为这个类是个基本的 public 类。 -
00 15
接下来 2 个字节为类名索引,其指向常量池中的第 21 个常量。
根据常量池我们可以知道,这个类名是
Hello
。 -
00 02
接下来 2 个字节为父类名索引,其指向常量池中的第 2 个常量。
根据常量池我们可以知道,这个父类名是
java/lang/Object
。说明这个类没有手动继承任何类,因此默认继承了java.lang.Object
。 -
00 00
接下来 2 个字节为接口数量,这里为 0。因此,接下来也不会由接口表。
-
00 00
接下来 2 个字节为字段表数量,这里为 0。因此,接下来也不会有字段表。
-
00 02
接下来 2 个字节为方法表数量,这里为 2。说明这个类有两个方法。
-
00 01 00 05 00 06 00 01 00 17 00 00 00 1D 00 01 00 01 00 00 00 05 2A B7 00 01 B1 00 00 00 01 00 18 00 00 00 06 00 01 00 00 00 01
-
前两个字节
00 01
为访问标志,查表可知为ACC_PUBLIC
; -
接下来 2 个字节
00 05
为方法名索引,其指向常量池中的第 5 个常量,即<init>
; -
接下来 2 个字节
00 06
为描述符索引,其指向常量池中的第 6 个常量,即()V
; -
接下来 2 个字节
00 01
为属性表数量,这里为 1,说明这个方法有一个属性;-
该属性内容为
00 17 00 00 00 1D 00 01 00 01 00 00 00 05 2A B7 00 01 B1 00 00 00 01
,其中:-
前 2 个字节
00 17
为属性名索引,其指向常量池中的第 23 个常量,即Code
; -
接下来 4 个字节
00 00 00 1D
为属性长度,这里为 29; -
接下来 2 个字节
00 01
为最大栈深度,这里为 1; -
接下来 2 个字节
00 01
为局部变量表大小,这里为 1; -
接下来 4 个字节
00 00 00 05
为代码长度,这里为 5;-
接下来 5 个字节
2A B7 00 01 B1
为代码,其内容为:-
2A
表示aload_0
,即将第 0 个局部变量(即this
)压入栈顶; -
B7 00 01
表示invokespecial #1
,即调用常量池中的第 1 个方法; -
B1
表示return
,即返回。
-
-
-
接下来 2 个字节
00 00
为异常表大小,这里为 0; -
接下来 2 个字节
00 01
为属性表大小,这里为 1;-
该属性内容为
00 18 00 00 00 06 00 01 00 00 00 01
,其中:-
前 2 个字节
00 18
为属性名索引,其指向常量池中的第 24 个常量,即LineNumberTable
; -
接下来 4 个字节
00 00 00 06
为属性长度,这里为 6; -
接下来 2 个字节
00 01
为行号表大小,这里为 1;-
该行号表内容为
00 00 00 01
,其中:-
前 2 个字节
00 00
为开始 PC,这里为 0; -
接下来 2 个字节
00 01
为行号,这里为 1。
-
-
-
-
-
-
这个方法的内容是调用
java/lang/Object
的无参构造函数。 -
-
00 09 00 19 00 1A 00 01 00 17 00 00 00 25 00 02 00 01 00 00 00 09 B2 00 07 12 0D B6 00 0F B1 00 00 00 01 00 18 00 00 00 0A 00 02 00 00 00 03 00 08 00 04
-
前两个字节
00 09
为访问标志,查表可知为public static
; -
接下来 2 个字节
00 19
为方法名索引,其指向常量池中的第 25 个常量,即main
; -
接下来 2 个字节
00 1A
为描述符索引,其指向常量池中的第 26 个常量,即([Ljava/lang/String;)V
; -
接下来 2 个字节
00 01
为属性表数量,这里为 1,说明这个方法有一个属性;-
该属性内容为
00 17 00 00 00 25 00 02 00 01 00 00 00 09 B2 00 07 12 0D B6 00 0F B1 00 00 00 01 00 18 00 00 00 0A 00 02 00 00 00 03 00 08 00 04
,其中:-
前 2 个字节
00 17
为属性名索引,其指向常量池中的第 23 个常量,即Code
; -
接下来 4 个字节
00 00 00 25
为属性长度,这里为 37; -
接下来 2 个字节
00 02
为最大栈深度,这里为 2; -
接下来 2 个字节
00 01
为局部变量表大小,这里为 1; -
接下来 4 个字节
00 00 00 09
为代码长度,这里为 9;-
接下来 9 个字节
B2 00 07 12 0D B6 00 0F B1
为代码,其内容为:-
B2 00 07
表示getstatic #7
,即获取java/lang/System.out:Ljava/io/PrintStream;
; -
12 0D
表示ldc #13
,即加载常量池中的第 13 个常量,即Hello, world!
; -
B6 00 0F
表示invokevirtual #15
,即调用java/io/PrintStream.println:(Ljava/lang/String;)V
; -
B1
表示return
,即返回。
-
-
-
接下来 2 个字节
00 00
为异常表大小,这里为 0; -
接下来 2 个字节
00 01
为属性表大小,这里为 1;-
该属性内容为
00 18 00 00 00 0A 00 02 00 00 00 03 00 08 00 04
,其中:-
前 2 个字节
00 18
为属性名索引,其指向常量池中的第 24 个常量,即LineNumberTable
; -
接下来 4 个字节
00 00 00 0A
为属性长度,这里为 10; -
接下来 2 个字节
00 02
为行号表大小,这里为 2;-
行号表第一行为
00 00 00 03
,即开始 PC 为 0,行号为 3; -
行号表第二行为
00 08 00 04
,即开始 PC 为 8,行号为 4。
-
-
-
-
-
这个方法的内容是调用
java.lang.System.out.println
方法。 -
-
-
00 01
接下来 2 个字节为类的属性表数量,这里为 1。说明这个类有一个属性。
-
00 1B 00 00 00 02 00 1C
-
前 2 个字节
00 1B
为属性名索引,其指向常量池中的第 27 个常量,即SourceFile
; -
接下来 4 个字节
00 00 00 02
为属性长度,这里为 2; -
接下来 2 个字节
00 1C
为源文件名索引,其指向常量池中的第 28 个常量,即Hello.java
。
这个属性表示这个类的源文件名是
Hello.java
。 -
-
终于分析完了!
我们可以对比一下 javap
的输出:
public class Hello
minor version: 0
major version: 65
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #21 // Hello
super_class: #2 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Fieldref #8.#9 // java/lang/System.out:Ljava/io/PrintStream;
#8 = Class #10 // java/lang/System
#9 = NameAndType #11:#12 // out:Ljava/io/PrintStream;
#10 = Utf8 java/lang/System
#11 = Utf8 out
#12 = Utf8 Ljava/io/PrintStream;
#13 = String #14 // Hello, world!
#14 = Utf8 Hello, world!
#15 = Methodref #16.#17 // java/io/PrintStream.println:(Ljava/lang/String;)V
#16 = Class #18 // java/io/PrintStream
#17 = NameAndType #19:#20 // println:(Ljava/lang/String;)V
#18 = Utf8 java/io/PrintStream
#19 = Utf8 println
#20 = Utf8 (Ljava/lang/String;)V
#21 = Class #22 // Hello
#22 = Utf8 Hello
#23 = Utf8 Code
#24 = Utf8 LineNumberTable
#25 = Utf8 main
#26 = Utf8 ([Ljava/lang/String;)V
#27 = Utf8 SourceFile
#28 = Utf8 Hello.java
{
public Hello();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #13 // String Hello, world!
5: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 3: 0
line 4: 8
}
SourceFile: "Hello.java"
可以说,跟我们解读出来的不能说是一模一样,只能说是毫无区别的。
JIT 编译器
JVM 拿到字节码文件后,会解释执行字节码。这种方法启动速度很快,但是执行速度很慢。因此,JVM 会采取混合的方式,在运行时将一部分字节码编译成本地机器码,以提高程序的执行效率。这个过程就是 JIT 编译器。
JIT 编译器(Just-In-Time Compiler)是 Java 虚拟机的一部分,它的作用是将 Java 字节码编译成本地机器码,以提高程序的执行效率。
HopSpot VM 内置了两个 JIT 编译器:C1 编译器和 C2 编译器。
-
C1 编译器:也叫做 Client 编译器,它是一个轻量级的编译器,主要用于对代码进行快速编译,以提高程序的启动速度。C1 编译器的编译速度快,但是生成的机器码质量一般。它适合对代码进行快速编译,以提高程序的启动速度,常常用于客户端应用程序。
-
C2 编译器:也叫做 Server 编译器,它是一个重量级的编译器,主要用于对代码进行优化编译,以提高程序的执行速度。C2 编译器的编译速度慢,但是生成的机器码质量高。它适合对代码进行优化编译,以提高程序的执行速度,常常用于服务器端应用程序。
我们在使用 java
命令时,对 JVM 的执行模式可以有如下几种选择:
- 默认模式为混合模式,即在程序运行过程中,JVM 会根据程序的运行情况,动态地选择 C1 编译器和 C2 编译器
-
-client
参数表示使用 C1 编译器 -
-server
参数表示使用 C2 编译器 -
-Xint
参数表示关闭 JIT 编译器,即完全采用解释执行的方式 -
-Xcomp
参数表示关闭解释执行,即完全采用编译执行的方式
使用 java -version
就可以看到当前 JVM 的执行模式。
AOT 编译器
AOT 编译器(Ahead-Of-Time Compiler)是一种在程序运行之前将 Java 字节码编译成本地机器码的编译器。AOT 编译器的优点是可以提高程序的启动速度,缺点是会增加程序的体积。
AOT 跟随 JEP 295 在 Java 9 中引入,但在 Java 17 时,已经被 JEP 410 删除。
JVM 内存模型
HotSpot VM 的内存根据 JDK 版本的不同,经历了一系列变化。
在 JDK 1.6 及之前,在 HotSpot VM 内部,所有线程共享堆和方法区。每个线程有自己独立的虚拟机栈、本地方法栈和程序计数器。此外,在 JVM 之外,还有一块叫做直接内存的区域。
在 JDK 1.7,方法中的静态变量和字符串常量池被移到了堆中,这样堆中就包含了对象实例、静态变量和字符串常量池。
在 JDK 1.8 及之后,HotSpot VM 的内存模型再次发生了变化,主要是将方法区扔到了 JVM 之外,变成了元空间(Metaspace)。
程序计数器
程序计数器(Program Counter Register)是一块较小的内存空间,它的作用是记录当前线程执行的字节码的位置。这是为了线程切换后能够恢复到正确的执行位置。
虚拟机栈
正如操作系统中的栈一样,虚拟机栈(Java Virtual Machine Stack)也是一种线程私有的内存空间,它的作用是存放方法的局部变量、操作数栈、动态链接、方法出口等信息。每当一个方法被调用时,虚拟机栈会分配一个栈帧(Stack Frame),用于存放这个方法的信息。当方法调用结束时,栈帧会被弹出。
虚拟机栈的每个栈帧包括以下几个部分:
- 局部变量表(Local Variables):用于存放局部变量,包括各种数据类型和对象引用
- 操作数栈(Operand Stack):用于存放方法执行过程中的操作数
- 动态链接(Dynamic Linking):指向运行时常量池中该方法的引用
- 方法出口(Return Address):指向方法返回的地址
本地方法栈
本地方法栈(Native Method Stack)与虚拟机栈类似,不同的是虚拟机栈是为 Java 方法(即字节码)服务的,而本地方法栈是为 Native 方法服务的。
堆
堆(Heap)是 Java 虚拟机中最大的一块内存空间,它的作用是存放对象实例。堆是线程共享的,所有线程都可以访问堆中的对象。
为了方便垃圾回收,堆一般会被划分为新生代、老年代和永久代(JDK 1.7 之前)或元空间(JDK 1.8 之后)。其结构如下图所示:
新生代分为 Eden 区(占比 80%)和两个 Survivor 区(占比 10%)。
- 新创建的对象会被分配到 Eden 区
- 对 Eden 区垃圾回收时,会将存活的对象复制到 From Survivor 区
- 当对 From Survivor 区垃圾回收时,会将存活的对象复制到 To Survivor 区,回收完成后交换 From Survivor 和 To Survivor 区,同时对象的年龄加 1
- 当对象的年龄达到一定值时(通常设置为不大于 15),会被晋升到老年代。
老年代主要存放存活时间较长的对象。同时,大对象(即占用空间较大的对象)也会直接分配到老年代。
字符串常量池存放了字符串常量,它的主要作用是避免重复创建相同的字符串对象。JDK 1.7 将字符串常量池移到了堆中,这是为了方便垃圾回收。
方法区 / 元空间
方法区(Method Area)是 Java 虚拟机中的一块内存空间,它的作用是存放类的元数据信息,如类的结构、字段、方法、接口等信息。方法区是线程共享的,所有线程都可以访问方法区。
在 JDK 1.8 时,方法区被移到了 JVM 之外,变成了元空间(Metaspace)。元空间的作用和方法区一样,只是位置不同。这么做的主要目的是充分利用系统的物理内存,减少方法区的内存溢出。
运行时常量池(Runtime Constant Pool)是方法区的一部分,它的作用是存放编译期生成的各种字面量和符号引用。运行时常量池是线程共享的,所有线程都可以访问运行时常量池。
直接内存
直接内存(Direct Memory)是 JVM 之外的一块内存空间,它的作用是存放 NIO 的缓冲区。直接内存是线程共享的,所有线程都可以访问直接内存。
类加载
类加载是 Java 虚拟机的一个重要组成部分,它的作用是将类的字节码文件加载到 Java 虚拟机中,以便程序能够运行。
这个过程分为七个阶段:加载、验证、准备、解析、初始化、使用和卸载。
加载
加载是类加载器的第一个阶段,主要作用是将类的字节码文件加载到 Java 虚拟机中,并将其转换成 Java 虚拟机可以执行的类。
这一步会使用类加载器完成以下几个工作:
- 通过一个类的全限定名(即包含包名的类名)来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的
java.lang.Class
对象,作为方法区这个类的各种数据的访问入口
类加载器可以分为四种:
-
启动类加载器
负责加载 Java 的核心类库,如
rt.jar
(包含了java.lang.*
、java.util.*
等),是虚拟机的一部分,是用原生代码实现的。 -
平台类加载器
负责加载平台类库,如
lib/ext
目录下的类库,是由sun.misc.Launcher$ExtClassLoader
实现的。在 Java 9 之前,其被称为扩展类加载器(Extension ClassLoader)。 -
应用程序类加载器
负责加载应用程序的类,是由
sun.misc.Launcher$AppClassLoader
实现的。 -
自定义类加载器
用户自定义的类加载器,继承自
java.lang.ClassLoader
类,可以用来加载用户自定义的类。我们可以通过继承ClassLoader
类,重写findClass
方法,来实现自定义的类加载器。
这几个类加载器存在着父子关系,启动类加载器没有父类加载器,而平台类加载器的父类加载器是启动类加载器,应用程序类加载器的父类加载器是平台类加载器。
当需要加载一个类的时候,JVM 会首先一路向上查找该类是否已经被加载,如果没有找到,就会调用类加载器的 loadClass
方法来加载这个类。加载时,则是从启动类加载器开始,依次向下尝试加载。
事实上,这并不是绝对的,用户自定义的类很可能打破这样的双亲委派模型。例如,Spring 的
SpringBootClassLoader
就是一个打破双亲委派模型的类加载器,它会为每个线程创建一个新的类加载器,这样就可以实现不同线程加载不同的类。
验证
验证是类加载器的第二个阶段,他会确保类的字节码文件是符合 Java 虚拟机规范的。它主要包括以下几个方面:
- 文件格式验证:文件格式验证
- 元数据验证:字节码语义验证
- 字节码验证:程序语义验证
- 符号引用验证:类自身的符号引用验证
准备
准备(Preparation)是类加载器的第三个阶段,主要会按照代码语句的顺序,做下面几件事情:
- 为类的静态变量(
static
)分配内存- 为变量赋予默认值,即零值(
0
、null
、false
、\u0000
) - 为常量(
final
)赋予用户指定的值
- 为变量赋予默认值,即零值(
- 执行类的静态代码块(
static {}
)
这些内容存在于元空间和堆中。
解析
解析(Resolution)是类加载器的第四个阶段,JVM 会将类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符的符号引用解析为直接引用。
初始化
初始化(Initialization)是类加载器的第五个阶段,JVM 会按照语句的顺序执行类的初始化方法:
-
new
、getstatic
、putstatic
或invokestatic
字节码指令会触发对应类的初始化。这四条字节码常常由以下代码产生:- 使用
new
关键字创建类的实例 - 读取一个类的静态字段(被
final
修饰的静态字段和在常量池中的静态字段除外),触发getstatic
指令 - 设置一个类的静态字段,触发
putstatic
指令 - 调用一个类的静态方法,触发
invokestatic
指令
- 使用
- 使用
java.lang.reflect
包的Method
对象进行反射调用时,如果类没有初始化,会触发初始化 - 初始化一个类的子类会触发父类的初始化
- 虚拟机启动时,会初始化
main
方法所在的类 - 当使用
JDK 1.7
的java.lang.invoke.MethodHandle
实例的invoke
方法句柄时,初始化该MethodHandle
指向的类 - 当一个接口中定义了
default
方法时,如果有这个接口的实现类被初始化,那么这个接口也会被初始化
使用
使用(Using)是类加载器的第六个阶段,主要是程序运行时,Java 虚拟机会执行程序的代码。
JVM 会从 main
方法开始执行,然后按照代码的顺序执行。
卸载
卸载(Unloading)是类加载器的第七个阶段,主要是当类加载器不再需要加载某个类时,Java 会卸载这个类。
类需要满足以下条件才能被卸载:
- 该类的所有实例都已经被回收
- 加载该类的类加载器已经被回收
- 该类的
java.lang.Class
对象没有在任何地方被引用
根据这三条可以看出,JDK 自带的类加载器(启动类加载器、平台类加载器、应用程序类加载器)加载的类是无法被卸载的,因为这些类加载器是一直存在的。
一个栗子
我们接下来看一个例子:
public class InitializationDemo {
public static void main(String[] args) {
staticFunction();
}
int a = 1;
static int b = 2;
static final int c = 3;
static InitializationDemo id = new InitializationDemo();
static final Son son = new Son();
static {
System.out.println("主类的静态代码块");
}
public InitializationDemo() {
System.out.println("主类的构造方法");
System.out.println("a=" + a + " b=" + b + " c=" + c + " d=" + d + " e=" + e + " f=" + f);
}
public static void staticFunction() {
System.out.println("主类的静态方法");
}
{
System.out.println("主类的普通代码块");
}
int d = 4;
static int e = 5;
static final int f = 6;
}
class Son extends Father {
static {
System.out.println("子类的静态代码块");
}
public Son() {
System.out.println("我是子类");
}
}
class Father {
static {
System.out.println("父类的静态代码块");
}
public Father() {
System.out.println("我是父类");
}
}
输出为:
主类的普通代码块
主类的构造方法
a=1 b=2 c=3 d=4 e=0 f=6
父类的静态代码块
子类的静态代码块
我是父类
我是子类
主类的静态代码块
主类的静态方法
我们来分析一下这个程序的输出:
点击展开分析
-
首先 JVM 会初始化
main
方法所在的类InitializationDemo
类,首先执行static
:static int b = 2; static final int c = 3; static InitializationDemo id = new InitializationDemo(); static Son son = new Son(); static { System.out.println("主类的静态代码块"); } static int e = 5; static final int f = 6;
-
为所有静态变量(
b
、c
、id
、son
、e
、f
)分配内存,其中:-
b
赋予默认值0
-
c
为final
,赋予3
-
id
赋予默认值null
-
son
虽然为final
,但此刻无法执行new
,赋予默认值null
-
e
赋予默认值0
-
f
为final
,赋予6
-
-
静态变量依次初始化:
-
b
赋予2
-
id
赋予new InitializationDemo()
,触发了InitializationDemo
的构造方法
-
-
-
InitializationDemo
实例化时:-
首先执行普通代码块和普通变量初始化:
int a = 1; { System.out.println("主类的普通代码块"); } int d = 4;
-
a
赋予1
- 输出
主类的普通代码块
-
d
赋予4
-
-
然后执行构造方法:
public InitializationDemo() { System.out.println("主类的构造方法"); System.out.println("a=" + a + " b=" + b + " c=" + c + " d=" + d + " e=" + e + " f=" + f); }
- 输出
主类的构造方法
- 输出
a=1 b=2 c=3 d=4 e=0 f=6
- 输出
-
-
然后回到
static
,将刚刚实例化后的InitializationDemo
类赋予id
-
继续执行
static
部分,son
也有new
,触发了Son
类的初始化。 -
Son
类初始化时,由于继承了Father
类,因此Father
类也会被初始化。-
首先执行
Father
类的static
:static { System.out.println("父类的静态代码块"); }
- 输出
父类的静态代码块
- 输出
-
然后执行
Son
类的static
:static { System.out.println("子类的静态代码块"); }
- 输出
子类的静态代码块
- 输出
-
然后执行
Father
类的构造方法:public Father() { System.out.println("我是父类"); }
- 输出
我是父类
- 输出
-
然后执行
Son
类的构造方法:public Son() { System.out.println("我是子类"); }
- 输出
我是子类
- 输出
-
-
最后回到
InitializationDemo
类的static
部分,输出主类的静态代码块
-
然后
e
终于被赋值5
-
最后执行
main
方法,输出主类的静态方法
我想,现在已经完全搞明白了类加载了。
垃圾回收
垃圾回收(GC)是 Java 虚拟机的一个重要组成部分,它的作用是在程序运行时,自动回收不再使用的内存。
垃圾判断算法
要想判断一个对象是否是垃圾,就需要用到垃圾判断算法。
引用回收
在 Java 中,引用类型有四种:强引用、软引用、弱引用和虚引用。
-
强引用:是 Java 中最常见的引用类型,它的生命周期和对象的生命周期一样长,只有当没有任何引用指向对象时,对象才可以被回收。
Object obj = new Object();
-
软引用:是一种相对强引用弱化了一些的引用类型,只有当内存不足时,JVM 会尝试回收软引用指向的对象。软引用可以用来实现缓存。
SoftReference<Object> softRef = new SoftReference<>(new Object());
-
弱引用:是一种比软引用更弱化了的引用类型,当 JVM 进行垃圾回收时,无论内存是否充足,都会回收弱引用指向的对象。
WeakReference<Object> weakRef = new WeakReference<>(new Object());
弱引用常常和
ReferenceQueue
一起使用,当对象被回收时,会将对象的引用加入到ReferenceQueue
中。程序可以通过ReferenceQueue
来跟踪对象被回收的状态。ReferenceQueue<Object> refQueue = new ReferenceQueue<>(); WeakReference<Object> weakRef = new WeakReference<>(new Object(), refQueue);
-
虚引用:是一种最弱化了的引用类型,它的作用是跟踪对象被垃圾回收的状态,当对象被垃圾回收时,虚引用会收到一个通知。
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), new ReferenceQueue<>());
引用通常由引用计数法和可达性分析法来判断是否是垃圾。
-
引用计数算法
引用计数算法是一种最基本的判断算法,它的原理是通过引用计数来判断对象是否存活。当对象被引用时,引用计数加 1;当对象被取消引用时,引用计数减 1。当引用计数为 0 时,说明对象不再被引用,可以被回收。
引用计数算法的优点是实现简单,缺点是无法解决循环引用的问题。例如,对象 A 引用了对象 B,对象 B 引用了对象 A,这样两个对象的引用计数永远不会为 0,也永远不会被回收。这样,引用计数算法就会导致内存泄漏。
-
可达性分析算法
可达性分析算法是一种更为高效的垃圾回收算法,它的原理是通过一组根对象,递归地遍历所有的引用,标记所有的存活对象。当遍历完成后,未被标记的对象就是垃圾对象,可以被回收。
这里的根对象是指一组对象,它们是程序的入口,可以直接或间接地引用到所有的对象。根据文档,它们包括了:
- System Class,如
java.util.*
- JNI Local,即 native 方法中的局部变量
- JNI Global,即 native 方法中的全局变量
- Thread Block,即当前活跃线程引用的对象
- Thread,已经
start
但没有stop
的线程 - Busy Monitor,即正在等待锁或者正在使用锁的线程
- Java Local,即仍然在线程栈中的对象
- Native Stack,例如用于文件或网络的方法或者反射
- Finalizable,即
finalize
方法还没有执行的对象 - Unfinalized,即有
finalize
方法但还没有执行的对象 - Unreachable,即不可达的对象,但被 MAT 标记为根
- Java Stack Frame,即 Java 栈帧中的对象
- Unknown,其它未知的对象
- System Class,如
常量池回收
在 JDK 1.8 之后,常量池被分为了堆中的字符串常量池和方法区中的运行时常量池。
对于常量池中的内容,只要没有引用指向它,就可以被回收。
方法区的类回收
判定方法区中的一个类可以被回收需要同时满足以下三个条件:
- 该类的所有实例都已经被回收
- 加载该类的类加载器已经被回收
- 该类的
java.lang.Class
对象没有在任何地方被引用
垃圾回收算法
我们前面讲到,堆被分为新生代、老年代和永久代(JDK 1.7 之前)或元空间(JDK 1.8 之后)。不同的内存区域使用不同的垃圾回收算法。这些算法总体上可以分为以下几种:
标记-清除算法
标记-清除算法就是可达性分析算法的基本应用,它分为两个阶段:
- 每个对象创建时都会有一个标记位,初始为未标记
- 从一组根对象开始,递归地遍历所有的引用,标记所有的存活对象
- 遍历所有的对象,清除所有未标记的对象
标记-清除算法的优点是足够简单,而且只要遍历两遍内存。但它的缺点是 GC 后的内存不连续,会产生内存碎片。
复制算法
复制算法(Copying)是一种将内存分为两块的垃圾回收算法,分别为 From
和 To
,正常只使用 From
块。它的垃圾回收过程分为以下几步:
- 从一组根对象开始,递归地遍历所有的引用,标记所有的存活对象
- 将存活的对象复制到
To
内存块 - 然后清理
From
内存块的所有对象 - 清除完毕后,将
From
和To
两个名字互换
复制算法的优点是不会产生内存碎片,缺点是空间浪费了一半。同时,如果存活对象较多或者对象较大,复制算法的效率会很低。
标记-压缩算法
标记-压缩算法(Mark-and-Compact)
- 从一组根对象开始,递归地遍历所有的引用,标记所有的存活对象
- 将存活的对象压缩整理到内存的一端,使其形成连续的内存
- 清理另一端的剩余内存
标记-压缩算法的优点是不会产生内存碎片,但整理的过程需要扫描内存多次,更加适合 GC 频率不高的场景。
垃圾回收器
垃圾回收器被用来应用以上介绍的垃圾回收算法,针对不同的内存区域使用不同的垃圾回收器。
Serial Collector
串行收集器是一种单线程的垃圾回收器
- 新生代:使用的是复制算法,当垃圾回收时,其它所有的线程都会挂起,直到垃圾回收完成
- 老年代:使用的是标记-压缩算法,同样会触发 Stop-The-World
串行收集器适合用于并发能力较弱的机器上。
ParNew Collector
ParNew 收集器是一种多线程的垃圾回收器
- 新生代:它单纯的将串行收集器在新生代使用多线程运行,但它同样会触发 Stop-The-World,只是停顿的时间会更短。
Parallel Scavenge Collector
Parallel Scavenge 收集器是一种多线程的垃圾回收器
- 新生代:它和 ParNew 收集器几乎相同,唯一的区别是它拥有自适应的调节策略,它可以设置最大停顿时间和最大吞吐量(即程序运行时间与垃圾回收时间的比值)。
Serial Old Collector
Serial Old 收集器是一种单线程的垃圾回收器
- 老年代:它使用的是标记-压缩算法。在 JDK 1.5 之前,它是老年代的默认垃圾回收器。而目前,它是 CMS 收集器的备用垃圾回收器。
Parallel Old Collector
Parallel Old 收集器是一种多线程的垃圾回收器
- 老年代:它使用的是标记-压缩算法。它和 Parallel Scavenge 收集器几乎相同,唯一的区别是它是老年代的垃圾回收器。
CMS Collector
CMS 收集器是一种并发的垃圾回收器,它可以让 GC 线程和用户线程同时执行。它在工作时分为以下几个阶段:
- 初始标记阶段:标记所有和根对象直接连接的对象
- 并发标记阶段:同时运行 GC 线程和用户线程,标记所有的存活对象。由于在此阶段用户线程也在运行,因此可能会有新的对象产生,所以这个阶段的标记并不是最终的标记。它会记录下在此期间发送了变动的对象,以便在下一个阶段重新标记时重新标记这些对象。
- 预清理阶段(可选):同时运行 GC 线程和用户线程,清理当前确定为垃圾的对象
- 重新标记阶段:重新标记所有的存活对象
- 并发清除阶段:同时运行 GC 线程和用户线程,并发地清除所有的未标记对象
- 并发重置阶段:同时运行 GC 线程和用户线程,重置 CMS 收集器的内部状态
其中,只有初始标记阶段和重新标记阶段会触发 Stop-The-World,其它阶段都是并发的。
CMS 收集器的停顿时间很短,适合用于对停顿时间要求较高的应用程序。然而,它有几个缺点:
- 由于并发执行,它的吞吐量较低
- 它无法处理浮动垃圾,即在并发标记阶段产生的新对象
- 它无法处理内存碎片,可能会导致内存泄漏
CMS 收集器已经在 JDK 9 中被标记为废弃,在 JDK 14 中被移除。
G1 Collector
G1 收集器是一种面向服务端的垃圾回收器。它的特点是年轻代、老年代和元数据都可以不是连续的,而是由多个小块组成的。每个小块的大小可以设置为 1/2/4/8/16/31 MB。G1 收集器的工作过程如下:
- 初始标记阶段:标记所有和根对象直接连接的对象
- 并发标记阶段:同时运行 GC 线程和用户线程,标记所有的存活对象
- 最终标记阶段:处理在并发标记阶段产生的新对象
- 筛选阶段:根据各个小块的回收价值,选择回收价值最高的小块进行回收
其中,只有并发标记阶段不会触发 Stop-The-World。
G1 收集器通过内存分块并回收价值最高的小块,可以有效提高内存回收的效率,并拥有可预测的停顿时间。
从 JDK 9 开始,G1 收集器已经成为了默认的垃圾回收器。
ZGC Collector
ZGC 收集器是一种低延迟的垃圾回收器,它的特点是停顿时间短,可以控制在 10ms 以内。它的工作过程如下:
- 初始标记阶段:标记所有和根对象直接连接的对象
- 并发标记阶段:同时运行 GC 线程和用户线程,标记所有的存活对象
- 重新标记阶段:处理在并发标记阶段产生的新对象
- 并发预备重分配阶段:同时运行 GC 线程和用户线程,准备重分配内存,处理软引用和弱引用等
- 初始迁移阶段:标记所有和根对象直接连接的对象
- 并发迁移阶段:同时运行 GC 线程和用户线程,迁移所有的存活对象
- 并发重映射阶段:同时运行 GC 线程和用户线程,重映射所有的存活对象
相比于 G1 收集器,ZGC 收集器几乎全程并发运行,因此停顿时间更短。
垃圾回收的类型
垃圾回收的类型主要有以下几种:
Minor GC / Young GC
Minor GC 是指对新生代进行垃圾回收。当新生代的 Eden 区满时,会触发 Minor GC。Minor GC 会将 Eden 区和 From Survivor 区的存活对象复制到 To Survivor 区,然后清理 Eden 区和 From Survivor 区。
Minor GC 一定是 Stop-The-World 的,然而,由于 Eden 区的绝大多数对象生命周期极短,用完就扔,因此这个停顿时间通常很短。这也是为什么 Eden 区会占了新生代约 80% 的空间。
Major GC / Old GC
Major GC 是指对老年代进行垃圾回收。当老年代满时,会触发 Major GC。Major GC 会对老年代进行垃圾回收,清理掉不再使用的对象。
Major GC 常常由 Minor GC 触发,因为 Minor GC 会将存活对象复制到老年代。如果此时老年代满了,就会触发 Major GC。
Full GC
Full GC 是指对整个堆进行垃圾回收。Full GC 会对新生代和老年代进行垃圾回收,清理掉不再使用的对象。
当准备触发 Minor GC 时,如果发现年轻代的晋升空间比以往要小,就会转为触发 Full GC。Full GC 会清理整个堆,包括新生代和老年代。
此外,如果永久代满了,也会触发 Full GC。
性能调优
性能调优是 Java 程序优化的一个重要环节,它的目的是提高程序的性能,减少资源的消耗。
这里,我们直接列出常用的:
JVM 参数
-
空间参数
- 堆内存
-
-Xms
:设置堆的初始大小,例如-Xms512m
单位有
k
、m
、g
,分别表示 KB、MB、GB -
-Xmx
:设置堆的最大大小,例如-Xmx1024m
-
-XX:MaxHeapFreeRatio
:设置堆的最大空闲比例,默认为 70%,防止自动收缩 -
-XX:+UseStringCache
:启用字符串缓存 -
-XX:+UseCompressedStrings
:启用字符串压缩 -
-XX:+OptimizeStringConcat
:启用字符串拼接优化
-
- 新生代
-
-Xmn
:设置新生代的大小,例如-Xmn256m
-
-XX:NewSize
:设置新生代的初始大小 -
-XX:MaxNewSize
:设置新生代的最大大小 -
-XX:SurvivorRatio
:设置 Eden 区和 Survivor 区的比例,默认为 8,即 Eden 区占 8/10,Survivor 区占 1/10
-
- 老年代
- 老年代没有单独的参数,它的大小由
-Xmx
减去新生代的大小得到 -
-XX:NewRatio
:设置新生代和老年代的比例,默认为 2,即新生代占 1/3,老年代占 2/3
- 老年代没有单独的参数,它的大小由
- 永久代/元空间
-
-XX:PermSize
:设置永久代的初始大小 -
-XX:MaxPermSize
:设置永久代的最大大小 -
-XX:MetaspaceSize
:设置元空间发生 Full GC 的阈值,而其初始化大小总是为218070104
字节 -
-XX:MaxMetaspaceSize
:设置元空间的最大大小
-
- 直接内存
-
-XX:MaxDirectMemorySize
:设置直接内存的最大大小
-
- 堆内存
-
垃圾回收参数
- 垃圾回收
-
-XX:+UseSerialGC
:使用串行收集器 -
-XX:+UseParNewGC
:使用 ParNew 收集器 -
-XX:+UseParallelGC
:使用 Parallel 收集器 -
-XX:+UseParallelOldGC
:使用 Parallel Old 收集器 -
-XX:+UseConcMarkSweepGC
:使用 CMS 收集器 -
-XX:+UseG1GC
:使用 G1 收集器 -
-XX:+UseZGC
:使用 ZGC 收集器
-
- 垃圾回收日志
-
-XX:+PrintGCDetails
:打印 GC 详细信息 -
-XX:+PrintGCDateStamps
:打印 GC 时间戳 -
-XX:_PrintVMOptions
:打印虚拟机参数 -
-XX:+PrintCommandLineFlags
:打印命令行参数 -
-XX:+PrintFlagsFinal
:打印所有参数 - 日志输出
-
-Xloggc:/path/to/gc-%t.log
:将 GC 日志输出到/path/to/gc-%t.log
文件中 -
-XX:+UseGCLogFileRotation
:启用 GC 日志轮换 -
-XX:NumberOfGCLogFiles=5
:设置 GC 日志文件的数量 -
-XX:GCLogFileSize=10M
:设置 GC 日志文件的大小
-
- 详细信息
-
-XX:+PrintTenuringDistribution
:打印对象年龄分布 -
-XX:+PrintHeapAtGC
:在 GC 时打印堆信息 -
-XX:+PrintReferenceGC
:打印 GC 时的引用信息 -
-XX:_PrintGCTimeStamps
:打印 GC 时间戳 -
-XX:+PrintGCApplicationConcurrentTime
:打印 GC 时应用程序运行的时间 -
-XX:+PrintGCApplicationStoppedTime
:打印 GC 时应用程序停止的时间
-
-
- 垃圾回收
-
内存转储
-
-XX:+HeapDumpOnOutOfMemoryError
:在内存溢出时生成堆转储文件 -
-XX:HeapDumpPath=/path/to/dump.hprof
:设置堆转储文件的路径 -
-XX:OnOutOfMemoryError="kill -9 %p"
:在内存溢出时执行命令 -
-XX:+UseGCOverheadLimit
:在 GC 超过 98% 的时间时抛出OutOfMemoryError
-
JDK 命令
-
jps:查看 Java 进程
-
jps -l
:显示完整的包名,如com.example.Main
-
jps -v
:显示 JVM 参数,如-Dfile.encoding=UTF-8
-
jps -q
:只显示进程 ID -
jps -m
:显示传递给main
方法的参数
-
-
jstat:查看 JVM 统计信息
-
jstat -gcutil pid
:查看 GC 使用率 -
jstat -gc pid
:查看 GC 统计信息 -
jstat -gcnew pid
:查看新生代 GC 统计信息 -
jstat -gcold pid
:查看老年代 GC 统计信息 -
jstat -gccapacity pid
:查看 GC 容量信息 -
jstat -gcmetacapacity pid
:查看元空间容量信息 -
jstat -class pid
:查看类加载信息 -
jstat -compiler pid
:查看 JIT 编译信息
-
-
jinfo:查看 JVM 参数
-
jinfo pid
:查看所有参数 -
jinfo -flag name pid
:查看指定参数 -
jinfo -flag [+|-]name pid
:启用或禁用参数
-
-
jmap:生成堆转储文件
-
jmap -dump:format=b,file=dump.hprof pid
:生成堆转储文件 -
jmap -heap pid
:查看堆信息 -
jmap -histo pid
:查看堆直方图 -
jmap -permstat pid
:查看永久代信息
-
-
jhat:分析堆转储文件
-
jhat dump.hprof
:分析堆转储文件
-
-
jstack:生成线程转储文件
-
jstack pid
:生成线程转储文件 -
jstack -l pid
:生成线程转储文件,包括锁信息
-
Comments