JVM 内存结构与垃圾回收
运行时数据区
| 区域 | 线程共享 | 存什么 | OOM |
|---|---|---|---|
| 堆 | 是 | 对象实例、数组 | Java heap space |
| 方法区 / 元空间 | 是 | 类元数据、常量、静态变量 | Metaspace OOM |
| 虚拟机栈 | 否 | 栈帧、局部变量、引用 | StackOverflow / 无法新建线程 |
| 本地方法栈 | 否 | Native 方法 | 同上 |
| 程序计数器 | 否 | 当前字节码行号 | 不会 OOM |
┌─────────────────────────────────────┐
│ 堆 Heap(Young + Old) │ ← 对象
├─────────────────────────────────────┤
│ 元空间 Metaspace(JDK8+,本地内存) │ ← 类信息
└─────────────────────────────────────┘
每线程:虚拟机栈 + 程序计数器
堆 vs 栈:堆存对象;栈存局部变量表、操作数栈、方法出口(引用指向堆中对象)。
分代模型
| 区域 | 说明 |
|---|---|
| Eden | 新对象优先分配 |
| Survivor S0/S1 | Minor GC 存活对象复制,年龄 +1 |
| Old | 长期存活、大对象(直接进老年代可配置) |
Minor GC:新生代;频繁、较快。
Full GC / Major GC:常含老年代;停顿长,应尽量减少。
如何判断可回收
可达性分析:从 GC Roots(栈中引用、静态字段、常量、JNI 等)出发,不可达对象可回收。
常见 GC 算法
| 算法 | 思路 | 缺点 |
|---|---|---|
| 标记-清除 | 标记存活,清除其余 | 碎片 |
| 标记-复制 | 存活复制到另一块 | 空间浪费(新生代常用) |
| 标记-整理 | 存活移到一端 | 移动成本 |
收集器(常问)
| 收集器 | 特点 |
|---|---|
| Serial / Parallel | 新生代单线程 / 多线程 |
| CMS | 老年代并发标记清除,低停顿,已逐步被 G1 替代 |
| G1 | 分区 Region,可设 -XX:MaxGCPauseMillis 目标停顿 |
| ZGC / Shenandoah | 超低延迟,染色指针 / 并发整理 |
类加载(简)
加载 → 验证 → 准备 → 解析 → 初始化。详见 类加载与双亲委派。
OOM 排查
- 看日志:
Java heap space/Metaspace/GC overhead limit -XX:+HeapDumpOnOutOfMemoryErrorjmap -dump+ MAT 看 dominator tree、泄漏引用链- 常见原因:缓存无上限、ThreadLocal 未 remove、大集合、元空间类加载过多
常见面试题
Q:对象一定在堆上吗?
A:多数在堆;逃逸分析后可能栈上分配或标量替换(JIT)。
Q:强软弱虚引用?
A:强默认;软内存不足回收;弱 GC 即回收(ThreadLocalMap key);虚用于跟踪回收时机(PhantomReference)。
Q:Minor GC 会触发 Full GC 吗?
A:可能——老年代空间不足、担保失败、System.gc() 建议等。
Q:如何调优 GC?
A:先 metrics(停顿、吞吐);设堆大小;选 G1/ZGC;查泄漏而非盲目调参。