Kyle's Notebook

Java 内存区域

Word count: 2.4kReading time: 8 min
2020/06/30

Java 内存区域

  • 运行时数据区(Runtime Data Area)
    • 线程区域
      • 程序计数器(Program Counter Register)
      • 本地方法栈(Native Method Stack)
      • Java 虚拟机栈(JVM Stack)
        • 栈帧(Stack Frame)
          • 局部变量表(Local Variable Array)
            • 基本数据类型
            • 对象引用(reference)
            • 字节码指令地址(returnAddress)
          • 操作数栈(Operand Stack)
          • 常量池引用(Reference to Constant Pool)
          • 动态链接
          • 方法出口
    • 共享区域
      • 堆(Heap)
        • 新生代(Young Generation)
          • Eden
          • From Survivor
          • To Survivor
        • 老年代(Old Generation)
        • 永久代(Permanent Generation)
        • 线程私有缓冲区(TLAB)
      • 方法区(Method Area)
        • 运行时常量池(Runtime Constant Pool)
        • 类的版本、字段、方法,接口,静态静态变量等描述信息
  • 直接内存(Direct Memory)
    • 元空间(Meta Area)
  • 其他区域
    • JIT 代码缓存(Code Cache)

线程区域

该区域为每个线程独占内存,包括程序计数器、本地方法栈、Java 虚拟机栈。

程序计数器(Program Counter Register)

对于 Java 方法,记录正在执行的字节码指令的地址。

  • 字节码解释器通过改变程序计数器来依次读取指令,实现代码的流程控制,如:顺序执行、选择、循环、异常处理等;

  • 在多线程的情况下,线程轮流切换并分配处理器执行时间,PCR 用于记录当前线程执行的位置,便于切换时恢复;

  • 对于 Native 方法,计数器为空;

  • JVM 规范中唯一没有定义 OOM 情况的区域。

本地方法栈(Native Method Stack)

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

该区域中使用的语言、使用方式和数据结构没有强制规定,因此 JVM 可以自由实现它。
本地方法.png

Java 虚拟机栈(JVM Stack)

Java 虚拟机栈生命周期与线程相同。

每个 Java 方法在执行的同时会创建一个栈帧,用于存储局部变量表、操作数栈、常量池引用等信息。

从方法调用直至执行完成的过程,对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。

栈帧.jpg

设置每个线程的 Java 虚拟机栈内存大小:

1
java -Xss512M HackTheJava
  • 其中当线程请求栈深度大于 JVM 允许的栈深度(比如递归),将抛出 SOF;JVM 栈可动态扩展,当栈扩展仍无法申请到足够内存则抛出 OOM。

  • 当限制栈大小(不允许动态扩展),则无论时栈帧太大还是 JVM 栈容量太小,都是抛出 SOF。

栈帧(Stack Frame)

每个方法执行时, JVM 都会同步创建一个栈帧。其需要使用多少内存,在类结构确定时就可以确定。

操作数栈(Operand Stack)

存放计算的操作数和返回结果,元素可以是任意 Java 类型,32 位数据占 1 个栈容量,64 位数据占 2 个栈容量。

执行每一条指令之前,JVM 要求操作数已被压入操作数栈中。在执行指令时,JVM 会将该指令所需的操作数弹出,并且将指令的结果重新压入栈中。

常量池引用(Reference to Constant Pool)

动态链接

指向运行时常量池中该栈帧所属方法的引用。

方法出口

即方法返回地址,方法在遇到 return 或触发异常后退出。

退出方法时可能执行的操作:

  • 恢复上层方法的局部变量表和操作数栈;

  • 把返回值压入调用者栈帧的操作数栈;

  • 调整 PC 计数器指向方法调用后面的指令。

局部变量表(Local Variable Array)

其等价于一个数组,所存放的元素、最大容量在编译期可知。一般包含以下内容:

  • 基本数据类型:以 Slot 为最小单位(32 位)分配内存,64 位长度的 long 和 double 类型会占用 2 个局部变量空间(Slot),其余的数据类型只占用一个。

  • 对象引用(reference):不等同于对象本身,可能是一个指向对象初始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置。可查找 Java 堆中的实例对象的起始地址、查找方法区中的 Class 对象。

  • 字节码指令地址(returnAddress):指向一条字节码指令的地址。

至少存在一个指向当前对象实例的局部变量,预留出第一个 Slot 位存放对象实例的引用,方法参数值从 1 开始计算(this 关键字访问自动转为对一个普通方法参数的访问,由 JVM 调实例方法时自动传入此参数)。

Slot 的复用: 如果当前位置已经超过某个变量的作用域时,这个变量对应的 Slot 可以给其他变量服用。同时也说明只要其他变量没有使用这部分 Slot 区域,这个变量还会保存,因此这会对 GC 操作产生影响。

1
2
3
4
5
6
public static void main(String[] args) {
{
byte[] placeholder = new byte[64 * 1024 * 1024];
}
System.gc();
}

-verbose:gc 输出(没有被回收):

1
2
[GC (System.gc())  68813K->66304K(123904K), 0.0034797 secs]
[Full GC (System.gc()) 66304K->66204K(123904K), 0.0086225 secs]

进行如下修改:

1
2
3
4
5
6
7
public static void main(String[] args) {
{
byte[] placeholder = new byte[64 * 1024 * 1024];
}
int a = 1; // 新加一个赋值操作
System.gc();
}

-verbose:gc 输出(被回收了):

1
2
[GC (System.gc())  68813K->66320K(123904K), 0.0017394 secs]
[Full GC (System.gc()) 66320K->668K(123904K), 0.0084337 secs]

被回收的关键在于局部变量表中的 Slot 是否还存在关于 placeholder 的引用:

  • 出了 placeholder 所在的代码块后,还没有进行其他操作,所以 placeholder 所在的 Slot 还没有被其他变量复用,也就是说,局部变量表的 Slot 中依然存在着 placeholder 的引用;

  • 第二次修改后,int a 占用了原来 placeholder 所在的 Slot,所以可以被 GC 掉了。

共享区域

堆(Heap)

为 Java 程序中的对象分配内存,GC 的主要区域。

  • 存放几乎所有对象实例和数组(了解“JIT 编译器”、“逃逸分析”,可实现栈上分配、标量替换优化技术)。
  • 对于 Oracle Hotspot JVM 而言,可以明确所有对象实例都是在堆上创建(包括静态遍历和 Intern 字符串缓存),参考 文档
  • 对于 byte、char 以及 short 这三种类型的字段或者数组单元,它们在堆上占用的空间分别为 1byte、2bytes,以及 2bytes,与这些类型的值域相吻合。
  • 现代的垃圾收集器基本都是采用分代收集算法,针对 新生代老年代
  • 可划分出多个线程私有的分配缓冲区(TLAB):与存放内容无关,都是存储对象实例。
  • 不需要连续的物理内存,可以动态增加其内存,失败时会抛出 OutOfMemoryError 异常。

G1 收集器1.png

主流的 JVM 支持堆扩展,通过 -Xms(初始值)和 -Xmx(最大值)指定一个程序的堆内存大小。

1
java -Xms1M -Xmx2M HackTheJava

指定新生代大小:-Xmn,所以堆内存为:

1
-Xms, -Xmx == -Xmn + 老年代

新生代(Young Generation)

  • 大多数对象(80%)生存时间很短,主要使用复制算法。

  • Survivor 区中每经过一次 Minor GC 都没有被清理的对象,年龄会增加一岁,增加到一定程度就会被移动到老年代。

  • 如果使用 Appel 划分,则分为 Eden 区和 Survivor 区。

老年代(Old Generation)

进入老年代的四种对象:

  • 对象年龄太大(经历太多次 GC 后存活);

  • 对象占用内存太大;

  • Young GC 后存活对象过多、Survivor 放不下;

  • 动态年龄判断。

永久代(Permanent Generation)

容易造成内存溢出,JDK 1.8 已移除。

线程私有缓冲区(TLAB)

从内存分配的角度来看,线程共享的 Java 堆中可能划分出多个线程私有的线程分配缓冲区(TLAB,按内存分配分区)。

实际上这部分不属于共享区域,目的是更好地回收内存、提升对象分配的效率。

方法区(Method Area)

存放运行时常量池,以及类的版本、字段、方法,接口,静态静态变量等描述信息(即时编译器编译后的代码,.class 文件)。

  • 不需要连续的内存,可以动态扩展,动态扩展失败会抛出 OutOfMemoryError 异常;

  • 垃圾回收:主要目标是对常量池的回收和对类的卸载,但比较难实现,未完全回收可能导致内存泄漏。

HotSpot 虚拟机把它当成永久代来进行垃圾回收。

但很难确定其的大小,因为它受到很多因素影响,并且每次 Full GC 之后永久代的大小都会改变,所以经常会抛出 OutOfMemoryError 异常。

运行时常量池(Runtime Constant Pool)

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

  • JVM 规范对于 Class 文件中的运行时常量池没有任何细节要求,可由 JVM 厂商实现;

  • 动态性:除了在编译期生成的常量,还允许动态生成,例如 String.intern()

CATALOG
  1. 1. Java 内存区域
  2. 2. 线程区域
    1. 2.1. 程序计数器(Program Counter Register)
    2. 2.2. 本地方法栈(Native Method Stack)
    3. 2.3. Java 虚拟机栈(JVM Stack)
      1. 2.3.1. 栈帧(Stack Frame)
        1. 2.3.1.1. 操作数栈(Operand Stack)
        2. 2.3.1.2. 常量池引用(Reference to Constant Pool)
        3. 2.3.1.3. 动态链接
        4. 2.3.1.4. 方法出口
        5. 2.3.1.5. 局部变量表(Local Variable Array)
  3. 3. 共享区域
    1. 3.1. 堆(Heap)
      1. 3.1.1. 新生代(Young Generation)
      2. 3.1.2. 老年代(Old Generation)
      3. 3.1.3. 永久代(Permanent Generation)
      4. 3.1.4. 线程私有缓冲区(TLAB)
    2. 3.2. 方法区(Method Area)
      1. 3.2.1. 运行时常量池(Runtime Constant Pool)