JVM 题的典型答法是背名词。更好的方式是把「对象放在哪、什么时候回收、回收时怎么保证正确、出问题怎么取证」这条链路讲通。以下以 HotSpot、JDK 17/21 为语境。
1. JVM 运行时数据区怎么划分?
按线程私有和共享划分。私有部分包括程序计数器、虚拟机栈和本地方法栈:程序计数器记录当前字节码位置,是唯一不会 OOM 的区域;虚拟机栈存放栈帧,包含局部变量表、操作数栈和返回地址,递归过深会抛 StackOverflowError。
共享部分是堆和方法区。堆存放对象实例,是 GC 的主战场;方法区在 HotSpot 中由元空间实现,存放类元数据、运行时常量池等,JDK 8 起从永久代迁到本地内存。
此外还有直接内存,它不属于运行时数据区,但 NIO 的 DirectByteBuffer 会占用,受 -XX:MaxDirectMemorySize 影响,超限时报 Direct buffer memory。字符串常量池在 JDK 7 已移到堆中。
2. 一个对象从创建到回收经历了什么?
new 之后先检查类是否已加载,然后在堆上分配内存。分配通常用指针碰撞或空闲列表,多线程下通过 TLAB 减少竞争;分配后先把实例字段清零,再设置对象头,最后执行构造器。
对象布局包括 Mark Word、类型指针、实例数据和对齐填充。Mark Word 存哈希、GC 年龄和锁状态;开启指针压缩后引用占 4 字节,堆小于约 32GB 时默认开启。
新对象一般进入 Eden。Eden 满时触发 Young GC,存活对象复制到 Survivor 并增加年龄,达到阈值后晋升老年代;大对象可能直接进老年代。老年代压力过大时触发 Full GC 或混合回收。
3. 如何判断对象可以回收?三色标记解决了什么问题?
主流 JVM 用可达性分析:从 GC Roots 出发遍历引用链,不可达的对象才可回收。GC Roots 包括栈上的局部变量、静态字段、JNI 引用、活动线程和同步监视器等。
引用强度分强、软、弱、虚:软引用在内存不足时回收,弱引用下次 GC 就回收,虚引用只用于回收通知。finalize 已废弃,不应作为资源释放手段。
并发标记时用户线程仍在改引用,三色标记可能漏标:黑对象新增了指向白对象的引用,或灰对象到白对象的引用被删除。CMS 用增量更新记录新增引用,G1 用 SATB 记录删除前的快照,两者都靠写屏障实现。
4. G1 和 CMS、ZGC 有什么区别?
CMS 以低停顿为目标,但存在碎片和并发模式失败,已在 JDK 14 移除。G1 从 JDK 9 起是默认收集器,把堆划分成大小相等的 Region,年轻代和老年代是逻辑集合而非物理连续区域,可以按回收价值优先选择 Region。
G1 有 Young GC、Mixed GC 和必要的 Full GC。-XX:MaxGCPauseMillis 是软目标,不是硬保证;大对象(超过 Region 一半)会进入 Humongous 区域。跨 Region 引用靠记忆集和卡表维护。
ZGC 用染色指针和读屏障实现并发标记与并发整理,停顿与堆大小基本无关,JDK 21 引入分代 ZGC。选型不应只看「谁更新」:吞吐敏感先看 Parallel,延迟敏感看 G1 或 ZGC,还要结合堆大小、CPU 和 JDK 版本压测。
5. 类加载过程和双亲委派模型?
类的生命周期是加载、链接(验证、准备、解析)、初始化、使用、卸载。准备阶段给静态变量分配内存并设零值,static final 常量在编译期已写入常量池;初始化阶段才执行 <clinit>,包括静态赋值和静态块。
双亲委派指加载请求先交给父加载器,父加载器无法完成才自己加载。它保证核心类库不被同名类替换,也避免重复加载。启动类加载器、平台类加载器、应用类加载器是三层常见结构。
委派可以被打破:SPI 通过线程上下文类加载器加载实现类;Tomcat 为每个 Web 应用维护独立加载器,实现应用隔离;OSGi 则是网状委派。注意「被动引用」不会触发初始化,例如通过子类访问父类静态字段、访问编译期常量。
6. 有哪些 OOM?线上怎么排查?
常见类型:堆空间不足、GC 开销超限、元空间不足(动态生成类过多)、直接内存不足、无法创建本地线程(线程数或栈过大)、数组长度超限。
排查顺序:先确认是哪种 OOM,再看是否有 -XX:+HeapDumpOnOutOfMemoryError 留下的堆转储;用 jcmd <pid> GC.heap_info 看堆使用,用 jmap -histo:live 看对象分布,必要时导出 hprof 用 MAT 找支配树和 GC Root 路径。
还要区分「内存泄漏」和「内存不够」:前者是对象该回收却仍被引用,需要定位持有链;后者可能是缓存无上限、批量查询过大或堆设置不合理。直接内存和线程数问题不会体现在堆转储里,要看 NMT 和系统级指标。
7. JIT 编译和逃逸分析对性能有什么影响?
HotSpot 采用解释器加分层编译:热点方法先由 C1 快速编译,执行更久后交给 C2 做激进优化。方法内联、循环展开、常量折叠、去虚拟化都会显著影响峰值性能,所以短跑基准测试往往失真。
逃逸分析判断对象是否逃出方法或线程。不逃逸的对象可以标量替换,把字段拆成局部变量,从而避免堆分配;锁对象不逃逸时可以做锁消除。它不等于「对象一定分配在栈上」,HotSpot 主要做的是标量替换。
实际影响:压测前要预热,避免把解释执行阶段算进吞吐;不要用过于复杂的继承层次阻碍内联;-Xint 只用于排查,不适合生产。
8. JVM 参数怎么调?GC 日志怎么看?
先定堆:-Xms 和 -Xmx 设成相同值,避免运行期反复扩缩;容器环境用 -XX:MaxRAMPercentage 按比例设置,并给堆外内存和元空间留余量。再设 -XX:MaxMetaspaceSize、开启 OOM 堆转储。
GC 参数从业务目标出发:延迟优先可设 -XX:MaxGCPauseMillis,但不要设得过小导致 Young 区被压缩、回收更频繁。别照抄网上的「万能参数」,不同 JDK 的默认收集器和日志参数都不一样。
JDK 9 起用统一日志:-Xlog:gc*:file=gc.log:time,uptime,level,tags。重点看停顿时间、回收前后占用、晋升量和 Full GC 频率。单次 Young GC 变长可能是存活对象变多,频繁 Full GC 往往要看是不是晋升过快或存在泄漏。
易错点与自测
- 永久代已在 JDK 8 移除,元空间在本地内存,不等同于「没有上限」。
- G1 的 Region 不等于物理连续的新生代/老年代,不要把分代模型硬套到内存布局上。
- 堆 dump 只能解释堆内问题,直接内存、线程栈、元空间要另找证据。
自测:① 说明 CMS 与 G1 处理漏标的方式差异;② 给出一组容器内 4C8G 服务的 JVM 参数并解释理由;③ 描述一次「堆内存持续上涨」的完整取证步骤。
参考资料
- Java SE 21 GC 调优指南 — 收集器选择、G1 与 ZGC 的停顿目标
- JVM 规范 SE 21 — 运行时数据区与类加载的规范定义
- jcmd 手册(Java 21) —
Thread.print、GC.heap_info、GC.heap_dump、JFR.start的语法与影响 - JEP 439:Generational ZGC — 分代 ZGC 的设计目标