首页/新闻资讯/正文详情

JAVA进阶—JVM篇

发布时间:2026/11/5 6:53:32 来源:云帆数科 栏目:资讯中心
JAVA进阶—JVM篇
运行时数据区总览JVM 规范把运行时数据区分为五块外加一块规范之外但实际存在的直接内存区域线程私有/共享存放内容可能抛出的错误程序计数器私有当前线程执行的字节码指令地址无虚拟机栈私有栈帧局部变量表、操作数栈等StackOverflowError / OOM本地方法栈私有native 方法调用StackOverflowError / OOM堆共享对象实例、数组OOM: Java heap space方法区共享类元数据、常量、静态变量、JIT 代码OOM: Metaspace直接内存共享堆外NIO DirectByteBuffer 等OOM: Direct buffer memory程序计数器是唯一不会 OOM 的区域虚拟机栈和本地方法栈可能 StackOverflowError也可能 OOM堆和方法区主要 OOM。1. 程序计数器PC Register每个线程一块很小的内存记录当前线程正在执行的字节码指令地址。字节码解释器靠它取指令、分支跳转、循环、异常处理都依赖它。为什么线程私有CPU 在多线程间切换线程恢复执行时必须知道自己执行到哪了所以每个线程必须有独立的 PC。两个核心细节执行native 方法时PC 的值是undefined因为 native 方法不是字节码PC 没有意义。它是唯一在 JVM 规范中没有规定任何 OutOfMemoryError的区域。2. 虚拟机栈Java Virtual Machine Stack线程私有生命周期与线程相同。每个方法被执行时创建一个栈帧入栈方法结束栈帧出栈。栈帧结构局部变量表存放方法参数和方法内定义的局部变量。以 Slot 为最小单位32 位类型占 1 个 Slotlong/double占 2 个 Slot。引用类型存的是指向对象的引用具体是句柄还是直接指针取决于访问方式。注意局部变量表在编译期就确定大小运行期不会变写在 Code 属性的max_locals里。操作数栈字节码指令的工作区。比如iadd就是从栈顶弹出两个 int 相加再压回。它和局部变量表之间的数据搬运由字节码指令完成。动态链接每个栈帧持有一个指向运行时常量池中该栈帧所属方法的引用。为什么要“动态”一个方法在编译期不知道会调用哪个具体实现多态需要在运行期把符号引用解析为直接引用。方法返回地址方法正常返回或异常退出后回到调用者的哪条指令继续执行。附加信息调试信息等。会出现两个错误StackOverflowError线程请求的栈深度超过-Xss限制典型是无限递归。OutOfMemoryError栈可以动态扩展HotSpot 不支持动态扩展但规范允许扩展时申请不到内存。3. 本地方法栈Native Method Stack为 native 方法服务作用和虚拟机栈类似。HotSpot 把虚拟机栈和本地方法栈合并实现所以实际只有一个栈。规范允许两者合并也允许分开。4. 堆HeapJVM 管理的内存中最大的一块线程共享唯一目的就是存放对象实例和数组。几乎所有的对象实例都在这里分配逃逸分析下的栈上分配是例外见上一轮 JIT 部分。分代原因基于两个经验假说——弱分代假说绝大多数对象朝生夕死和强分代假说熬过越多次 GC 的对象越难死。分代后可以对不同年龄的对象用不同收集算法新生代用复制算法对象存活少复制成本低老年代用标记-清除/标记-整理。典型分代结构新生代 Eden Survivor0 Survivor1默认比例8:1:1-XX:SurvivorRatio8。两个 Survivor 中总有一个是空的用于复制算法。老年代。新生代与老年代默认比例1:2-XX:NewRatio2即新生代占 1/3。对象分配流程面试高频要能串成一条线先在TLABThread Local Allocation Buffer里分配。TLAB 是 Eden 区中每个线程私有的小块缓冲区避免多线程分配时的 CAS 竞争。TLAB 用完才走共享 Eden 的分配。Eden 分配不下 → 触发Minor GC。Minor GC 时Eden 和当前 From Survivor 中存活对象复制到 To Survivor年龄 1两个 Survivor 角色互换。对象年龄达到阈值默认 15-XX:MaxTenuringThreshold实际受对象头 4 bit 限制最大 15→ 晋升老年代。动态年龄判定Survivor 中相同年龄所有对象大小之和超过 Survivor 空间一半年龄大于等于该年龄的对象直接晋升老年代不必等到阈值。大对象直接进老年代-XX:PretenureSizeThreshold只对 Serial/ParNew 有效避免在 Eden 和 Survivor 之间大量复制。空间分配担保Minor GC 前检查老年代最大可用连续空间是否大于新生代所有对象总大小不满足则可能触发 Full GC。堆参数-Xms初始大小、-Xmx最大大小、-Xmn新生代大小。生产上通常-Xms和-Xmx设成一样避免动态扩缩容带来的抖动。5. 方法区Method Area逻辑概念JVM 规范定义线程共享存储已被虚拟机加载的类型信息、字段信息、方法信息、运行时常量池、静态变量、JIT 编译后的代码等。关键变迁面试必考JDK7 及以前HotSpot 用永久代实现方法区位于堆内大小固定受-XX:MaxPermSize限制。JDK8 开始移除永久代改用元空间实现使用本地内存受-XX:MetaspaceSize和-XX:MaxMetaspaceSize约束。移除永久代原因永久代大小固定类加载多时容易OOM: PermGen space调参困难。永久代在堆内Full GC 要扫描它影响效率。字符串常量池、静态变量等在 JDK7 已迁到堆永久代剩余内容类元数据更适合放本地内存。为 JRockit 与 HotSpot 合并铺路JRockit 没有永久代。几个容易混的池运行时常量池方法区的一部分存放编译期生成的字面量和符号引用以及运行期解析出的直接引用。每个类一份。字符串常量池JDK7 起在堆中JDK8 仍在堆。它和运行时常量池不是一回事。Class 文件常量池编译期产物在 Class 文件中加载后进入运行时常量池。6. 直接内存Direct Memory不是 JVM 运行时数据区的一部分但被频繁使用。NIO 的ByteBuffer.allocateDirect()分配的就是直接内存绕过 Java 堆减少一次堆内到堆外的拷贝。受物理内存和-XX:MaxDirectMemorySize限制不设置则默认等于-Xmx。回收依赖Cleaner虚引用 ReferenceQueueFull GC 时触发所以直接内存泄漏往往表现为堆内存正常但物理内存上涨最终OOM: Direct buffer memory。排查时jmap -histo看不到它要看BufferPoolMXBean或 NMTNative Memory Tracking。对象在内存中的布局HotSpot一个对象在堆中的布局分三块对象头HeaderMark Word存储哈希码、GC 分代年龄、锁状态标志、偏向线程 ID、偏向时间戳等。32 位 JVM 占 4 字节64 位占 8 字节。它是复用的不同锁状态下存不同内容——这也是锁升级无锁 → 偏向锁 → 轻量级锁 → 重量级锁能实现的基础。Klass Pointer指向方法区中该类的元数据即“这个对象是哪个类的实例”。开启指针压缩-XX:UseCompressedOops64 位 JVM 默认开启后占 4 字节否则 8 字节。数组长度只有数组对象才有4 字节。实例数据Instance Data真正存储的字段内容包括父类继承下来的。HotSpot 默认分配顺序是long/double → int → short/char → byte/boolean → oop相同宽度字段放在一起便于对齐。对齐填充PaddingHotSpot 要求对象起始地址是 8 字节的整数倍不足则填充。所以对象大小总是 8 的倍数。指针压缩64 位 JVM 上引用占 8 字节堆内存消耗大。开启压缩后引用存的是“偏移量/8”用 32 位表示可寻址 2^32 * 8 32GB。堆超过 32GB 压缩失效所以很多服务把堆控制在 32GB 以内不是随意定的。对象的创建过程从字节码new指令到对象可用大致五步类加载检查检查常量池中能否定位到类的符号引用类是否已加载、解析、初始化。没有则先执行类加载。分配内存根据堆是否规整用指针碰撞规整如复制算法/标记-整理后或空闲列表不规整如标记-清除后。并发安全靠CAS 失败重试或TLAB。初始化零值把分配到的内存空间初始化为零值不包括对象头保证实例字段不赋初值也能用。这就是为什么成员变量有默认值而局部变量没有。设置对象头设置 Mark Word哈希码、GC 年龄、锁标志等和 Klass Pointer。执行init执行构造方法按程序员意图初始化。注意第 3 步和第 5 步的区别零值初始化是 JVM 行为init是 Java 代码行为。字节码里new之后是dup、invokespecial initinit执行完对象才真正可用。对象访问定位引用变量怎么找到堆上的对象有两种方式句柄访问堆中划出句柄池引用指向句柄句柄包含对象实例指针和类型指针。优点是对象移动GC 复制/整理时只需改句柄引用本身稳定缺点是多一次间接寻址。直接指针引用直接指向对象对象头里存类型指针。优点是快少一次寻址缺点是对象移动时要改引用。HotSpot 使用直接指针访问因为对象访问非常频繁速度优先。垃圾收集器收集器分代/分区核心停顿特征核心算法/机制设计取向Serial / Serial Old分代物理隔离全停顿单线程新生代复制 / 老年代标记-整理简单、低开销ParNew分代全停顿多线程并行复制新生代多核下的 Serial 替代Parallel Scavenge / Old分代全停顿多线程并行复制 / 标记-整理吞吐量优先CMS分代大部分并发少 STW标记-清除 增量更新低停顿优先G1Region 分区可预测停顿目标值Region SATB 标记-整理停顿与吞吐的平衡ZGC非分代Region亚毫秒级 STW染色指针 读屏障极低延迟ShenandoahRegion亚毫秒级 STWBrooks 指针 读写屏障极低延迟另一路线分代 ZGCJDK 21Region 逻辑分代亚毫秒级 STW染色指针 读屏障 写屏障低延迟 更高吞吐“停顿时间”和“吞吐量”在大多数情况下是此消彼长的。停顿压得越低通常意味着更多的并发工作和屏障开销单位时间内完成的业务量会有所折损。后面每个收集器的取舍都可以放回这条轴上来理解。Serial / Serial OldSerial 是最基础的收集器单线程完成回收且回收期间所有用户线程暂停STW。新生代用复制算法老年代版本 Serial Old 用标记-整理算法。它的“单线程”不仅仅意味着只用一条 GC 线程更关键的是它假设回收时应用完全让出 CPU因此没有线程间同步、没有并发屏障开销极低。在单核或小堆通常小于 1GB的场景下这种“简单到极致”的设计反而可能是综合成本最低的选择。面试中的分寸感不要因为“单线程”就轻视它。Serial 是理解后续所有并发收集器开销来源的参照系——后面每一个优化本质上都是在为“不停顿或少停顿”支付额外的复杂度成本。Parallel Scavenge / Parallel OldParallel 系列是 Serial 的多线程并行版本新生代 Parallel Scavenge 用复制算法老年代 Parallel Old 用标记-整理。它的关注点和 CMS 截然不同它不追求最短停顿而是追求可控的吞吐量用户代码运行时间占总时间的比例。Parallel 提供了两个直接服务于吞吐目标的参数-XX:MaxGCPauseMillis停顿时间目标和-XX:GCTimeRatioGC 时间占比目标。但要注意Parallel 的停顿时间目标不是“承诺”它会在吞吐优先的前提下尽量去逼近如果设置得过激可能反而导致频繁回收、吞吐下降。适用判断批处理、离线计算、日志聚合这类对单次停顿不敏感、但对单位时间总产出敏感的服务端场景Parallel 仍然是务实的选择。尤其在小堆上G1 的并发周期开销可能反而不如 Parallel 专注吞吐来得高效。CMSCMSConcurrent Mark Sweep是 JVM 历史上第一个实现 GC 线程与用户线程并发执行的收集器作用范围是老年代必须搭配新生代的 Serial 或 ParNew 使用。它的出现标志着收集器的关注点从“怎么收得更快”转向了“怎么让应用少停”。核心机制CMS 用三色标记法做并发标记用增量更新解决并发标记期间用户线程修改引用导致的“对象消失”问题——当一个黑色对象新增了对白色对象的引用时将它重新标记为灰色留到最终标记阶段重新扫描。整个流程中初始标记和最终标记是 STW中间的并发标记、并发预清理、并发清除都与用户线程并行。三个无法根治的固有缺陷也是 CMS 被淘汰的根本原因浮动垃圾并发清除阶段用户线程仍在运行新产生的垃圾本轮无法回收只能留到下一次。内存碎片基于标记-清除不整理内存。碎片累积到无法为大对象分配连续空间时触发并发模式失败Concurrent Mode FailureJVM 被迫退化为 Serial Old 做一次全停顿的压缩回收停顿反而可能比不用 CMS 更长。CPU 争抢并发阶段 GC 线程与业务线程抢 CPU在核数有限的环境下对吞吐的影响不可忽视。CMS 在 JDK 9 被标记废弃JDK 14 正式移除。它证明了并发回收可行但“不整理内存”这个选择带来的碎片和退化风险是它无法跨越的结构性天花板。G1G1 是 JDK 9 起的默认收集器它的核心创新不在于“更快的并发”而在于改变了回收的粒度。Region 化整为零G1 把堆划分为约 2048 个等大小的 Region1MB–32MB每个 Region 动态扮演 Eden、Survivor、Old 或 Humongous 角色。分代不再是物理隔离而是逻辑划分。收益优先每次回收不再追求清理全堆而是根据-XX:MaxGCPauseMillis的停顿目标优先回收垃圾占比最高的若干个 RegionCollection Set。用“每次少收一点、但每次都很快”来逼近停顿目标。跨 Region 引用的代价因为 Region 之间可以互相引用G1 为每个 Region 维护一个Remembered Set (RSet)记录“谁引用了我”。回收某个 Region 时只扫它的 RSet而不扫全堆。RSet 的维护约占堆内存的 1%–5%这是 G1 为实现部分回收付出的空间和写屏障开销。并发标记用 SATB与 CMS 的增量更新不同G1 采用原始快照Snapshot At The Beginning——在标记开始时对对象图做一个逻辑快照之后新产生的引用变化通过写屏障记录下来标记过程基于快照进行。这样最终标记阶段只需处理快照期间变化的少量引用STW 时长通常远低于 CMS 的重新标记。混合回收Mixed GC当老年代占用达到-XX:InitiatingHeapOccupancyPercent默认 45%时触发一次回收同时清理所有年轻代 Region 和部分垃圾最多的老年代 Region尽量避免触发全停顿的 Full GC。G1 的边界它的优势建立在堆足够大、Region 足够多之上。小堆上 Region 数量有限每次并发周期要处理的比例更高加上 RSet 和写屏障的开销综合表现未必优于 Parallel。另外MaxGCPauseMillis是目标而非承诺回收收益不足时 G1 会超出目标。ZGC 与 Shenandoah当堆达到数百 GB 甚至 TB 级时G1 的停顿通常在 100ms 以上仍然无法满足极低延迟的需求。ZGC 和 Shenandoah 的目标都是将 STW 控制在 10ms 以内且停顿时间基本不随堆大小增长。两者都基于 Region但实现并发转移的底层机制选择了不同路线。ZGC染色指针 读屏障ZGC 的核心思路是把对象的标记和转移状态直接编码到指针本身。利用 64 位指针中尚未使用的若干高位存储 Marked0、Marked1、Remapped、Finalizable 等标志位。这样做的好处是对象头中不需要再存 GC 标记信息标记和转移可以完全并发进行转移完成后也不需要遍历修改所有指向该对象的引用因为指针的“颜色”已经携带了转发信息。代价体现在读屏障上每当应用线程从堆中读取一个对象引用时会触发一小段屏障代码检查指针的颜色。如果对象已被转移但指针还是旧颜色屏障会通过转发信息找到新地址并尝试用 CAS 把引用“自愈”为新指针。ZGC 的回收周期中只有初始标记和最终标记两个极短的 STW 阶段均小于 1ms并发标记、并发转移、并发重映射全部与应用线程并行。ShenandoahBrooks 指针 读写屏障Shenandoah由 Red Hat 主导走了另一条路。它没有把状态编码进指针而是在每个对象前增加一个转发指针Brooks Pointer。对象被转移时旧对象上的 Brooks 指针被更新为指向新副本所有对对象的访问都要先经过这个转发指针间接寻址。Shenandoah 同时使用读屏障和写屏障读屏障处理并发转移期间的访问转发写屏障则基于 G1 的 SATB 并扩展以支持并发复制。两者对比的关键ZGC 的染色指针使得读屏障的条件判断可以转化为对指针颜色的简单检查开销极低而 Shenandoah 的 Brooks 指针需要每次对象访问都承担一次间接寻址的固定开销。因此 ZGC 对用户程序的运行时负载通常更低但 Shenandoah 的优势在于不依赖特定的指针位布局平台适应性更广。分代 ZGCJDK 21非分代 ZGC 虽然把停顿压到了极致但它对所有对象一视同仁地扫描和转移而实际应用中绝大多数对象朝生夕死。这意味着大量短命对象被反复卷入昂贵的并发标记和转移流程造成吞吐浪费。JDK 21 的分代 ZGCJEP 439将堆在逻辑上划分为年轻代和老年代年轻代可以更频繁、更低成本地回收老年代只在必要时处理从而在保持亚毫秒停顿的前提下提升整体吞吐。官方数据显示分代 ZGC 相比单代 ZGC 约有10% 的性能提升。代价是屏障复杂度的上升分代 ZGC 在原有的染色指针和读屏障基础上增加了写屏障store barrier用于追踪跨代引用确保年轻代回收时不需要扫描整个老年代。这也印证了那条主线每一次吞吐或延迟上的改进几乎都要用更精细的屏障和记账来交换。选型判断依据小堆 4GB、批处理或资源受限环境Parallel 或 Serial 仍是合理选择简单、吞吐高、无并发开销。中等到大堆4GB–数十GB、通用服务端G1 是默认起点利用MaxGCPauseMillis设定合理目标观察实际停顿分布和 RSet 开销。超大堆100GB、延迟极度敏感ZGCJDK 21 优先考虑分代 ZGC或 Shenandoah。但要接受吞吐略低于 G1/Parallel的代价ZGC 在低核数或需要高吞吐的场景下未必优于 G1。CMS 的遗留系统理解它正在被 G1 取代迁移时重点关注 Humongous 对象、RSet 内存开销和 Mixed GC 的触发阈值。不要把“停顿时间短”等同于“GC 总开销低”。ZGC 的单次停顿极短但并发标记、转移和屏障的总 CPU 消耗是实打实的G1 为了达成停顿目标也会做比 Parallel 更多的工作。评估收集器最终要落到单位时间内完成的业务量和P99 延迟这两个综合指标上。其他最后我们来再深入聊聊上面提到的几个算法、浮动垃圾、内存碎片等。标记-清除回收的本质是“记录空闲”而不是“抹掉字节”标记-清除分两个阶段标记阶段从 GC Roots 出发做可达性分析把存活对象打上标记清除阶段并不真的去把垃圾对象的内存字节清零而是把已死亡对象的起始和结束地址记录到一个空闲列表Free List里下次分配新对象时内存管理模块从这个列表中寻找足够大的空闲块。碎片为什么必然产生因为清除阶段不移动存活对象。存活对象留在原地垃圾对象留下的空洞就散布在堆的各个位置形成大量不连续的小块空闲内存。空闲列表里记录的就是这些零散的空洞的起止地址。如果接下来要分配一个大对象需要连续的内存空间而空闲列表中虽然有大量空闲总量却没有一块足够大的连续区间就会被迫提前触发下一次 GC——明明堆里还有不少空闲却因为不连续而用不上。分配方式标记-清除之后堆内存不规整所以分配新对象必须走空闲列表不能用简单的指针碰撞。空闲列表的选择策略首次适应、最佳适应等本身也会影响碎片分布的形态。标记-整理用“移动”换“连续”标记-整理的前半段和标记-清除一样——做可达性分析打标记。区别在后续它把所有存活对象向内存的一端移动并紧凑排列然后直接清理掉边界以外的内存。因为没有留下空洞堆重新变得规整分配对象可以回到简单的指针碰撞碎片问题从根源上被消除。但“移动对象”不是免费的。在 HotSpot 的生产级实现中标记-整理最典型的算法是Lisp2 滑动压缩sliding compaction分为四个阶段阶段一标记。从 GC Roots 遍历对象图标记所有存活对象。阶段二计算新地址。顺序扫描堆维护一个“下一个可用位置”指针把每个存活对象将要搬去的新地址写入它对象头中的转发指针forwarding pointer字段。这一步只计算不移动。阶段三更新引用。再扫描一遍堆对于所有存活对象中存储的引用顺着目标对象的转发指针把引用值改成目标对象的新地址。阶段四执行移动。把每个存活对象实际拷贝到它在阶段二中计算出的新地址。Lisp2 的代价很明确需要额外的转发指针空间通常借用对象头的一个字段并且要多次扫描堆。但它支持变长对象和保持对象原有顺序这是两指针算法Two-Finger只适用于等长对象做不到的。更关键的是移动存活对象意味着必须暂停用户线程——并发环境下其他线程可能正在读写这些对象地址一变就会出错。所以标记-整理的 STW 停顿通常比标记-清除更长。总结标记-清除不动对象省了移动成本但把碎片代价留给了未来的分配标记-整理用一次移动的成本换来了长期规整的分配空间。标记算法三色标记法并发标记的“状态机”标记-清除和标记-整理在“标记”阶段如果全程 STW那什么都不用操心。但现代收集器要让标记和用户线程并发执行用户线程可能在标记过程中修改引用关系。三色标记法就是用来在这种并发环境下描述和推导标记状态的理论工具。三种颜色白色尚未被 GC 访问过的对象。初始时所有对象都是白色标记结束后仍然是白色的就是不可达的垃圾。灰色对象本身已经被访问过但它至少还有一个直接引用的对象没有被扫描。灰色是“正在处理中”的中间状态。黑色对象本身和它所有直接引用的对象都已经被访问过。黑色是“安全存活”的状态——黑色对象不可能不经过灰色对象直接指向白色对象这是三色标记正确性的核心不变式。流程初始标记时GC Roots 变为黑色它们直接引用的对象变为灰色放入灰色队列。然后反复从队列取出灰色对象把它引用的白色对象变灰入队自身变黑直到灰色队列为空。最后仍为白色的对象即为不可达。并发带来的问题——漏标。在 GC 标记的同时用户线程可以修改引用。漏标会导致存活对象被误当垃圾回收是必须解决的严重 bug。漏标的发生需要两个条件同时成立黑色对象指向了白色对象建立了一条新引用该白色对象从所有灰色对象那里可达的路径被切断原有的灰色→白色引用被删除。两个条件缺一不可。只要破坏其中任何一个漏标就不会发生。这直接引出了两种解决方案增量更新Incremental Update破坏条件 1。当黑色对象插入指向白色对象的新引用时通过写屏障记录这个插入在重新标记阶段以该黑色对象为根重新扫描相当于把它“退回灰色”。CMS 走的是这条路。SATBSnapshot At The Beginning原始快照破坏条件 2。当灰色对象要删除指向白色对象的引用时通过写屏障把被删除的旧引用记录下来在重新标记阶段以这些旧引用为根重新扫描保证那个白色对象本轮至少存活下来留到下一轮再判定。G1 和 Shenandoah 的并发标记基于 SATB。浮动垃圾不是“标记错了”而是“本轮来不及处理”浮动垃圾和漏标是两个方向相反的问题漏标是该活的被误杀不可接受浮动垃圾是该死的本轮没死可容忍留到下一轮。在三色标记的并发场景下浮动垃圾至少有两个来源来源一SATB 的“保守存活”。当灰色对象删除了对白色对象的引用时SATB 通过写屏障把旧引用记录下来重新标记阶段这个白色对象会被标记为存活。即使它实际上已经不可达了本轮也会存活下来成为浮动垃圾。这是 SATB 为“不误杀”付出的代价——宁可多留不可错杀。来源二并发清理阶段新产生的垃圾。以 CMS 为例并发清除阶段用户线程还在运行期间新产生的垃圾对象在标记过程结束后才出现CMS 无法在当次收集中处理它们只能留到下一次 GC。并发标记期间新分配的对象通常也直接当作黑色处理本轮不清除其中后来变成垃圾的部分同样构成浮动垃圾。浮动垃圾的实际影响在于空间压力它占着老年代空间但不能被本轮回收如果浮动垃圾积累过快预留的空间被耗尽就会触发Concurrent Mode FailureJVM 被迫退化为 Serial Old 做一次全停顿压缩回收停顿反而可能比不用 CMS 更长。所以浮动垃圾本身不破坏正确性但它会吃掉并发回收的安全余量。总结收集器标记算法清除/整理方式并发正确性方案浮动垃圾的主要来源碎片来源Serial / Serial Old可达性分析全程 STW新生代复制 / 老年代标记-整理Lisp2 滑动无并发无需屏障无无整理后规整Parallel Scavenge / Old可达性分析全程 STW复制 / 标记-整理无并发无无CMS三色标记标记-清除空闲列表增量更新 写屏障并发清理新垃圾 并发标记期间新分配对象标记-清除不移动碎片必然产生碎片累积触发 Concurrent Mode FailureG1三色标记Region 内复制/整理SATB 写屏障RSet 记录跨 Region 引用SATB 保守存活的旧引用不整体整理但 Region 粒度复制使单 Region 内规整Humongous 对象和 Region 间碎片仍需关注ZGC三色标记染色指针承载状态并发转移Region 间复制读屏障染色指针自带标记/转发信息并发标记期间新分配对象通常视为存活转移后规整碎片在单次周期内被压缩Shenandoah三色标记SATB并发压缩Brooks 转发指针读屏障 写屏障Brooks 指针做间接寻址SATB 保守存活并发压缩碎片被消除Brooks 指针每次访问有固定间接开销CMS 的碎片和浮动垃圾是同一个设计选择的两面。它为了不停顿而选择并发并发就必须接受“清理阶段用户线程还在跑”于是产生浮动垃圾它为了不移动对象而选择标记-清除于是碎片是结构性的。CMS 后来提供-XX:UseCMSCompactAtFullCollection在 Full GC 时整理碎片但整理必须移动对象又回到了 STW 的老路——只能缓解不能根治。G1 用 SATB 而不是增量更新和它的 Region 化设计有关。SATB 在重新标记阶段只需要处理被删除的旧引用工作量通常比增量更新需要重新扫描的黑色对象集合更可控同时 G1 的 RSet 已经承担了跨 Region 引用的记录成本SATB 的写屏障可以复用类似机制。代价是 G1 的浮动垃圾比 CMS 更“保守”——被删除的引用所指向的对象本轮至少活下来。ZGC 和 Shenandoah 的“碎片消除”是靠并发转移实现的不是靠标记-整理。它们没有走传统 Lisp2 的“STW 滑动压缩”而是把“移动对象”本身做成了并发ZGC 用染色指针 读屏障Shenandoah 用 Brooks 指针 读写屏障使得对象在转移过程中应用线程仍然能正确访问。代价从“STW 停顿”转移到了“每次引用访问的屏障开销”上——这是低延迟收集器共同的交易结构。线上排查方法CPU 飙高top定位进程 →top -Hp PID定位线程 →printf %x\n TID转十六进制 →jstack PID | grep 十六进制TID -A 20看堆栈。如果线程是RUNNABLE且在执行业务代码怀疑死循环或算法复杂度如果是VM Thread或 GC 线程转去jstat -gcutil确认是否频繁 Full GC。关键是能根据线程状态把方向从“业务代码”切换到“GC”或“锁竞争”。OOM第一步永远是确认耗尽的是哪一块内存——Java 堆、元空间、直接内存还是创建线程所需的本地内存。盲目“调大-Xmx”会漏掉无界集合、类加载器泄漏、直接内存未释放、线程数量失控这些根因。堆 OOM 的标准证据链是-XX:HeapDumpOnOutOfMemoryError拿到 dump → MAT 看支配树和 GC Roots 路径 → 定位持有链 → 回到代码确认持有是否合理。元空间 OOM 则要往动态生成类反射、代理、类加载器泄漏的方向查。类加载双亲委派收到加载请求先委派给父加载器父加载器无法完成才自己加载目的是保证核心类库不被应用代码替换。打破的场景和代价SPIJDBC接口在启动类加载器加载的rt.jar中驱动实现在应用类路径下。启动类加载器无法“看见”子加载器路径上的类于是通过线程上下文类加载器让父加载器反向请求子加载器去加载驱动。Tomcat每个 Web 应用需要隔离各自的依赖版本Tomcat 的WebAppClassLoader优先加载WEB-INF下的类形成 child-first 的委派顺序。双亲委派从来不是 JVM 强制规则它只是ClassLoader.loadClass的默认实现重写这个方法就可以改变委派行为。同时类在 JVM 中的唯一性由“全限定名 加载它的类加载器”共同决定同一个类名被不同加载器加载出来就是两个不同的类型不能互相强转——这正是自定义类加载器做热部署和隔离的基础。初始化时机new、读写静态字段编译期常量除外、调用静态方法、反射、初始化子类先初始化父类等。static final的基本类型和 String 字面量在编译期就内联进调用方的常量池不会触发定义类的初始化通过子类引用父类静态字段只初始化父类不初始化子类。JIT 与运行时优化逃逸分析逃逸分析本身不是优化手段而是一个分析依据判断对象是否会从方法返回、存入外部字段、传给其他线程。如果证明不逃逸C2 编译器可以在此基础上做三件事栈上分配对象随栈帧弹出即回收不经过 GC标量替换把对象拆成若干标量直接放在寄存器/局部变量中对象本身被消除锁消除对不逃逸的局部对象加锁其他线程根本看不到它同步可以被完全去掉。你不能通过注解或参数“强制”某个对象栈上分配是否做、做到什么程度完全由 JIT 根据方法热度是否达到 C2 编译阈值和分析结果决定。它并不是可以主动使用的“优化技巧”而应定位为“理解 JVM 在什么条件下能替你做掉哪些事”。

相关推荐

L08 RuoYi 代码生成器实战:使用代码生成功能实现学生信息表 CRUD
L08 RuoYi 代码生成器实战:使用代码生成功能实现学生信息表 CRUD

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 6:53:32

3d 分割优化实战笔记2026
3d 分割优化实战笔记2026

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 6:53:32

第 2 篇|搭入口层:提权、进程连接与日志
第 2 篇|搭入口层:提权、进程连接与日志

系列:从 0 到 1 搭建桌面应用 UI 自动化测试框架 上一篇:第 1 篇|先盘点约束,再决定怎么做 下一篇:第 3 篇|跑通业务主链路 关键词:UAC、UIPI、AppSession、窗口反查 PID、Tee 本篇导读 第一道… · 2026/11/5 6:53:32

ADS122U04与STM32F410RB工业温度采集方案:UART接口ADC实战详解
ADS122U04与STM32F410RB工业温度采集方案:UART接口ADC实战详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 7:35:44

ADS122U04IPWR + PIC18F66K40:PT100/NTC高精度温度采集方案解析
ADS122U04IPWR + PIC18F66K40:PT100/NTC高精度温度采集方案解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 7:35:44

Figma设计稿转LVGL嵌入式UI:自动化链路与工程实践
Figma设计稿转LVGL嵌入式UI:自动化链路与工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 7:35:44

ADS122U04IPWR+PIC18F56K42实现0.1℃工业温度测量
ADS122U04IPWR+PIC18F56K42实现0.1℃工业温度测量

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 7:35:44

工业温度测量信号链设计:ADS122U04与PIC18F57Q43实战
工业温度测量信号链设计:ADS122U04与PIC18F57Q43实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 7:35:44

1km人口分布栅格数据处理:口径、裁剪与时间序列分析
1km人口分布栅格数据处理:口径、裁剪与时间序列分析

去年帮一位做城市扩张研究的朋友处理人口栅格,他拿到一份 2000—2023 年的 1km 精度人口分布栅格数据,第一件事就是把研究区内的像元值全部加总,想直接得到总人口曲线。结果算出来的数比公开统计口径少了将近三成,他一度以为是数据… · 2026/11/5 7:34:44

双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49

STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 6:59:29

DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 7:02:38

多智能体协作系统架构设计与实践优化
多智能体协作系统架构设计与实践优化

1. 多智能体协作系统的时代背景2017年Transformer架构的诞生彻底改变了人工智能的发展轨迹。当行业还沉浸在单一大模型带来的性能突破时,Anthropic的研究团队已经将目光投向了更前沿的领域——如何让多个智能体通过协作产生超越单体能力的系统智能。这就像交响乐团中… · 2026/11/5 0:00:39

大模型系统性入门实战指南:从原理到部署微调的全链路路线
大模型系统性入门实战指南:从原理到部署微调的全链路路线

提到大模型的系统性入门,我先说个结论:这事真不难,难的是资料太散、路线太多,今天这个框架明天那个工具,新人很容易在“收藏夹吃灰”和“教程看到一半放弃”之间反复横跳。市面上关于大模型的内容少说有几十万篇&#… · 2026/11/5 0:00:39

鸿蒙文件系统沙箱机制解析与开发实践
鸿蒙文件系统沙箱机制解析与开发实践

1. 鸿蒙文件系统沙箱机制深度解析作为一名从Android转向鸿蒙开发的工程师,我深刻体会到文件系统访问是迁移过程中最先遇到的"拦路虎"。在Android上运行良好的文件读写代码,在鸿蒙真机环境却频频出现文件写入后神秘消失或直接抛出权限拒绝错误的… · 2026/11/5 0:00:39

双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49

STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 6:59:29

DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 7:02:38

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码