JVM基本知识(一):内存结构与对象管理
2026/5/17大约 7 分钟
JVM基本知识(一):内存结构与对象管理
导语:本系列是 JVM 的「知识地图」,不讲题、只搭骨架,帮你先把整体轮廓立起来。第一篇回答两个最基础的问题:JVM 把内存分成了哪几块?一个对象从
new到住进堆里,经历了什么? 看完再翻后面的面试题系列,会顺很多。
一、一张图看懂 JVM 内存
JVM 的内存可以粗暴地分成三类:线程私有、线程共享,以及一块不属于运行时数据区的直接内存。
看图要点:
- 为什么线程私有:程序计数器记录执行到哪条字节码、虚拟机栈保存方法调用链,这些都是「每个线程各有一份」的执行状态,因此必须私有;
- 程序计数器是唯一不会 OOM 的区域(执行 native 方法时值未定义,仅此而已);
- 虚拟机栈内部是栈帧:局部变量表(方法参数与局部变量)、操作数栈(计算的工作区)、动态链接、方法返回地址。栈太深报
StackOverflowError(如无限递归),栈太多(线程太多)报unable to create new native thread; - 堆是最大的一块,也是 GC 的主战场;方法区存类元信息、运行时常量池等,JDK 8 起由元空间实现,改用本机内存。
方法区的三代形态
这块区域是面试最爱挖的点,因为它「改过名、搬过家」:
| JDK 版本 | 方法区的实现 | 位置 | 相关参数 |
|---|---|---|---|
| JDK 6 及之前 | 永久代 PermGen | 堆内 | -XX:PermSize / -XX:MaxPermSize |
| JDK 7 | 永久代(部分搬迁) | 堆内 | 字符串常量池、静态变量已移到堆 |
| JDK 8 及之后 | 元空间 Metaspace | 本机内存 | -XX:MetaspaceSize / -XX:MaxMetaspaceSize |
记忆:「方法区」是规范里的逻辑区域,永久代/元空间都只是它的实现。所以「JDK 8 把方法区移到了本地内存」这种说法不严谨,准确说法是「JDK 8 用元空间替代永久代来实现方法区」。
二、堆为什么要分代
堆不是一整块,而是按对象「寿命」切成了新生代和老年代——因为绝大多数对象朝生夕死,把它们堆在一起回收效率最高。
看图要点:
- Eden 满就触发 Minor GC(也叫 Young GC):用复制算法把存活对象搬到一块空的 Survivor,其余整体清空,所以新生代回收又快又干净;
- 两个 Survivor 交替使用,同一时刻总有一块是空的,专门用来「装下一轮存活的」;
- 对象不是永远待在新生代的——每熬过一次 Minor GC 年龄就 +1,够老就晋升老年代;
- 分代只是 HotSpot 的实现选择,不是 JVM 规范的强制要求,这也为 G1 的 Region 化留出了空间。
三、对象是怎么被创建出来的
new 一行代码,JVM 背后要做五件事。关键在「分配内存」这一步——多线程同时分配,怎么保证不冲突?
看图要点:
- TLAB(Thread Local Allocation Buffer) 是 HotSpot 给每个线程在 Eden 里预留的一小块私有缓冲,对象优先在这里分配,完全不需要加锁,这是 JVM 分配性能的关键优化,默认开启;
- TLAB 用完 → 申请新的 TLAB;再不行 → 退回共享 Eden 用 CAS 争抢;最后还是不行 → 触发 Minor GC;
- 零值初始化保证了「字段没写也有默认值」(
int为 0、引用为null),<init>才按代码赋值; - 对象头是在构造方法之前设置的,这一点是 DCL 单例必须加
volatile的根因——引用可能先于字段初始化被其他线程看到。
四、对象在堆里长什么样
看图要点:
- Mark Word 是「一物多用」的样板:同一块 8 字节,随锁状态在「哈希码 + GC 年龄 + 偏向标志」「偏向线程 ID」「锁记录指针」「Monitor 指针」之间切换。它同时承担了 GC 和锁两件事,这也是并发篇里锁升级的物理基础;
- 类型指针指向方法区的类元数据(HotSpot 用该指针就能定位类信息),开启指针压缩时 4 字节;
- 数组对象会在对象头多出 4 字节的数组长度;
- 为什么按 8 字节对齐:一方面让对象引用可以用「地址 / 8」压缩到 32 位(即指针压缩,堆 32G 内默认开启),另一方面避免跨缓存行访问。
五、对象最终会「住」在哪儿
分配不是只有 Eden 一个选项,JVM 会按对象的大小和存活情况分流。
看图要点:
- 大对象直接进老年代:
-XX:PretenureSizeThreshold控制(仅 Serial / ParNew 生效)。G1 是另一套机制——超过 Region 一半的对象称为 Humongous,会占用连续的专门 Region; - 动态年龄判定:不必等年龄攒够 15 岁,只要 Survivor 中同龄对象总和超过一半,就可能提前晋升——这是「为什么对象晋升得比预期快」的常见答案;
- 空间分配担保:Minor GC 前,JVM 会检查老年代最大可用连续空间是否装得下新生代所有对象,装不下还要看是否大于历次晋升的平均大小,否则直接改做 Full GC;
- 实际分配位置由 JVM 决定:JIT 的逃逸分析可以把未逃逸的对象「打散」成标量(标量替换),根本不产生对象实例,这也是「对象一定分配在堆上吗」的标准答案。
六、一页速记
| 区域 | 私有/共享 | 存什么 | 异常 | 关键参数 |
|---|---|---|---|---|
| 程序计数器 | 私有 | 当前字节码指令地址 | 无 | — |
| 虚拟机栈 | 私有 | 栈帧(局部变量表、操作数栈) | StackOverflowError | -Xss |
| 本地方法栈 | 私有 | native 方法栈帧 | 同上(HotSpot 与虚拟机栈合并) | -Xss |
| 堆 | 共享 | 对象实例、数组 | OOM: Java heap space | -Xms / -Xmx / -XX:NewRatio |
| 方法区(元空间) | 共享 | 类元信息、运行时常量池 | OOM: Metaspace | -XX:MaxMetaspaceSize |
| 直接内存 | 不在规范内 | NIO 缓冲区 | OOM: Direct buffer memory | -XX:MaxDirectMemorySize |
下一篇:《JVM基本知识(二)》讲垃圾回收——怎么判断对象已死、三种回收算法、分代回收流程,以及收集器从 Serial 到 ZGC 的演进主线。
