深入理解JVM虚拟机:Java运行时数据区域 - Sanarous的博客

深入理解JVM虚拟机:Java运行时数据区域

Java运行时数据区域分析

JDK1.8JVM运行时数据区域结构模型如下:

我们可以从整体看到,JVM运行时数据区域大致可以分为:程序计数器、Java虚拟机栈、本地方法栈、堆区、元空间、运行时常量池、直接内存等区域。
JDK1.6时,字符串常量池位于永久代的运行时常量池中,从JDK1.7开始,字符串常量池就已经从永久代剥离放入了堆中。JDK1.8开始,元空间取代了永久代,并且放入了本地内存(物理内存)中。

其中,线程隔离的有:

  • 程序计数器
  • Java虚拟机栈
  • 本地方法栈

其它的都是线程共享的区域。

下面我们来一一详解其中每部分的作用。

1、程序计数器

  程序计数器(Program Counter Register)可以看作是当前线程所执行的字节码的行号指示器,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。每个线程都需要有一个独立的程序计数器,因此PC是“线程私有”的。并且这个计数器记录的是正在执行的虚拟机字节码指令的地址(如果是Native本地方法,那么计数器值为Undifined)。

2、Java虚拟机栈

  与程序计数器一样,Java虚拟机栈也是线程私有的,并且其生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型,每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。

可以通过 -Xss 这个虚拟机参数来指定每个线程的 Java 虚拟机栈内存大小:

1
java -Xss512M HackTheJava

该区域可能抛出以下异常:

  • 当线程请求的栈深度超过最大值,会抛出StackOverflowError异常;

  • 栈进行动态扩展时如果无法申请到足够内存,会抛出OutOfMemoryError异常。

3、本地方法栈

本地方法栈与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。

本地方法一般是用其它语言(C、C++ 或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理。

与Java虚拟机栈一样,本地方法栈区域也会抛出StackOverFlowOutOfMemoryError异常。

4、堆

所有对象都在这里分配内存,是垃圾收集的主要区域(GC 堆)。

现代的垃圾收集器基本都是采用分代收集算法,其主要的思想是针对不同类型的对象采取不同的垃圾回收算法。可以将堆分成两块:

  • 新生代(Young Generation)
  • 老年代(Old Generation)

具体地,也可以细致分为 Eden 区、From Survivor 区和 To Survivor 区。

堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。

可以通过 -Xms-Xmx 这两个虚拟机参数来指定一个程序的堆内存大小,第一个参数设置初始值,第二个参数设置最大值。

1
java -Xms1M -Xmx2M HackTheJava

5、方法区

用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法去描述为堆的一个逻辑部分,但是它却有一个别名叫做Non0Heap(非堆),目的应该是与Java堆区分开。

不管是永久代还是元数据区都是JVM规范中方法区的实现方式。方法区和堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。

对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现。HotSpot 虚拟机把它当成永久代来进行垃圾回收。但很难确定永久代的大小,因为它受到很多因素影响,并且每次 Full GC 之后永久代的大小都会改变,所以经常会抛出 OutOfMemoryError 异常。为了更容易管理方法区,从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。

6、运行时常量池

Class 文件中的常量池(编译器生成的字面量和符号引用)会在类加载后被放入这个区域。

除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern( )。

在 JDK1.7 之前,字符串常量池包括于运行时常量池中,在其后,字符串常量池被剥离出运行时常量池,放入了堆中,而运行时常量池仍然在方法区(现在称为元空间区)中。

我们可以在不同的JDK版本中验证一下,测试方式是以死循环方式创建字符串常量:

JVM参数配置:

1
2
3
4
5
-XX:MaxPermSize=10m
-XX:PermSize=10m
-Xms100m
-Xmx100m
-XX:-UseGCOverheadLimit

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class StringOomMock {
public static void main(String[] args) {
try {
List<String> list = new ArrayList<String>();
for (int i = 0; ; i++) {
System.out.println(i);
list.add(String.valueOf("String" + i++).intern());
}
} catch (java.lang.Exception e) {
e.printStackTrace();
}
}
}

JDK1.6 下运行结果:永久代 OOM 异常

1
2
3
4
5
153658
153660
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
at java.lang.String.intern(Native Method)
at com.jd.im.StringOomMock.main(StringOomMock.java:17)

JDK1.7 下运行结果:堆区 OOM 异常,并且伴随频繁的 FullGC,CPU 一直处于高位运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2252792
2252794
2252796
2252798
*** java.lang.instrument ASSERTION FAILED ***: "!errorOutstanding" with message can't create name string at ../../../src/share/instrument/JPLISAgent.c line: 807
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.nio.CharBuffer.wrap(CharBuffer.java:369)
at sun.nio.cs.StreamEncoder.implWrite(StreamEncoder.java:265)
at sun.nio.cs.StreamEncoder.write(StreamEncoder.java:125)
at java.io.OutputStreamWriter.write(OutputStreamWriter.java:207)
at java.io.BufferedWriter.flushBuffer(BufferedWriter.java:129)
at java.io.PrintStream.write(PrintStream.java:526)
at java.io.PrintStream.print(PrintStream.java:597)
at java.io.PrintStream.println(PrintStream.java:736)
at com.jd.im.StringOomMock.main(StringOomMock.java:16)

JDK1.8下运行结果:与 JDK1.7 一样,都是 OOM 异常

1
2
3
4
5
6
7
8
9
2236898
2236900
2236902
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.lang.Integer.toString(Integer.java:403)
at java.lang.String.valueOf(String.java:3099)
at java.io.PrintStream.print(PrintStream.java:597)
at java.io.PrintStream.println(PrintStream.java:736)
at com.jd.im.StringOomMock.main(StringOomMock.java:16)

7、直接内存

在 JDK 1.4 中新引入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在堆内存和堆外内存来回拷贝数据。

参考文章:

  1. 周志明《深入理解Java虚拟机:JVM高级特性与最佳实战》
  2. JDK1.8 JVM运行时数据区域划分
  3. 基于JDK1.8 分析运行时常量池、字符串常量池、各种常量池
如果这篇文章对您很有帮助,不妨
-------------    本文结束  感谢您的阅读    -------------
0%