【Java核心-进阶】JVM 内存区域划分


【Java核心-进阶】JVM 内存区域划分
 

注:Virtual 表示已申请,但未真正使用的内存区域(最小堆容量(Xms)不等于最大堆容量(Xmx)时会出现该现象)

 

1. 程序计数器

PC,Program Counter Register

  • 存放线程当前所执行方法的指令地址
    • 如果当前执行的是本地方法,而非 Java方法,则为 undefined (未指定值)
  • 每个Java线程都有一个程序计数器
  • 任何时间,一个线程只有一个方法在执行——当前方法

2. Java虚拟机栈

  • 每个Java线程都有一个 Java虚拟机栈
  • 栈中每个单元——栈帧——对应一次Java方法调用
  • 与程序计数器对应,任何时间点,一个栈只有一个活动的栈帧——当前栈帧
  • 执行新方法时,创建新的栈帧并入栈;方法结束时,对应的栈帧出栈
  • 栈帧存放:局部变量表、操作数栈(operand)、动态链接、方法退出的定义(正常退出或异常退出)

3. 本地方法栈

  • 与Java虚拟机栈类似
  • 用于支持本地方法的调用
  • 每个线程都有一个本地方法栈
  • Oracle Hotspot JVM 中,本地方法栈 与 Java虚拟机栈 是同一片区域

4. 堆

  • 存放 Java对象实例
    • Oracle Hotspot JVM 会将所有 Java对象都存放在堆中
    • Intern 字符串缓存 和 静态变量 都存放在堆中 (Oracle JDK 8)
  • 所有线程共享堆
    • 除了TLAB,Thread Local Allocation Buffer。这是大部分JVM具体实现的一个设计
  • 垃圾收集器(GC)不同,堆的具体划分方式也不同
    • 本文主要涉及JDK8的典型设置

4.1 新生代

大部分对象会在该区域创建、销毁。

4.1.1 Eden

对象一般会先被创建分配在Eden区

4.1.1.1 TLAB, Thread Local Allocation Buffer

这是Eden的一部分。每个线程都有一个TLAB,供其缓存数据。

这样可以减少多线程场景中,分配内存是需要加锁的开销。

 

4.1.2 Survivor

用来放 Minor GC后存活的对象。

两个Survivor区,一个 from,一个 to。GC过程中,Eden存活的对象和 from区的对象会被复制到 to区;to区就成了下一次GC的 from区,原 from区则成为 to区。

这样便于整理内存,防止内存碎片化。

 

4.2 老年代

一般存放从Survivor 区存活的对象

 

对象分配的一般逻辑

  • 普通对象一般分配在TLAB上;
  • 如果对象较大,会被分配到Eden的其它位置;
  • 如果对象太大,新生代放不下,就会被分配到老年代

多大的对象会被直接放入老年代?

GC方法不同,对大对象的判定方式也不同:
  • CMS(Concurrent Mark Sweep):可通过JVM启动参数 PretensureSizeThreshold 设置大对象的判定标准
  • G1:达到Region大小的一半即为大对象。可通过JVM启动参数 G1HeapRegionSize 设置Region的大小

 

5. 方法区

  • 存放元数据:类结构信息、运行时常量池、字段、方法代码等
  • 所有线程共享方法区
  • Oracle JDK 8 已移除原方法区(永久代),用元数据区(Metaspace)代替
    • Intern 字符串缓存 和 静态变量 都移到了堆中
    • 注:“方法区”是逻辑概念,“永久代”和“元数据区”是具体设计实现的选择

5.1 运行时常量池

  • 方法区的一部分
  • 存放编译期生成的字面量、运行时决定的符号引用

6. 直接内存区

  • 位于 JVM内存之外

7. 其它内存区

  • JIT Compiler 对热点方法的编译结果存放在 Code Cache 中
  • 垃圾收集器(GC)运行在本地线程中

8. 设置内存区容量

【常见方式】可通过以下JVM启动参数设置相关容量

 
【Java核心-进阶】JVM 内存区域划分