Java虚拟机运行时数据区

Java虚拟机有三块组成部分,分别是:类装载子系统、运行时数据区、字节码执行引擎。运行时数据区是Java虚拟机内部的一个运行时内存,是性能调优的重点区域。身为Java工程师,必须要了解底层原理,才能从最根本上让机器发挥出应有的性能。

本章分析的是JDK8的JVM运行时数据区,有关JVM数据区的文档可到Oracle官网进行查阅
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5

概念部分

Java虚拟机定义了在程序执行期间使用的各种运行时数据区域。图中黄色区域是线程共享的,在虚拟机启动时创建,随着虚拟机的停止而销毁。图中绿色区域是线程私有的,随着线程的启动被分配,随着线程的停止而销毁。Java虚拟机运行时数据区主要分为5个部分:程序计数器、Java虚拟机栈、堆、方法区、本地方法栈。

程序计数器(Program Counter Register)

线程私有区域,是一块较小的内存空间,用来记录当前线程所执行的字节码行号。每个线程都有自己的程序计数器,各个线程的程序计数器互不影响。
如果线程正在执行的是一个Java方法,则记录的是字节码的指令地址,如果执行的是本地方法,则计数器的值为空。此区域不会出现OOM的情况。

Java虚拟机栈(Java Virtual Machine Stack)

线程私有区域,生命周期与线程相同。Java虚拟机栈描述的是Java方法执行时的内存模型,每个方法在执行时会在虚拟机栈中创建一个栈帧,进行入栈操作。栈帧内部包含4个区域:本地变量表、操作数栈、动态链接、方法出口。方法执行完毕会将栈帧出栈。

在Java虚拟机规范中,此区域可能出现 StackOverflowError和 OutOfMemoryError。
比如在一个线程中方法调用的深度超出了Java虚拟机栈的大小,就无法为新的方法调用分配栈帧,抛出 StackOverflowError。
在多线程中为方法创建栈帧,每个线程为方法创建的栈大小总和超出了Java虚拟机栈的大小,无法为新的方法调用分配内存空间,就会抛出OutOfMemoryError。

堆(Heap)

线程共享区域,在虚拟机启动时创建,用来存储对象实例,是JVM垃圾收集器的主要区域。
垃圾收集器为了更加高效的进行垃圾回收,采用了各种不同的垃圾回收算法,将堆内存进行划分。
在JDK1.8中,堆内存可以细分为:Eden区、Survivor区(From Survivor,To Survivor)、Old区。

堆区在逻辑上是连续的内存区域,不要求物理内存连续。堆大小可以固定也设置成动态,可以通过JVM运行参数 -Xmx 和 -Xms 指定堆大小。如果堆中的内存无法满足分配要求将会抛出 OutOfMemoryError。
关于垃圾收集策略此处不展开分析,后面会专门写关于垃圾收集的文章。

方法区(Method Area)

线程共享区域,在虚拟机启动时创建,用来存储类信息、常量、静态变量和及时编译后的代码等数据。
可以选择不进行垃圾收集。在JDK7中使用永久代来实现方法区,在JDK8中废弃了永久代采用Metaspace(元空间)来进行代替。
元空间在本地内存中。

在方法区中还包含了一个 运行时常量池(Runtime Constant Pool),用来存放各种字面量和符号引用,这些字面量不一定是在编译期产生,还可以是程序动态生成的,比如通过 String类的intern方法。
如果常量池中的内存无法满足分配要求,将会抛出 OutOfMemoryError。

本地方法栈(Native Method Stacks)

与Java虚拟机方法栈的作用类似,区别在于本地方法栈服务于Native方法,在Java中调用的Native方法都将由本地方法栈为方法分配内存,Java虚拟机并不要求本地方法栈中使用的语言和数据结构,在 HotSpot虚拟机中本地方法栈和Java虚拟机方法栈合二为一。 这个区域同样可能发生StackOverflowError和OutOfMemoryError。

案例分析

一个方法的执行过程

通过一个方法的执行过程,分析Java虚拟机栈内部工作流程

编写如下代码,在一个Demo类中,包含一个main方法和一个compute方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Demo {

public int compute() {
int a = 1;
int b = 2;
int c = a + b;
return c * 100;
}

public static void main(String[] args) {
Demo demo = new Demo();
int result = demo.compute();
System.out.println("result="+result);
}
}

使用javac将代码编译成java字节码文件Demo.class
使用 vim -b Demo.class 打开文件,再使用 :%!xxd 以16进制查看文件,得到如下字节码指令

1
2
3
4
5
6
7
8
9
00000000: cafe babe 0000 0034 0030 0a00 0d00 1807  .......4.0......
00000010: 0019 0a00 0200 180a 0002 001a 0900 1b00 ................
00000020: 1c07 001d 0a00 0600 1808 001e 0a00 0600 ................
00000030: 1f0a 0006 0020 0a00 0600 210a 0022 0023 ..... ....!..".#
00000040: 0700 2401 0006 3c69 6e69 743e 0100 0328 ..$...<init>...(
00000050: 2956 0100 0443 6f64 6501 000f 4c69 6e65 )V...Code...Line
00000060: 4e75 6d62 6572 5461 626c 6501 0007 636f NumberTable...co
00000070: 6d70 7574 6501 0003 2829 4901 0004 6d61 mpute...()I...ma
...

这些字节码指令能够被JVM虚拟机执行,每个指令字符都有对应的解释,可以通过查阅Oracle官方的虚拟机字节码指令表进行对照。
本章不展开分析字节码,后续会写对应的文章分析字节码。
每个字节码指令都有对应的助记符,为了使人类更加易读,可以使用javap命令,将Demo.class 反汇编成等效的虚拟机指令助记符。

1
javap -c Demo.class > Demo.txt

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
Compiled from "Demo.java"
public class com.lanshiqin.Demo {
public com.lanshiqin.Demo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return

public int compute();
Code:
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: istore_3
8: iload_3
9: bipush 100
11: imul
12: ireturn

public static void main(java.lang.String[]);
Code:
0: new #2 // class com/lanshiqin/Demo
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method compute:()I
12: istore_2
13: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
16: new #6 // class java/lang/StringBuilder
19: dup
20: invokespecial #7 // Method java/lang/StringBuilder."<init>":()V
23: ldc #8 // String result=
25: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
28: iload_2
29: invokevirtual #10 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
32: invokevirtual #11 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
35: invokevirtual #12 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
38: return
}

每个方法在执行时,都会创建一个栈帧,该程序中有main方法和compute方法,在Java虚拟机中对应入下图

字节码文件Demo.class被类装载子系统加载到JVM运行时数据区,做了一系列操作,这些操作此处不展开分析。
首先执行的是main方法,Java虚拟机栈对main方法进行入栈操作。
main方法中调用了compute方法,对compute进行入栈。
此处对compute方法的源代码和对应的字节码助记符详细的分析一下

1
2
3
4
5
6
public int compute() {
int a = 1;
int b = 2;
int c = a + b;
return c * 100;
}

为了方便阅读,查阅了虚拟机字节码指令表,将对应助记符的含义注释在右侧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public int compute();
Code:
0: iconst_1 // 将int型1推送至栈顶
1: istore_1 // 将栈顶int型数值存入第二个本地变量
2: iconst_2 // 将int型2推送至栈顶
3: istore_2 // 将栈顶int型数值存入第三个本地变量
4: iload_1 // 将第二个int型本地变量推送至栈顶
5: iload_2 // 将第三个int型本地变量推送至栈顶
6: iadd // 将栈顶两个int型数值相加并将结果压入栈顶
7: istore_3 // 将栈顶int型数值存入第四个本地变量
8: iload_3 // 将第四个int型本地变量推送至栈顶
9: bipush 100 // 将单字节的常量值100 推送至栈顶
11: imul // 将栈顶两个int型数值相乘并将结果压入栈顶
12: ireturn // 从当前方法返回int

可以看出字节码助记符还是很容易理解的,Code:底下每一行代表一个指令,指令前的数字可以当作指令行号来理解,程序计数器利用行号来记录当前执行到了哪一行。

为了便于理解,制作了执行过程的动画:

在compute方法被调用时,会创建computer的栈帧入栈到Java虚拟机栈中,此处省略方法入栈的演示。
ireturn指令,会返回当前操作数栈上的值300,方法执行完成后compute栈帧会出栈,此处省略方法出栈的演示。

将java源代码编译成class字节码后,就可以确定本地变量表的空间大小,compute栈帧中会创建对应大小的固定内存空间。有关虚拟机栈的详细内容此处不展开讨论,后续会有专门的文章进行分析。

StackOverflowError

如果方法的调用深度超过Java虚拟机栈的大小,就会发生StackOverflowError。比如一个方法递归的调用自身,没有退出条件:

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
package com.lanshiqin.jvm;

/**
* HotSpot Java 虚拟机栈和本地方法栈溢出示例
* StackOverflowError
* VM Args:-Xss160k
* @author 蓝士钦
*/
public class StackOF {

private int stackLength = 1;

public void stackLeak(){
stackLength ++;
stackLeak();
}

public static void main(String[] args) {
StackOF stackOF = new StackOF();
try {
stackOF.stackLeak();
}catch (Throwable e){
System.out.println("stack length:" + stackOF.stackLength);
throw e;
}
}
}

错误信息如下:

1
2
3
4
5
6
7
8
9
stack length:773
Exception in thread "main" java.lang.StackOverflowError
at com.lanshiqin.jvm.StackOF.stackLeak(StackOF.java:14)
at com.lanshiqin.jvm.StackOF.stackLeak(StackOF.java:15)
at com.lanshiqin.jvm.StackOF.stackLeak(StackOF.java:15)
at com.lanshiqin.jvm.StackOF.stackLeak(StackOF.java:15)
...
... // 省略
at com.lanshiqin.jvm.StackOF.main(StackOF.java:21)

可以通过-Xss参数指定每个线程运行时的栈大小,栈越大能容纳的方法调用深度就越大。

OutOfMemoryError

如果为每个线程分配过大的方法栈帧,则可能导致OOM,每个方法都占用很大的空间,在多线程执行时就会有很多个方法栈帧,Java虚拟机栈无法为新的方法分配内存空间,就会出现OOM。

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
package com.lanshiqin.jvm;

/**
* Java 虚拟机栈和本地方法栈 多线程内存溢出示例
* OutOfMemoryError,栈帧越大,越容易发生OOM
* VM Args:-Xss100m
* @author 蓝士钦
*/
public class JavaVMStackOOM {

private void dontStop(){
while (true){
// 让方法不被销毁
}
}

public void stackLeakByThread(){
while (true){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
dontStop();
}
});
thread.start();
}
}

public static void main(String[] args) {
JavaVMStackOOM javaVMStackOOM = new JavaVMStackOOM();
javaVMStackOOM.stackLeakByThread();
}
}

这个程序会创建无数个线程,可能会让操作系统假死,需要做好备份。
运行一段时间后,报错信息如下:

1
2
3
4
5
6
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
at java.lang.Thread.start0(Native Method)
at java.lang.Thread.start(Thread.java:717)
at com.lanshiqin.jvm.JavaVMStackOOM.stackLeakByThread(JavaVMStackOOM.java:25)
at com.lanshiqin.jvm.JavaVMStackOOM.main(JavaVMStackOOM.java:31)
Java HotSpot(TM) 64-Bit Server VM warning: Exception java.lang.OutOfMemoryError occurred dispatching signal SIGINT to handler- the VM may need to be forcibly terminated

大多数情况下,OOM 更多的是发生在堆内存分配,可以通过JVM参数 -Xms指定最小堆内存空间,-Xmx最大堆内存空间,当堆无法为新对象分配内存时,抛出OOM。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.lanshiqin.jvm;

import java.util.ArrayList;
import java.util.List;

/**
* Java堆溢出示例
* OutOfMemoryError
* VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./java_heap_dump.hprof
*
* @author 蓝士钦
*/
public class HeapOOM {
static class OOMObject {
}

public static void main(String[] args) {
List<OOMObject> list = new ArrayList<>();
while (true) {
list.add(new OOMObject());
}
}
}

错误信息如下:

1
2
3
4
5
6
7
8
9
10
11
java.lang.OutOfMemoryError: Java heap space
Dumping heap to ./java_heap_dump.hprof ...
Unable to create ./java_heap_dump.hprof: File exists
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:265)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:239)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:231)
at java.util.ArrayList.add(ArrayList.java:462)
at com.lanshiqin.jvm.HeapOOM.main(HeapOOM.java:20)

本章对JVM运行时数据区做简单的概念分析,后续会更加底层的分析运行时的每个内存区域,转载请注明出处。

0%