JVM 内存模型和垃圾回收平时零零散散接触过——OOM、GC 停顿、堆栈这些词都听过,但"内存到底分几块、对象分配在哪、GC 怎么回收"一直没系统梳理。这篇把 JVM 最核心的两块讲清楚:运行时数据区 + 垃圾回收。
一、解决什么问题:对象放哪、垃圾怎么收
Java 的"自动内存管理"是它的招牌——不用像 C/C++ 那样手动 malloc/free。但自动不等于不用懂:内存溢出(OOM)怎么排查、GC 为什么卡顿、怎么调优,都得先搞清楚 JVM 内存怎么分的、垃圾怎么收的。
二、运行时数据区:内存分几块
JVM 把内存分成几块,各管各的:
| 区域 | 存什么 | 线程私有? | GC 管吗 |
|---|---|---|---|
| 堆(Heap) | 所有对象实例 | 共享 | 管(GC 主战场) |
| 虚拟机栈(Stack) | 方法调用、局部变量 | 私有 | 不管(方法结束自动弹出) |
| 方法区(元空间) | 类信息、常量、静态变量 | 共享 | 管(类的卸载) |
| 程序计数器 | 当前执行到哪一行 | 私有 | 不管 |
关键:new 出来的对象都在堆上,堆是 GC 的主战场;方法调用和局部变量在栈上,方法结束自动清理,不用 GC。
java
public void method() {
int local = 10; // 局部变量在栈上(方法结束自动回收)
Object obj = new Object(); // obj 引用在栈上,对象在堆上(靠 GC 回收)
}三、堆的分代:新生代 + 老年代
堆又分两代,基于一个经验假设——大部分对象"朝生夕死"(创建后很快就没用了):
堆
├── 新生代(Young) ← 新对象放这,大部分很快死
│ ├── Eden 区 ← 新对象先到 Eden
│ ├── Survivor 0(S0)
│ └── Survivor 1(S1)
└── 老年代(Old) ← 活得久的对象晋升到这- 新对象 → Eden
- Eden 满了触发 Minor GC → 活下来的对象进 Survivor(S0/S1 来回复制)
- 在 Survivor 来回几次还没死 → 晋升到老年代
- 老年代满了触发 Full GC(最慢,要尽量避免频繁发生)
四、垃圾回收算法
GC 的核心算法有三种,分代收集就是它们的组合:
| 算法 | 做法 | 缺点 | 用在哪 |
|---|---|---|---|
| 标记-清除 | 标记垃圾 → 清除 | 产生内存碎片 | 老年代基础 |
| 复制 | 把活对象复制到另一块,清空原块 | 浪费一半空间 | 新生代(对象死得快) |
| 标记-整理 | 标记垃圾 → 把活对象整理到一边 | 慢 | 老年代(解决碎片) |
为什么新生代用复制、老年代用整理:新生代对象大多"朝生夕死",活对象少,复制成本低;老年代对象存活率高,复制浪费空间,改用"整理"解决碎片。
五、类加载与双亲委派(一句话了解)
类加载分五步:加载 → 验证 → 准备 → 解析 → 初始化。核心机制是双亲委派:加载类时,先让父加载器加载,父加载不了才自己加载。
java
// 双亲委派:Application → Extension → Bootstrap(自下而上委派)
// 好处:保证核心类(如 java.lang.String)不会被篡改——永远由最顶层的 Bootstrap 加载小结
- 内存分几块:堆(对象+GC 主战场)、栈(方法调用)、方法区(类信息)、程序计数器
- 堆分两代:新生代(Eden+Survivor,对象朝生夕死)、老年代(长寿对象)
- GC 算法:标记-清除(碎片)、复制(新生代)、标记-整理(老年代)
- 类加载双亲委派:保证核心类不被篡改
想了解 JVM 之上并发是怎么保证线程安全的,看《Java 并发编程》;想了解对象怎么在堆上组织、HashMap 怎么存,看《Java 集合框架详解》。
