[{"data":1,"prerenderedAt":338},["ShallowReactive",2],{"\u002Fjava-interview\u002Fjvm":3,"java-interview-search:\u002Fjava-interview\u002Fjvm":293},{"id":4,"title":5,"body":6,"category":279,"cover":280,"date":281,"description":282,"extension":283,"group":284,"meta":285,"navigation":286,"order":287,"path":288,"seo":289,"sitemap":290,"stem":291,"__hash__":292},"javaInterview\u002Fjava-interview\u002Fjvm.md","JVM：内存结构、GC 与线上排查",{"type":7,"value":8,"toc":265},"minimark",[9,13,18,26,29,44,48,54,57,60,64,67,74,77,81,84,91,94,98,109,112,115,119,122,137,140,144,147,150,157,161,180,186,193,196,209,212,215],[10,11,12],"p",{},"JVM 题的典型答法是背名词。更好的方式是把「对象放在哪、什么时候回收、回收时怎么保证正确、出问题怎么取证」这条链路讲通。以下以 HotSpot、JDK 17\u002F21 为语境。",[14,15,17],"h2",{"id":16},"_1-jvm-运行时数据区怎么划分","1. JVM 运行时数据区怎么划分？",[10,19,20,21,25],{},"按线程私有和共享划分。私有部分包括程序计数器、虚拟机栈和本地方法栈：程序计数器记录当前字节码位置，是唯一不会 OOM 的区域；虚拟机栈存放栈帧，包含局部变量表、操作数栈和返回地址，递归过深会抛 ",[22,23,24],"code",{},"StackOverflowError","。",[10,27,28],{},"共享部分是堆和方法区。堆存放对象实例，是 GC 的主战场；方法区在 HotSpot 中由元空间实现，存放类元数据、运行时常量池等，JDK 8 起从永久代迁到本地内存。",[10,30,31,32,35,36,39,40,43],{},"此外还有直接内存，它不属于运行时数据区，但 NIO 的 ",[22,33,34],{},"DirectByteBuffer"," 会占用，受 ",[22,37,38],{},"-XX:MaxDirectMemorySize"," 影响，超限时报 ",[22,41,42],{},"Direct buffer memory","。字符串常量池在 JDK 7 已移到堆中。",[14,45,47],{"id":46},"_2-一个对象从创建到回收经历了什么","2. 一个对象从创建到回收经历了什么？",[10,49,50,53],{},[22,51,52],{},"new"," 之后先检查类是否已加载，然后在堆上分配内存。分配通常用指针碰撞或空闲列表，多线程下通过 TLAB 减少竞争；分配后先把实例字段清零，再设置对象头，最后执行构造器。",[10,55,56],{},"对象布局包括 Mark Word、类型指针、实例数据和对齐填充。Mark Word 存哈希、GC 年龄和锁状态；开启指针压缩后引用占 4 字节，堆小于约 32GB 时默认开启。",[10,58,59],{},"新对象一般进入 Eden。Eden 满时触发 Young GC，存活对象复制到 Survivor 并增加年龄，达到阈值后晋升老年代；大对象可能直接进老年代。老年代压力过大时触发 Full GC 或混合回收。",[14,61,63],{"id":62},"_3-如何判断对象可以回收三色标记解决了什么问题","3. 如何判断对象可以回收？三色标记解决了什么问题？",[10,65,66],{},"主流 JVM 用可达性分析：从 GC Roots 出发遍历引用链，不可达的对象才可回收。GC Roots 包括栈上的局部变量、静态字段、JNI 引用、活动线程和同步监视器等。",[10,68,69,70,73],{},"引用强度分强、软、弱、虚：软引用在内存不足时回收，弱引用下次 GC 就回收，虚引用只用于回收通知。",[22,71,72],{},"finalize"," 已废弃，不应作为资源释放手段。",[10,75,76],{},"并发标记时用户线程仍在改引用，三色标记可能漏标：黑对象新增了指向白对象的引用，或灰对象到白对象的引用被删除。CMS 用增量更新记录新增引用，G1 用 SATB 记录删除前的快照，两者都靠写屏障实现。",[14,78,80],{"id":79},"_4-g1-和-cmszgc-有什么区别","4. G1 和 CMS、ZGC 有什么区别？",[10,82,83],{},"CMS 以低停顿为目标，但存在碎片和并发模式失败，已在 JDK 14 移除。G1 从 JDK 9 起是默认收集器，把堆划分成大小相等的 Region，年轻代和老年代是逻辑集合而非物理连续区域，可以按回收价值优先选择 Region。",[10,85,86,87,90],{},"G1 有 Young GC、Mixed GC 和必要的 Full GC。",[22,88,89],{},"-XX:MaxGCPauseMillis"," 是软目标，不是硬保证；大对象（超过 Region 一半）会进入 Humongous 区域。跨 Region 引用靠记忆集和卡表维护。",[10,92,93],{},"ZGC 用染色指针和读屏障实现并发标记与并发整理，停顿与堆大小基本无关，JDK 21 引入分代 ZGC。选型不应只看「谁更新」：吞吐敏感先看 Parallel，延迟敏感看 G1 或 ZGC，还要结合堆大小、CPU 和 JDK 版本压测。",[14,95,97],{"id":96},"_5-类加载过程和双亲委派模型","5. 类加载过程和双亲委派模型？",[10,99,100,101,104,105,108],{},"类的生命周期是加载、链接（验证、准备、解析）、初始化、使用、卸载。准备阶段给静态变量分配内存并设零值，",[22,102,103],{},"static final"," 常量在编译期已写入常量池；初始化阶段才执行 ",[22,106,107],{},"\u003Cclinit>","，包括静态赋值和静态块。",[10,110,111],{},"双亲委派指加载请求先交给父加载器，父加载器无法完成才自己加载。它保证核心类库不被同名类替换，也避免重复加载。启动类加载器、平台类加载器、应用类加载器是三层常见结构。",[10,113,114],{},"委派可以被打破：SPI 通过线程上下文类加载器加载实现类；Tomcat 为每个 Web 应用维护独立加载器，实现应用隔离；OSGi 则是网状委派。注意「被动引用」不会触发初始化，例如通过子类访问父类静态字段、访问编译期常量。",[14,116,118],{"id":117},"_6-有哪些-oom线上怎么排查","6. 有哪些 OOM？线上怎么排查？",[10,120,121],{},"常见类型：堆空间不足、GC 开销超限、元空间不足（动态生成类过多）、直接内存不足、无法创建本地线程（线程数或栈过大）、数组长度超限。",[10,123,124,125,128,129,132,133,136],{},"排查顺序：先确认是哪种 OOM，再看是否有 ",[22,126,127],{},"-XX:+HeapDumpOnOutOfMemoryError"," 留下的堆转储；用 ",[22,130,131],{},"jcmd \u003Cpid> GC.heap_info"," 看堆使用，用 ",[22,134,135],{},"jmap -histo:live"," 看对象分布，必要时导出 hprof 用 MAT 找支配树和 GC Root 路径。",[10,138,139],{},"还要区分「内存泄漏」和「内存不够」：前者是对象该回收却仍被引用，需要定位持有链；后者可能是缓存无上限、批量查询过大或堆设置不合理。直接内存和线程数问题不会体现在堆转储里，要看 NMT 和系统级指标。",[14,141,143],{"id":142},"_7-jit-编译和逃逸分析对性能有什么影响","7. JIT 编译和逃逸分析对性能有什么影响？",[10,145,146],{},"HotSpot 采用解释器加分层编译：热点方法先由 C1 快速编译，执行更久后交给 C2 做激进优化。方法内联、循环展开、常量折叠、去虚拟化都会显著影响峰值性能，所以短跑基准测试往往失真。",[10,148,149],{},"逃逸分析判断对象是否逃出方法或线程。不逃逸的对象可以标量替换，把字段拆成局部变量，从而避免堆分配；锁对象不逃逸时可以做锁消除。它不等于「对象一定分配在栈上」，HotSpot 主要做的是标量替换。",[10,151,152,153,156],{},"实际影响：压测前要预热，避免把解释执行阶段算进吞吐；不要用过于复杂的继承层次阻碍内联；",[22,154,155],{},"-Xint"," 只用于排查，不适合生产。",[14,158,160],{"id":159},"_8-jvm-参数怎么调gc-日志怎么看","8. JVM 参数怎么调？GC 日志怎么看？",[10,162,163,164,167,168,171,172,175,176,179],{},"先定堆：",[22,165,166],{},"-Xms"," 和 ",[22,169,170],{},"-Xmx"," 设成相同值，避免运行期反复扩缩；容器环境用 ",[22,173,174],{},"-XX:MaxRAMPercentage"," 按比例设置，并给堆外内存和元空间留余量。再设 ",[22,177,178],{},"-XX:MaxMetaspaceSize","、开启 OOM 堆转储。",[10,181,182,183,185],{},"GC 参数从业务目标出发：延迟优先可设 ",[22,184,89],{},"，但不要设得过小导致 Young 区被压缩、回收更频繁。别照抄网上的「万能参数」，不同 JDK 的默认收集器和日志参数都不一样。",[10,187,188,189,192],{},"JDK 9 起用统一日志：",[22,190,191],{},"-Xlog:gc*:file=gc.log:time,uptime,level,tags","。重点看停顿时间、回收前后占用、晋升量和 Full GC 频率。单次 Young GC 变长可能是存活对象变多，频繁 Full GC 往往要看是不是晋升过快或存在泄漏。",[14,194,195],{"id":195},"易错点与自测",[197,198,199,203,206],"ul",{},[200,201,202],"li",{},"永久代已在 JDK 8 移除，元空间在本地内存，不等同于「没有上限」。",[200,204,205],{},"G1 的 Region 不等于物理连续的新生代\u002F老年代，不要把分代模型硬套到内存布局上。",[200,207,208],{},"堆 dump 只能解释堆内问题，直接内存、线程栈、元空间要另找证据。",[10,210,211],{},"自测：① 说明 CMS 与 G1 处理漏标的方式差异；② 给出一组容器内 4C8G 服务的 JVM 参数并解释理由；③ 描述一次「堆内存持续上涨」的完整取证步骤。",[14,213,214],{"id":214},"参考资料",[197,216,217,227,235,257],{},[200,218,219,226],{},[220,221,225],"a",{"href":222,"rel":223},"https:\u002F\u002Fdocs.oracle.com\u002Fen\u002Fjava\u002Fjavase\u002F21\u002Fgctuning\u002F",[224],"nofollow","Java SE 21 GC 调优指南"," — 收集器选择、G1 与 ZGC 的停顿目标",[200,228,229,234],{},[220,230,233],{"href":231,"rel":232},"https:\u002F\u002Fdocs.oracle.com\u002Fjavase\u002Fspecs\u002Fjvms\u002Fse21\u002Fhtml\u002Findex.html",[224],"JVM 规范 SE 21"," — 运行时数据区与类加载的规范定义",[200,236,237,242,243,246,247,246,250,246,253,256],{},[220,238,241],{"href":239,"rel":240},"https:\u002F\u002Fdocs.oracle.com\u002Fen\u002Fjava\u002Fjavase\u002F21\u002Fdocs\u002Fspecs\u002Fman\u002Fjcmd.html",[224],"jcmd 手册（Java 21）"," — ",[22,244,245],{},"Thread.print","、",[22,248,249],{},"GC.heap_info",[22,251,252],{},"GC.heap_dump",[22,254,255],{},"JFR.start"," 的语法与影响",[200,258,259,264],{},[220,260,263],{"href":261,"rel":262},"https:\u002F\u002Fopenjdk.org\u002Fjeps\u002F439",[224],"JEP 439：Generational ZGC"," — 分代 ZGC 的设计目标",{"title":266,"searchDepth":267,"depth":267,"links":268},"",2,[269,270,271,272,273,274,275,276,277,278],{"id":16,"depth":267,"text":17},{"id":46,"depth":267,"text":47},{"id":62,"depth":267,"text":63},{"id":79,"depth":267,"text":80},{"id":96,"depth":267,"text":97},{"id":117,"depth":267,"text":118},{"id":142,"depth":267,"text":143},{"id":159,"depth":267,"text":160},{"id":195,"depth":267,"text":195},{"id":214,"depth":267,"text":214},"java-interview",null,"2026-10-24","从运行时数据区、对象布局到三色标记和 G1\u002FZGC 选型，串起 JVM 面试题与 OOM、GC 日志的排查方法","md","Java 核心",{},true,"50","\u002Fjava-interview\u002Fjvm",{"title":5,"description":282},{"loc":288},"java-interview\u002Fjvm","dOzS-nwJvTzUqBc3HCiI7ePHVkuafCLBfjYGjvwucN4",[294,298,302,306,310,314,318,322,326,330,334],{"id":288,"title":5,"titles":295,"content":296,"level":297},[],"从运行时数据区、对象布局到三色标记和 G1\u002FZGC 选型，串起 JVM 面试题与 OOM、GC 日志的排查方法 JVM 题的典型答法是背名词。更好的方式是把「对象放在哪、什么时候回收、回收时怎么保证正确、出问题怎么取证」这条链路讲通。以下以 HotSpot、JDK 17\u002F21 为语境。",1,{"id":299,"title":17,"titles":300,"content":301,"level":267},"\u002Fjava-interview\u002Fjvm#_1-jvm-运行时数据区怎么划分",[5],"按线程私有和共享划分。私有部分包括程序计数器、虚拟机栈和本地方法栈：程序计数器记录当前字节码位置，是唯一不会 OOM 的区域；虚拟机栈存放栈帧，包含局部变量表、操作数栈和返回地址，递归过深会抛 StackOverflowError。 共享部分是堆和方法区。堆存放对象实例，是 GC 的主战场；方法区在 HotSpot 中由元空间实现，存放类元数据、运行时常量池等，JDK 8 起从永久代迁到本地内存。 此外还有直接内存，它不属于运行时数据区，但 NIO 的 DirectByteBuffer 会占用，受 -XX:MaxDirectMemorySize 影响，超限时报 Direct buffer memory。字符串常量池在 JDK 7 已移到堆中。",{"id":303,"title":47,"titles":304,"content":305,"level":267},"\u002Fjava-interview\u002Fjvm#_2-一个对象从创建到回收经历了什么",[5],"new 之后先检查类是否已加载，然后在堆上分配内存。分配通常用指针碰撞或空闲列表，多线程下通过 TLAB 减少竞争；分配后先把实例字段清零，再设置对象头，最后执行构造器。 对象布局包括 Mark Word、类型指针、实例数据和对齐填充。Mark Word 存哈希、GC 年龄和锁状态；开启指针压缩后引用占 4 字节，堆小于约 32GB 时默认开启。 新对象一般进入 Eden。Eden 满时触发 Young GC，存活对象复制到 Survivor 并增加年龄，达到阈值后晋升老年代；大对象可能直接进老年代。老年代压力过大时触发 Full GC 或混合回收。",{"id":307,"title":63,"titles":308,"content":309,"level":267},"\u002Fjava-interview\u002Fjvm#_3-如何判断对象可以回收三色标记解决了什么问题",[5],"主流 JVM 用可达性分析：从 GC Roots 出发遍历引用链，不可达的对象才可回收。GC Roots 包括栈上的局部变量、静态字段、JNI 引用、活动线程和同步监视器等。 引用强度分强、软、弱、虚：软引用在内存不足时回收，弱引用下次 GC 就回收，虚引用只用于回收通知。finalize 已废弃，不应作为资源释放手段。 并发标记时用户线程仍在改引用，三色标记可能漏标：黑对象新增了指向白对象的引用，或灰对象到白对象的引用被删除。CMS 用增量更新记录新增引用，G1 用 SATB 记录删除前的快照，两者都靠写屏障实现。",{"id":311,"title":80,"titles":312,"content":313,"level":267},"\u002Fjava-interview\u002Fjvm#_4-g1-和-cmszgc-有什么区别",[5],"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 版本压测。",{"id":315,"title":97,"titles":316,"content":317,"level":267},"\u002Fjava-interview\u002Fjvm#_5-类加载过程和双亲委派模型",[5],"类的生命周期是加载、链接（验证、准备、解析）、初始化、使用、卸载。准备阶段给静态变量分配内存并设零值，static final 常量在编译期已写入常量池；初始化阶段才执行 \u003Cclinit>，包括静态赋值和静态块。 双亲委派指加载请求先交给父加载器，父加载器无法完成才自己加载。它保证核心类库不被同名类替换，也避免重复加载。启动类加载器、平台类加载器、应用类加载器是三层常见结构。 委派可以被打破：SPI 通过线程上下文类加载器加载实现类；Tomcat 为每个 Web 应用维护独立加载器，实现应用隔离；OSGi 则是网状委派。注意「被动引用」不会触发初始化，例如通过子类访问父类静态字段、访问编译期常量。",{"id":319,"title":118,"titles":320,"content":321,"level":267},"\u002Fjava-interview\u002Fjvm#_6-有哪些-oom线上怎么排查",[5],"常见类型：堆空间不足、GC 开销超限、元空间不足（动态生成类过多）、直接内存不足、无法创建本地线程（线程数或栈过大）、数组长度超限。 排查顺序：先确认是哪种 OOM，再看是否有 -XX:+HeapDumpOnOutOfMemoryError 留下的堆转储；用 jcmd \u003Cpid> GC.heap_info 看堆使用，用 jmap -histo:live 看对象分布，必要时导出 hprof 用 MAT 找支配树和 GC Root 路径。 还要区分「内存泄漏」和「内存不够」：前者是对象该回收却仍被引用，需要定位持有链；后者可能是缓存无上限、批量查询过大或堆设置不合理。直接内存和线程数问题不会体现在堆转储里，要看 NMT 和系统级指标。",{"id":323,"title":143,"titles":324,"content":325,"level":267},"\u002Fjava-interview\u002Fjvm#_7-jit-编译和逃逸分析对性能有什么影响",[5],"HotSpot 采用解释器加分层编译：热点方法先由 C1 快速编译，执行更久后交给 C2 做激进优化。方法内联、循环展开、常量折叠、去虚拟化都会显著影响峰值性能，所以短跑基准测试往往失真。 逃逸分析判断对象是否逃出方法或线程。不逃逸的对象可以标量替换，把字段拆成局部变量，从而避免堆分配；锁对象不逃逸时可以做锁消除。它不等于「对象一定分配在栈上」，HotSpot 主要做的是标量替换。 实际影响：压测前要预热，避免把解释执行阶段算进吞吐；不要用过于复杂的继承层次阻碍内联；-Xint 只用于排查，不适合生产。",{"id":327,"title":160,"titles":328,"content":329,"level":267},"\u002Fjava-interview\u002Fjvm#_8-jvm-参数怎么调gc-日志怎么看",[5],"先定堆：-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 往往要看是不是晋升过快或存在泄漏。",{"id":331,"title":195,"titles":332,"content":333,"level":267},"\u002Fjava-interview\u002Fjvm#易错点与自测",[5],"永久代已在 JDK 8 移除，元空间在本地内存，不等同于「没有上限」。 G1 的 Region 不等于物理连续的新生代\u002F老年代，不要把分代模型硬套到内存布局上。 堆 dump 只能解释堆内问题，直接内存、线程栈、元空间要另找证据。 自测：① 说明 CMS 与 G1 处理漏标的方式差异；② 给出一组容器内 4C8G 服务的 JVM 参数并解释理由；③ 描述一次「堆内存持续上涨」的完整取证步骤。",{"id":335,"title":214,"titles":336,"content":337,"level":267},"\u002Fjava-interview\u002Fjvm#参考资料",[5],"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 的设计目标",1793263559683]