深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)第二章知识点问答(21题)
1 运行时数据区域总览(名称 → 私有/共享 → 典型异常)
- 程序计数器(PC) → 线程私有 → (规范未定义 OOME);记录下一条将执行的字节码指令地址/偏移(执行
native
时可能无意义)。 - 虚拟机栈 → 线程私有 →
StackOverflowError
(栈深超限);OutOfMemoryError
(需要扩栈但内存不足);一帧=局部变量表/操作数栈/动态链接/返回地址。 - 本地方法栈 → 线程私有 → 同上两类异常;为
native
方法服务。 - 堆 → 共享 →
OutOfMemoryError: Java heap space
(或 GC overhead limit exceeded);存放对象实例/数组,GC 主要管理区域。 - 方法区(HotSpot: Metaspace) → 共享 →
OutOfMemoryError: Metaspace
;承载类元数据/运行时常量池/静态方法信息(JIT 代码在 Code Cache)。 - 运行时常量池 → 共享(属方法区一部分)→
OutOfMemoryError
(常伴随 constant pool 报文);存字面量/符号引用等。 - 直接内存(NIO DirectBuffer) → 进程级共享(堆外)→
OutOfMemoryError: Direct buffer memory
;ByteBuffer.allocateDirect()
/mmap 用于少拷贝 I/O。
2 栈错误对比:StackOverflowError
vs 栈相关 OutOfMemoryError
-
触发
SOE
:深递归/极大方法嵌套导致栈深超限(受-Xss
影响)。OOME(栈)
:创建线程失败(OS 线程/内存/进程限制)或单线程-Xss
过大导致总内存不足(常见报文unable to create native thread
)。
-
复现
SOE
:小栈-Xss256k
+ 递归方法死递归;OOME(栈)
:-Xss8m
+ 循环创建休眠线程直到失败。
-
排查
- 看异常栈&
-Xss
; jcmd <pid> Thread.print
看线程数/状态,核对 OS/容器限制(ulimit -u
)。
- 看异常栈&
3 四类 OOM 的触发条件与首条线索
- Java heap space:堆无法分配新对象(泄漏/短时分配暴涨/堆太小)→ 首看 GC 日志与 heap dump。
- GC overhead limit exceeded:大部分时间在 GC,回收极少(近似 98%/2%)→ 首看 GC 日志确认症状,根因仍指向堆紧张。
- Metaspace:类元数据空间耗尽(动态生类/类加载器泄漏)→ 先看 NMT 与 class loader stats。
- Direct buffer memory:直接内存超上限或未释放 → 查
-XX:MaxDirectMemorySize
、allocateDirect
使用点与 NMT 的NIO
类别。
4 如何可靠复现 Metaspace OOM(思路)
- 做法:用 ByteBuddy/CGLIB/ASM 不断生成新类;每批使用新 ClassLoader 并把 Loader 放入
static List
强引用防卸载;运行时加-XX:MaxMetaspaceSize=64m
。 - 第一条排查线索:
OOME: Metaspace
报错出现后,立刻jcmd <pid> VM.native_memory summary
与jcmd <pid> GC.class_stats / VM.class_loader_stats
。 - 防护:① 复用 Loader/卸载前清所有强引用(含 TCCL/缓存/ThreadLocal);② 设上限并监控 NMT 指标。
5 运行时常量池 vs 字符串常量池(StringTable)
-
位置/时机
- 运行时常量池:在方法区(JDK8+ 为 Metaspace);随类加载/链接建立。
- 字符串常量池:JDK7+ 在堆(JDK6 在 PermGen);JVM 启动即有,运行期通过字面量/
intern()
填充。
-
典型 OOM
- 常量池:
OOME: Metaspace
(或常量池相关文案)。 - 字符串池(堆):
OOME: Java heap space
(或 GC overhead)。
- 常量池:
-
示例(施压 StringTable)
while (true) UUID.randomUUID().toString().intern();
6 new
对象的关键流程(6 步)
- 类可用性检查:未加载/未初始化则触发 加载→链接(验证/准备/解析)→初始化。
- 分配:优先在 TLAB(线程本地)用指针碰撞;不够时到共享堆用 CAS/加锁。
- 清零:实例字段所在内存全部置 0(得默认值)。
- 对象头:写入 Mark Word(哈希/锁标志/偏向/年龄)与 Klass 指针(数组还写长度)。
- 执行
<init>
:按继承层级构造;引用写入触发写屏障维护卡表。 - 失败分支:分配失败→尝试 GC/扩堆;仍失败→
OOME: Java heap space
。
7 新生代/晋升/G1&ZGC
- Eden / S0 / S1:Young GC 时将 Eden+from 的幸存者复制到 to,对象年龄+1,然后 from/to 互换。
- 三条晋升路径:① 到达 MaxTenuringThreshold;② 动态年龄判定(某年龄及以上占 Survivor 超阈值→直晋升);③ 大对象直接进老年代/巨型区(Parallel 的
PretenureSizeThreshold
;G1 Humongous ≥ 半个 Region)。 - G1:Region 化 + 分代(Young/Mixed GC),复制晋升;Humongous 用一组连续 Region(通常计入 Old)。
- ZGC:早期不分代,用染色指针+读屏障并发重定位;JDK 21+ 可开启
-XX:+ZGenerational
引入分代,仍保持低停顿。
8 OOM ↔ 关键参数/限制配对
- Java heap space →
-Xmx/-Xms
(堆不足);先看 GC 日志/heap dump。 - GC overhead limit exceeded → 根因仍是
-Xmx
紧张(可临时-XX:-UseGCOverheadLimit
获取证据)。 - Metaspace →
-XX:MaxMetaspaceSize
(类元空间不足);看 NMT/类加载器。 - Direct buffer memory →
-XX:MaxDirectMemorySize
(直接内存上限);看 NMTNIO
。 - unable to create native thread → OS/容器线程&内存限制(亦受
-Xss
影响)。
9 Heap OOM 的最小闭环(三步)
- 留证:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/heap.hprof -Xlog:gc*:file=/var/gc.log
; 必要时jcmd <pid> GC.heap_dump /tmp/heap.hprof
。 - 定位:MAT 看 Leak Suspects / Dominator Tree / Path to GC Roots;结合 GC 日志判断“泄漏”vs“分配暴涨”。
- 处置:短期限流/减并发/小幅调 Xmx、收紧缓存;长期修引用链/类加载器/缓存策略并加 JFR/GC/NMT 监控。
10(可选延伸)Safepoint 与“进入慢”
- 定义:在 GC/类卸载/去优化等全局操作前,JVM 要求所有 Java 线程停在带轮询的安全点,以便一致地枚举根并扫描。
- 进入慢的常见原因:① 紧凑长循环/大方法缺少计数 Safepoint;② JNI Critical/长时间阻塞 I/O导致线程久不回到轮询点。
- 对策:开
-Xlog:safepoint
或-XX:+PrintSafepointStatistics
定位;拆循环/开启-XX:+UseCountedLoopSafepoints
、缩短 JNI 临界区/改为可中断阻塞。
11 String
不可变性的利与弊 & 高拼接实践
- 利:可放入常量池、可缓存
hashCode
、天然线程安全,适合做 Map Key/跨线程共享。 - 弊:拼接/替换会生成新对象,循环中易分配放大→ GC 压力。
- 实践:循环中用
StringBuilder
(预估容量);批量拼接用StringJoiner/Collectors.joining
;流式写入StringWriter/BufferedWriter
;慎用intern()
处理动态值。
12 对象内存布局 & 访问方式
- 布局:对象头(Mark Word + Klass 指针;数组还有长度)+ 实例数据(父类→子类,遵循对齐)+ 对齐填充(HotSpot 默认8 字节对齐)。
- Mark Word 含义:哈希、锁状态/偏向信息、GC 年龄等;64 位下开 Compressed Oops/Class Pointers 以32 位编码引用,降低内存占用/提高缓存友好。
- 访问方式:句柄(引用→句柄表项→{对象地址, 类型元数据地址},移动对象只改句柄,多一次间接);直接指针(HotSpot 采用,更快,移动需更新引用)。
13 ThreadLocal 为何“看起来泄漏” & 防范
- 原因:
ThreadLocalMap
的 key(ThreadLocal)是弱引用、value 是强引用;当 key 被 GC 回收后,value 仍被线程强引用着,线程池中线程长寿导致 value 长期不清。 - 防范:①
try{ set } finally{ remove(); }
;② 线程池统一清理(装饰afterExecute
)并避免放大对象;必要时禁用/谨慎用InheritableThreadLocal
。
14 用 NMT 排查 Metaspace/Direct Memory
-
启动:
-XX:NativeMemoryTracking=summary
(或detail
)可选-XX:+PrintNMTStatistics
。 -
在线:
jcmd <pid> VM.native_memory baseline jcmd <pid> VM.native_memory summary.scale=MB jcmd <pid> VM.native_memory detail.diff
关注:Class(=Metaspace)、NIO(=Direct)、Thread/Code/Internal/Symbol。
-
第一动作:Metaspace 涨→看 class_loader_stats/动态生类点;Direct 涨→核对 MaxDirectMemorySize,定位
allocateDirect
持有链(Netty 可开 leakDetector)。
15 直接内存 vs 堆(实务四句)
- 管理者:堆→GC;直接内存→
DirectByteBuffer
的 Cleaner/Unsafe 释放,最终由 OS 回收。 - 场景:堆→业务对象/集合;直接内存→NIO/Netty/mmap 减少拷贝。
- 参数:堆→
-Xms/-Xmx
;直接内存→-XX:MaxDirectMemorySize
。 - 首证据:堆→
OOME: Java heap space
+ GC 日志/heap dump;直接内存→OOME: Direct buffer memory
+ NMTNIO
。
16 PermGen → Metaspace(JDK8 迁移)
- 差异:**PermGen(JDK8 前,堆内)**由
-XX:PermSize/MaxPermSize
控制;**Metaspace(JDK8+ 本地内存)**默认随系统增长,用-XX:MaxMetaspaceSize
控制。 - 典型 OOM:
OOME: PermGen space
(旧) /OOME: Metaspace
(新)。 - 遇到 Metaspace OOM 首动:看 NMT 与 class loader stats,确认是否类加载器泄漏/动态生类过多。
17 任意 OOM 的“最小证据包”(5 条)
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/heap.hprof
—— OOM 自动落地 dump-Xlog:gc*:file=/var/gc.log:time,uptime,tags
—— 持久化 GC 证据jcmd <pid> GC.heap_dump /tmp/heap.hprof
—— 在线抓堆jcmd <pid> VM.native_memory summary.scale=MB
—— 看本地内存分类jcmd <pid> Thread.print
—— 线程快照(线程 OOME/卡死)
18 用 MAT 看 dump 的三步法
- Leak Suspects:看是否存在可疑泄漏链/最大保留大小持有者。
- Dominator Tree / Top Consumers:按Retained Size 找“内存大户”(集合/缓存/字节数组)。
- Histogram + Path to GC Roots:锁定异常类型后,沿强引用链/静态字段/ThreadLocal 找根因。
19 TLAB(线程本地分配缓冲)
- 作用/好处:线程私有指针碰撞快速分配,无锁/无 CAS;提升缓存局部性、降低竞争。
- 代价/局限:内部碎片(零头浪费)、申请/回收 TLAB 的额外开销。
- 参数/观察:
-XX:+/-UseTLAB
;-Xlog:gc+tlab=info
;-XX:+ResizeTLAB
、-XX:TLABSize
。 - 回退场景:对象太大或当前 TLAB 剩余不足→到共享 Eden 分配/申请新 TLAB;G1 的巨型对象可能绕过新生代。
20 方法区/常量池/字符串池的位置变迁(JDK6 → 7 → 8)
- JDK6:方法区=PermGen(堆内);运行时常量池/字符串常量池在 PermGen;报错
OOME: PermGen space
;调参-XX:MaxPermSize
。 - JDK7:仍有 PermGen,但字符串常量池迁至堆;堆压力增大可能导致 heap OOM。
- JDK8:移除 PermGen,用 Metaspace(本地内存);运行时常量池随类元数据在 Metaspace,字符串常量池在堆;报错
OOME: Metaspace
;调参-XX:MaxMetaspaceSize
。
21 遇到 OOM 的标准操作流程(6 步)
- 启动就留证:开启 HeapDumpOnOOME 与 GC 日志。
- 线上取证:
jcmd ... GC.heap_dump
/VM.native_memory
/Thread.print
,必要时开 JFR 5–10 分钟。 - 快速研判:结合 OOME 文案/NMT 类别判断 heap / metaspace / direct / threads。
- 离线定位:MAT 看 Leak Suspects → Dominator Tree → Path to GC Roots;若是 metaspace/direct,看 class loader/NIO。
- 短期止血:限流降并发/缩小批量;谨慎小幅调
-Xmx
;收紧缓存;保留证据后重启。 - 长期治理:修复引用链/类加载器/缓存策略;加 JFR/GC/NMT 周期快照与告警。