JVM篇1:java的内存结构 + 对象分配理解

JVM 内存结构 + 对象分配详解(HotSpot 虚拟机,JDK 8 ~ 21+ 主流版本)

下面内容基于 HotSpot JVM(Oracle/OpenJDK 默认实现),2025–2026 年主流生产环境基本没有大变化(PermGen 已彻底移除,Metaspace 仍是常态)。

一、JVM 运行时数据区整体划分(线程共享 vs 线程私有)

JVM 内存主要分为以下几个逻辑区域:

区域名称是否线程共享生命周期主要存储内容可能抛出的 OOM 异常是否会 GC
程序计数器 (PC Register)私有线程生命周期当前线程执行的字节码指令地址(分支、循环、异常跳转等)几乎不会 OOM
Java 虚拟机栈 (VM Stack)私有线程生命周期栈帧(局部变量表、操作数栈、动态链接、方法出口等)StackOverflowError / OOM
本地方法栈 (Native Method Stack)私有线程生命周期native 方法(JNI 调用 C/C++)的栈帧StackOverflowError / OOM
Java 堆 (Heap)共享JVM 启动 ~ 关闭几乎所有对象实例、数组OutOfMemoryError: Java heap space是(重点 GC 区域)
元空间 (Metaspace)共享JVM 启动 ~ 关闭类元数据、方法字节码、常量池、符号引用、注解等OutOfMemoryError: Metaspace是(Full GC 时可能回收)
直接内存 (Direct Memory)NIO ByteBuffer.allocateDirect() 分配的 off-heap 内存OutOfMemoryError: Direct buffer memory否(手动或 GC 间接回收)

最核心一句话记忆

  • 线程私有:程序计数器 + 虚拟机栈 + 本地方法栈(每个线程一份,互不干扰)
  • 线程共享:堆 + 元空间(所有线程可见,GC 主要发生在这里)

二、HotSpot JVM 堆内存详细布局(分代 + G1 时代主流视图)

现代 HotSpot(JDK 8+)默认使用 分代思想,但具体收集器不同,物理布局有差异。

1. 最经典的分代布局(Parallel / CMS 时代常见)

Heap
├── Young Generation(新生代)          ≈ 1/3 heap
│   ├── Eden(伊甸园)                 ≈ 8/10 young
│   └── Survivor(幸存者区)×2         From / To 各 ≈ 1/10 young
└── Old Generation(老年代 / 养老区)   ≈ 2/3 heap
  • Eden:几乎所有 new 对象最先在这里分配
  • Survivor:经过 Minor GC 后仍存活的对象进入这里(复制算法),默认比例 Eden:From:To = 8:1:1
  • Old:经历多次 Minor GC 仍存活(年龄阈值默认 15,可调 -XX:MaxTenuringThreshold)或大对象直接进入

2. G1 收集器时代(JDK 9+ 默认,2025–2026 生产主流)

G1 已经取消严格的连续 Young/Old 区域,而是把整个堆切成很多个 Region(默认 2048 个,可调):

  • 每个 Region 大小固定(1MB ~ 32MB,堆越大 Region 越大)
  • 同一个时刻,Region 可以动态扮演以下角色:
  • Eden Region
  • Survivor Region
  • Old Region
  • Humongous Region(超大对象 > 50% Region 大小)
  • Young GC 只回收 Eden + Survivor Region
  • Mixed GC 同时回收部分 Old Region

G1 内存布局示意(逻辑分代,物理碎片化):

Heap → 很多个 Region(大小相等)
   ┌───────────────┐
   │ Eden Regions  │
   ├───────────────┤
   │ Survivor Regs │
   ├───────────────┤
   │   Old Regions │
   ├───────────────┤
   │ Humongous Reg │ ← 大对象专用
   └───────────────┘

三、Java 对象创建 & 内存分配全过程(面试高频)

new 对象时 JVM 做了什么?(8 个步骤)

  1. 类加载检查
    遇到 new 指令 → 先检查常量池中是否有该类的符号引用 → 如果没有则触发类加载(加载 → 链接 → 初始化)
  2. 分配内存(核心步骤)
    在堆中划出一块确定大小的内存给对象(对象大小在类加载后已知)。 分配方式两种(取决于堆是否规整): 方式 适用收集器 堆是否规整 原理 并发安全解决方案 指针碰撞 Serial / ParNew 是 指针向空闲端移动 size 距离 TLAB(线程本地分配缓冲) 空闲列表 CMS / G1(部分情况) 否 从空闲列表中找一块足够大的空间 CAS + 失败重试 TLAB(Thread Local Allocation Buffer) 是 HotSpot 解决并发分配的优化:
  • 每个线程在 Eden 预先申请一块私有缓冲区
  • 线程内分配直接在 TLAB 上指针碰撞(无锁)
  • TLAB 用完再去 Eden 申请新的一块(仍需同步)
  1. 初始化零值
    分配的内存空间(对象头除外)全部置为零值(保证字段不赋初值也能用)
  2. 设置对象头
  • Mark Word(hashCode、GC 分代年龄、锁状态等)
  • Klass Pointer(指向类元数据)
  • (数组才有)数组长度
  1. 执行 方法(构造器)
    按照代码顺序执行父类构造 → 成员变量显式赋值 → 构造代码块 → 构造方法体

四、对象分配的“特殊规则”(面试加分项)

  1. 大对象直接进老年代
    -XX:PretenureSizeThreshold=(默认 0)
    大于这个值的对象直接分配到老年代(避免在 Survivor 来回复制)
  2. 动态年龄判定(Survivor 区晋升老年代规则)
    如果 Survivor 空间中相同年龄的所有对象大小总和 > Survivor 空间的一半(默认 50%,-XX:TargetSurvivorRatio),年龄 >= 该年龄的对象直接晋升老年代
  3. 长期存活的对象进入老年代
    默认经历 15 次 Minor GC 后晋升(-XX:MaxTenuringThreshold=15)
  4. 空间分配担保
    Minor GC 前,老年代最大可用连续空间 < 新生代所有对象总大小 → 提前 Full GC

五、快速记忆口诀(面试背诵版)

内存结构口诀
“程栈本堆元,私私私共共”
程序计数器、虚拟机栈、本地方法栈(私有)
堆、元空间(共享)

堆内布局口诀(分代时代):
“新生伊甸幸存俩,老年养老养老家”

对象分配口诀
“先查类加载 → 指针碰撞或空闲列表 → TLAB 加速 → 零值填充 → 对象头 → 构造执行”

晋升老年代三板斧
大 → 直接老
老 → 年龄阈值(默认 15)
多 → 动态年龄判定(同龄 > Survivor/2)

希望这篇能让你对 JVM 内存结构 + 对象分配有清晰的整体认知。
下一期想看哪个方向?

  • GC 算法详解(Serial / Parallel / CMS / G1 / ZGC / Shenandoah)
  • OOM 常见场景 & 定位思路
  • 常用 JVM 参数实战调优
  • 对象头 MarkWord 详细位图

随时告诉我!

文章已创建 3958

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

相关文章

开始在上面输入您的搜索词,然后按回车进行搜索。按ESC取消。

返回顶部