资讯详情

重新认识JDK11:JVM内存分布与G1回收调优实战

📅 2026/10/17 11:06:35 | 华诺云谱 👁 阅读
重新认识JDK11:JVM内存分布与G1回收调优实战
1. 为什么我说“重新认识”——JDK11时代的内存变化写这篇文章的起因是前阵子帮某个业务系统做一次JVM参数调优结果发现一个很有意思的现象团队里大部分人还在用JDK8时代的思路来理解内存以为JVM就是“堆 永久代 栈 方法区”那套老框架。但当我打开JDK11的GC日志时发现不少旧观念已经不能直接用了一一尤其是永久代彻底变成历史、G1成为默认收集器之后内存分布的逻辑从“物理分代”转向了“逻辑分代 Region化布局”很多排查手段和分析路径都得跟着调整。这篇文章不打算面面俱到地复述《深入理解Java虚拟机》里的每一段描述而是想把JDK11 G1这条组合下的内存分布讲实、讲透每个内存区域在哪里、谁在用、什么时候分配、什么时候回收、出了问题时日志和监控里看到的数据到底意味着什么。适合四种人看刚把项目从JDK8迁到JDK11的同行面试被问到JVM内存模型想答得更有深度的候选人正在用G1但没搞懂GC日志里“Humongous”“Region”“RSet”这些词的同学以及纯粹想刷新一遍底层认知的Java开发者。先抛一个反直觉的结论JDK11里你在代码里new出来的对象并不一定全部“活”在堆里也不一定按照你想象的方式分代流转。JIT逃逸分析、TLAB、大对象Humongous分配、堆外缓存甚至G1的Region归属方式都会影响一个对象最终“住”在哪里。所以内存分布不是一张静止的图而是一条动态的流水线我们得从头开始把这套流水线捋清楚。2. JDK11下JVM内存区域的底层划分2.1 运行时数据区还剩下几块先摆一张基础地图。JDK11的JVM依然遵循Java虚拟机规范把运行时数据区分为几大块程序计数器、虚拟机栈、本地方法栈、堆、元空间Metaspace取代旧版永久代。其中堆和元空间是线程共享的其他区域基本是线程私有的。JDK11里最容易被忽视的变化是元空间的彻底独立。从JDK8开始永久代被删除原本放在永久代里的类元数据、方法元数据、常量池的一部分被搬到了本地内存中的Metaspace。JDK11延续并强化了这一设计Metaspace默认可以无限增长增长速度受物理内存限制。很多人在JDK8时代养成的习惯是“调大PermSize”到了JDK11如果还沿用这个参数不但没用反而会直接报错——这两个参数已经被移除了。2.2 字符串常量池和静态变量的真实归属另一个常见认知误区是字符串常量池。老资料说字符串常量放在永久代或方法区里而JDK11里字符串常量池在堆上更准确地说是在堆的“字符串去重存储”StringTable里。这意味着一个经过intern()的字符串它的char[]数组对象本身是在堆上分配并参与GC的。静态变量呢在JDK8以前静态字段存储在永久代中类卸载时随之回收。JDK11中静态变量对应的引用和基础类型值已经存放在堆上的Class对象里——这个Class对象在元空间里描述类结构但它持有的静态字段数据本身在堆中。换句话说静态变量是GC的“耙子”之一它会以“GC Roots”的身份被遍历但存储位置已经不在元空间。内存区域JDK8及以前JDK11GC参与情况堆新生代 老年代新生代Eden/Survivor 老年代物理上由Region组成Young GC / Mixed GC / Full GC参与永久代 / 元空间永久代在堆内有大小上限元空间占用本地内存动态扩展周期性回收废弃类与类加载器字符串常量池永久代堆参与GC可被回收静态变量永久代堆上的Class对象中作为GC Root不直接参与年轻代提升栈上分配对象无JIT逃逸分析后可入栈不参与GC随栈帧弹出销毁这张表建议截图保存面试和排查时都挺有用。2.3 元空间到底还回收什么元空间最容易被误解成“无限就没事了”其实它在高压力场景下也会出现瓶颈。元空间内部主要由ClassLoaderData、类元信息、方法字节码、常量池数据等组成。JDK11提供了元空间大小调整参数-XX:MaxMetaspaceSize、-XX:MetaspaceSize触发GC的阈值水位线以及-XX:MinMetaspaceFreeRatio和-XX:MaxMetaspaceFreeRatio控制扩展和收缩比例。如果你在JDK11上频繁加载卸载类比如热部署、动态生成大量增强类会看到元空间的周期性增长和回落。一个真实案例某项目用了大量运行时编译动态代理Metaspace一度涨到1.2GB而实际有效类并不多——这说明类加载器泄漏了旧类加载器没有被完全回收。排查时可以查看metaspace日志或使用jhsdb具体方法后面章节详说。3. 堆外和栈上JDK11中对象并不全在堆里3.1 TLAB与栈上分配的逻辑JVM在堆上为每个线程划分了一个TLABThread Local Allocation Buffer这是一个Eden区内部的私有缓冲区。小对象优先在TLAB中分配避免了线程间的锁竞争。很多人以为只要new对象就是在堆上分配其实在服务端编译器C2开启逃逸分析后一部分没有逃逸出方法作用域的对象可以直接在栈上分配标量替换实现这个对象完全不进入堆也不参与GC。我在一次压测中见过一个极端情况某个高频方法内部创建一个临时容器对象无逃逸被JIT优化后栈上分配结果Young GC次数少了20%以上。这不是玄学而是逃逸分析实实在在起效。JDK11默认开启了标量替换所以**“new出来的对象一定在堆上”这个说法是不准确**的——只能说绝大多数场景下对象最终会落到堆里。3.2 堆外内存绕过JVM管辖的“法外之地”JDK11里很多高性能库Netty、RocketMQ都使用堆外内存Direct Memory来减少GC压力与数据拷贝。Direct Memory是通过ByteBuffer.allocateDirect()分配的本地内存由JVM统一管理上限-XX:MaxDirectMemorySize但不参与堆GC回收依赖Cleaner机制。堆外内存的分配速度比堆内慢因为需要系统调用mmap但读写性能在高并发IO场景下优势明显。需要特别注意DirectMemory使用不当会出现“堆小但内存持续走高”的诡异现象伴随GC日志几乎正常、但进程OOM的情况。排查时要监控MaxDirectMemorySize和本机内存的使用量不能只盯着堆。4. G1视角下的内存空间Region、Humongous与RSet4.1 Region化布局带来的影响G1把整个Java堆划分为多个大小相等的Region每个Region默认大小由堆初始大小和目标Region数量默认2048决定范围为1MB~32MB通过-XX:G1HeapRegionSize可以手动指定。每个Region会按需要被标记为Eden、Survivor、Old、Humongous中的一种。这种“逻辑分代 物理不分代”的布局带来的直接变化是老年代对象和新生代对象在物理位置上完全打乱年轻代不再是连续的地址空间。旧时代的“连续内存晋升”模型被彻底抛弃取而代之的是基于Remembered Set的跨Region引用追踪。4.2 Humongous对象超过Region一半的大对象G1有一个特殊分配路径当对象大小超过Region容量的50%时会直接晋升为大对象区域Humongous Region直接在老年代连续Region上分配且不会在Young GC中被回收。Humongous区域从分配那一刻起就属于老年代。如果这种对象频繁出现会导致堆碎片化和Mixed GC周期变长。我在一个实际项目中遇到过非常典型的Humongous性能陷阱某个团队用Gson解析大JSON每解析一次产生一个20MB左右的byte[]在2GB堆上直接占据一整个Humongous Region结果Full GC频繁发生。后来改成分段解析问题立刻消失。这套排查方法会在第6章结合GC日志展开。4.3 RSet空间换时间的引用管理G1为了实现在回收某个Region时不必扫描整个堆引入了RSetRemembered Set。每个Region维护一个RSet记录“从其他Region指向本Region对象”的引用信息。代价是写屏障需要在每次引用赋值时维护RSet增加一些额外开销好处是Young GC和Mixed GC扫描范围大幅缩小。JDK11里可以关注-XX:G1RSetRegionEntries、-XX:G1RSetSparseRegionEntries这些参数但大多数场景不需要手工调。如果观察GC日志发现“RSet Updating”时间特别长说明跨Region引用很多这时候优化手段是调整卡表处理策略或者考虑提高Region大小以减少跨Region引用数量。这块偏进阶一般用默认配置即可。5. G1的回收机制与Stop The World的真实代价5.1 Young GCG1的日常回收G1的Young GC发生在Eden区耗尽时只回收Eden和Survivor区域将存活对象复制到新的Survivor或晋升到Old。这个过程是多线程并行执行的会有STWStop The World停顿但停顿时间通过增量回收和自适应预测控制在目标范围内-XX:MaxGCPauseMillis默认200ms。Young GC的日志里重点看几项Parallel Time、Def New、Copy。Parallel Time里含有多个子阶段比如Ext Root Scanning扫描GC Roots、Update RS更新RSet、Scan RS扫描RSet、Object Copy对象复制。如果一个系统的大部分GC停顿时间耗在“Update RS”上说明写屏障开销偏高需要审视代码里是否存在大量引用赋值操作或者代际引用是否过于频繁。5.2 并发标记与Mixed GCG1的“大扫除”当堆占用率达到-XX:InitiatingHeapOccupancyPercent默认45%时G1会启动并发标记周期。标记分为初始标记、并发标记、最终标记、清理四个阶段。初始标记和最终标记是STW的但并发标记阶段与业务线程并行运行不会停顿。标记完成后G1会触发Mixed GC在Young GC的基础上额外回收部分高回收收益的Old Region。这就是G1能控制停顿时间的核心逻辑——它不一次性回收全部老年代而是按“垃圾最多、代价最小”的原则挑着回收。Mixed GC可以连续发生多次直到老年代垃圾比例降到合理范围。5.3 Full GCG1最后的“兜底手段”G1的Full GC通常不是主动触发而是Mixed GC来不及回收时的退路或者是Humongous分配失败、RSet更新失败等异常场景导致的。JDK11中G1的Full GC是单线程串行的停顿时间通常非常长几百毫秒到几十秒都有可能。如果你在GC日志中看到Full GC第一反应不是调参数而是排查为什么Mixed GC没有及时控制住堆增长。常见原因有大对象Humongous频繁分配导致老年代空间提前耗尽-XX:InitiatingHeapOccupancyPercent设置过低并发标记频繁发生但回收跟不上分配速度To-Space ExhaustedSurvivor区放不下晋升对象提前晋升到老年代元空间扩展触发Full GCJDK11中少见但可能6. 从GC日志看内存分布一次真实问题排查6.1 被误解的“jstat老年代”先分享一个我实际踩过的坑。某个服务的GC日志显示老年代占用持续偏高监控面板上FGCFull GC Count也隔几分钟就1团队的第一反应是“G1没把老年代内存释放干净”于是开始调整G1HeapRegionSize和G1ReservePercent。但我在jmap和jhsdb分析后发现老年代里占大头的不是业务对象而是一批20MB左右的字节数组——也就是Humongous对象。Humongous对象一旦进入老年代不受Young GC控制只能等Mixed GC或Full GC来清理。如果这批大对象是短期存活的它会造成“堆占用虚高、回收收益为负”的局面。日志中如果大量出现“Humongous Allocation”相关记录先排查代码中的大数组、大集合。6.2 排查工具组合jstat、jhsdb、GC日志三板斧在JDK11上我一般用以下命令组合定位问题# 查看堆概览和各代容量 jstat -gc pid 1000 # 查看GC统计与停顿分布 jstat -gccause pid 1000 # 堆内对象分布粗看 jhsdb jmap --heap --pid pid # 导出堆转储强制Full GC后再对比堆大小 jhsdb jmap --histo:live --pid pid新JDK已经不支持老jmap的部分参数比如-F所以尽量用jhsdb。jhsdb jmap --heap能显示Region划分情况Eden、Survivor、Old、Humongous各自占多少Region这对判断大对象影响非常直观。6.3 分析GC日志的关键节点GC日志里需要盯住的字段是[Pause: Young (Normal)] (G1 Evacuation Pause)、[Pause: Young (Mixed)] (G1 Evacuation Pause)、[Concurrent Cycle]、[Pause: Full]。识别频率和停顿时间后建立几个阈值观察项正常范围异常信号Young GC频率每秒或数秒一次每秒数次且停顿超过平均停顿Mixed GC频率并发标记完成后周期出现长时间不出现又突然Full GCHumongous Allocation极少每次GC日志都出现Full GC几乎不出现一天超过一次就需重视堆占用率平稳波动持续上升且Mixed GC后不回落7. 基于内存分布设计JVM参数一组经过实测的基线配置7.1 先厘清目标停顿优先还是吞吐优先调整JVM参数之前先想清楚业务容忍度。互联网接口型应用通常希望单次GC停顿更短适合采用G1并设置合理的MaxGCPauseMillis离线批处理型应用更看重吞吐量可以适当提高停顿目标减少GC频率。JDK11上我常用的基线参数如下以8GB堆、16核服务为例-Xms8g -Xmx8g -XX:UseG1GC -XX:MaxGCPauseMillis150 -XX:ParallelGCThreads8 -XX:ConcGCThreads4 -XX:G1HeapRegionSize4m -XX:InitiatingHeapOccupancyPercent45 -XX:PrintGCDetails -XX:PrintGCDateStamps -Xloggc:/path/to/gc.log-Xms和-Xmx设为一致避免JVM扩容带来的额外成本和毛刺。G1HeapRegionSize设置成4m的好处是8GB堆会被划分为2048个Region这是G1的设计目标同时避免Region太小导致RSet膨胀。7.2 参数不是越多越好很多人喜欢堆砌-XX:UseConcMarkSweepGC、-XX:UseAdaptiveSizePolicy这类参数这在JDK11中不但没有意义有些还直接不支持了。G1的特点是自适应调节它会根据目标停顿和吞吐自动调整新生代大小、晋升阈值、回收Region数量。你调得越勤反而可能破坏它的自适应性。真正值得下功夫的参数就几个堆大小、Region大小、并发标记阈值、停顿目标、GC日志开关。其他的交给G1自己决策。7.3 停止手工调优的迷信用数据说话G1内部有很多启发式算法比如-XX:G1NewSizePercent、-XX:G1MaxNewSizePercent控制新生代大小范围-XX:G1MixedGCCountTarget控制Mixed GC次数。它们的作用是让G1“跟着业务走”。我的经验是先跑一段时间拿GC日志做充分分析再决定是否修改任何一个参数不要一上来就参数轰炸。举个例子如果Young GC频繁但停顿很短说明新生代过小可以适当提高G1NewSizePercent如果Mixed GC后老年代占用仍持续在高位才需要调整InitiatingHeapOccupancyPercent或检查Humongous问题。8. 常见误区与易踩的坑8.1 “内存越大越好”的幻觉给堆分配过大内存不一定是好事。在G1下堆越大Region越多RSet和并发标记的耗时也越大。一次分配24GB堆Region数量超过6000GC相关数据结构占用的内存和CPU都明显增加。业务峰值只需要6GB硬给16G结果就是GC停顿反而变长。合理评估对象规模再来定堆大小比拍脑袋给两倍更可靠。8.2 Survivor区并非越大越安全年轻代里Eden和Survivor的比例不是固定8:1:1G1是按目标停顿自动调整的。新人容易误调-XX:SurvivorRatio在G1下这个参数的作用有限。Survivor过小可能导致对象提前晋升到老年代Survivor过大又浪费新生代空间导致Eden变小、Young GC更频繁。让G1自己调整即可。8.3 “Full GC一旦发生就完蛋”需要区分场景G1的Full GC确实很慢但有些Full GC是“伪Full GC”——比如显式调用System.gc()。JDK11中如果没有加-XX:DisableExplicitGC业务代码里的System.gc()会触发Full GC导致你误以为内存出问题结果是代码bug。线上排查时先用jstack看是否有业务线程调用了System.gc()别一看到Full GC就慌着堆调参。8.4 元空间的二次踩坑前面说过元空间可以无限扩展这带来另一个坑某些框架如果持续创建类加载器且不释放元空间会不断攀升最终触发系统级内存不足。因为元空间不在堆内jstat -gc看不到它多大得用jstat -gcutil或jcmd查看jcmd pid VM.metaspace如果发现元空间占用远高于预期优先怀疑类加载器泄漏。9. 实操建议怎么给新项目起步并且长期观测9.1 起步三步走第一先把堆大小定成一个合理值不要小于稳定状态下的对象占用峰值。第二开启GC日志至少保留最近几十个文件方便事后回放。第三每天或每周定时导出GC日志统计关注停顿分布的P99、Full GC频率和Humongous数量。9.2 GC日志滚动配置JDK11的日志配置建议使用新版命令行模式-Xlog:gc*:filegc.log:time,uptime,level,tags:filecount10,filesize20m这样能自动切割日志避免日志文件无限制膨胀。生产环境一定要开日志没日志相当于盲人摸象很多问题根本没有排查入口。9.3 聊聊我推荐的分析思路拿到一段GC日志后我习惯按这么顺路走看格式是否正常、有没有Full GC看Young GC频率和平均停顿是否达到目标看Mixed GC有没有及时触发老年代趋势是涨还是跌看Humongous对象数量若有明显异常走堆转储定位来源结合业务流量时间点判断GC是否与高峰对应最后才决定是否动参数。这套方法已经帮我把几个“莫名其妙慢”的线上应用拉回正轨最大的体会是G1是一个优秀的自动调优器它比大多数手工参数组合都聪明。你要做的不是频繁干预它而是理解它在做什么并确保它能看到正确的运行环境。如果你正在JDK11上维护一个高并发服务建议花一两个小时把堆内存的Region状态和GC日志细细看一遍——这比收藏一百篇理论文章都有用。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑