Skip to content

概念卡片:JVM 内存区域与对象模型

一句话机制

JVM 把内存划成「线程私有的程序计数器/虚拟机栈/本地方法栈」+「线程共享的堆/方法区」五大运行时数据区;对象在堆上按「对象头 + 实例数据 + 对齐填充」三段式布局,通过栈上的 reference 访问。 内存问题(OOM/内存泄漏)排查的前提,是搞清楚每块区域存什么、何时 OOM。

运行时数据区(核心不变量)

区域线程职责关键点
程序计数器私有记录当前字节码指令地址唯一不 OOM;Native 方法时为 Undefined
虚拟机栈私有方法调用的栈帧StackOverflowError(深度超限)/ OOM(扩不动)
本地方法栈私有native 方法同上;C 实现
共享对象实例 + 数组GC 主战场;-Xms/-Xmx 调控
方法区共享类型信息/常量/静态变量/JIT 代码JDK8 后 = 元空间(本地内存)

栈帧五件套

每次方法调用压入一个栈帧,含:局部变量表(存方法参数+局部变量,基本单位是 Slot)、操作数栈(字节码执行的中转站)、动态链接(符号引用→直接引用)、方法返回地址、附加信息。

Slot 不变量:32 位以内类型占 1 槽,long/double 占 2 槽;实例方法/构造方法的 this 固定占 index 0;槽位在变量出了作用域后可被复用(省内存)。

java
public void test2(Date dateP, String name2) {
    double weight = 130.5;   // long/double 占两个 slot
    char gender = '男';       // byte/short/char/boolean 一律按 int 存
}

堆的分代结构

堆分新生代(Eden + S0 + S1,默认 8:1:1)+ 老年代。对象默认在 Eden 分配,每次 Minor GC 存活则年龄 +1 并在 S0/S1 间「复制-交换」(谁空谁是 to),年龄达 15(-XX:MaxTenuringThreshold)晋升老年代。

不变量:分代的唯一理由是优化 GC 性能。不分代也完全能工作,但每次 GC 都要全堆扫描;分代后只回收「朝生夕死」的新生代,能腾出大量空间。

对象分配规则

  1. 优先 Eden 分配
  2. 大对象直接进老年代(避免在 Eden/Survivor 间反复复制)
  3. 长期存活对象进老年代
  4. 动态年龄判断:Survivor 中同年龄对象总大小超过 Survivor 一半,该年龄及以上直接晋升
  5. 空间分配担保(-XX:HandlePromotionFailure

三种 GC

类型回收区域触发特点
Minor GC新生代Eden 满(Survivor 满不触发)频繁、快,STW
Major GC老年代老年代不足慢 10 倍以上,通常伴随 Minor GC
Full GC整堆 + 方法区5 种情况(见下)尽量规避

Full GC 触发:System.gc()、老年代不足、方法区不足、Minor GC 后晋升均值 > 老年代可用、向 Survivor 复制时对象 > To 区且老年代放不下。

对象创建与布局

创建过程(字节码角度)

new 指令 → 检查类是否加载 → NEW(分配内存)→ DUP(栈顶复制引用)→ INVOKESPECIAL(调用 <init> 构造)。内存分配方式取决于堆是否规整:指针碰撞(带 compact 的收集器,如 Serial/ParNew)vs 空闲列表(CMS 等非整理收集器)。

三段式布局

对象 = 对象头(Mark Word + 类型指针) + 实例数据 + 对齐填充
  • Mark Word:存 hashCode/分代年龄/锁标志位/偏向线程 ID,随锁状态动态复用空间
  • 类型指针:指向类元数据,确定对象是哪个类的实例
  • 对齐填充:保证对象大小是 8 字节整数倍(HotSpot 要求对象起始地址 8 字节对齐)

对象访问

  • 句柄访问:reference → 句柄池 → 实例数据/类型数据(对象移动只改句柄,reference 稳定)
  • 直接指针:reference → 对象(少一次间接访问,更快;HotSpot 采用

TLAB:线程本地分配缓冲

堆是共享的,多线程并发分配内存需加锁 → 慢。TLAB 给每个线程在 Eden 里划一块私有缓冲区(默认占 Eden 的 1%),线程先在自家 TLAB 分配,失败才走「CAS + 失败重试」或加锁到 Eden 全局分配。这就是对象分配的「快速分配策略」。

常见误解(避坑)

  1. ❌ "栈里存对象"。→ 栈存的是引用(reference),对象本身在堆;只有逃逸分析优化后,未逃逸对象才可能栈上分配/标量替换。
  2. ❌ "方法区会被频繁 GC"。→ 方法区(元空间)回收很少,Full GC 才处理;这正是 JDK7 把字符串常量池移出永久代的原因(字符串太多、永久代 GC 效率低)。
  3. ❌ "Minor GC 时 Survivor 满也会触发 GC"。→ Minor GC 只看 Eden 满,Survivor 满不触发。
  4. ❌ "直接内存受 -Xmx 限制"。→ 直接内存是堆外的本地内存(NIO DirectByteBuffer),不受堆大小限制,但受物理内存+寻址空间限制,配置时容易被忽略导致 OOM。

关联

最近更新