Java JVM 性能调优学习手册

内容纲要

简介

本手册旨在系统介绍 Java 虚拟机(JVM)的性能调优技术,特别针对线上高并发服务和大数据/微服务系统的优化实践。内容涵盖 JVM 架构原理、常见调优目标与思路、各类垃圾收集器机制及应用场景、垃圾回收日志解析与调优、JVM 常用参数、实用诊断调优工具、性能瓶颈排查流程与案例,以及容器环境下的 JVM 调优建议等主题。手册采用教学式结构编排,从入门到精通,配有示例代码、命令用法、调优流程演示和适量练习题,帮助读者全面掌握 JVM 调优知识。


第1章 JVM 架构与工作原理

在开始讨论调优之前,首先需要了解 JVM 内部结构和运作机制。本章将介绍 JVM 的架构,包括类加载过程、运行时数据区、执行引擎(含即时编译JIT)和垃圾回收原理等内容。

1.1 类加载机制

Java 程序源代码编译为 .class 字节码后,由 JVM 的类加载子系统加载执行。类加载分为加载(Loading)链接(Linking)初始化(Initialization)三个阶段:

  • 加载:找到字节码文件并读取其内容到内存,生成 Class 对象。在 HotSpot VM 中,类加载由系统类加载器(Bootstrap)、扩展类加载器(Extension)和应用类加载器(Application)按需完成。
  • 链接:将已加载的类合并到 JVM 运行时环境,包括验证(Verify)、准备(Prepare)和解析(Resolve)步骤。验证确保字节码格式正确、不会危害虚拟机安全;准备阶段为类的静态字段分配内存并设初始零值;解析则将符号引用转换为直接引用(这一步可能在初始化前或延迟到使用时再做)。
  • 初始化:执行类构造器 <clinit> 方法,对静态变量赋予程序设定的初始值,并执行静态代码块。类的初始化按依赖顺序进行(先父类后子类)。

示例:下面是一段简单的代码演示类加载和初始化顺序:

class Parent {
    static { System.out.println("父类静态初始化"); }
}

class Child extends Parent {
    static { System.out.println("子类静态初始化"); }
}

public class ClassLoadDemo {
    public static void main(String[] args) {
        Child c = new Child();  // 首次引用Child类,触发加载Parent和Child并初始化
    }
}

运行此程序将首先打印“父类静态初始化”,然后打印“子类静态初始化”,说明 Parent 类在 Child 之前被加载初始化。

1.2 运行时数据区

JVM 在执行 Java 程序时会将数据划分到不同的内存区域,即运行时数据区。根据 JVM 规范,这些区域包括方法区、堆、Java 栈、本地方法栈、程序计数器等[docs.oracle.com](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#:~:text=2.5. Run)docs.oracle.com

  • 程序计数器(PC寄存器):每个线程私有,记录当前线程正在执行的字节码指令地址。如果线程正在执行本地方法,则此寄存器的值是未定义的[docs.oracle.com](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#:~:text=2.5.1. The )。
  • Java 虚拟机栈:每个线程私有,用于存放栈帧(每个方法对应一个栈帧),保存局部变量、操作数栈、中间结果等。在JVM栈中,每当方法被调用时会创建新的栈帧入栈,方法结束后栈帧出栈docs.oracle.com。若线程请求的栈深度超过最大限制,将抛出 StackOverflowErrordocs.oracle.com
  • 本地方法栈:与 JVM 栈类似,但为本地(native)方法执行服务(在 HotSpot中,本地方法栈和Java栈合二为一)。
  • 堆(Heap):所有线程共享的内存区域,用于存放对象实例和数组。在 JVM 启动时创建,可动态扩展或收缩。堆是垃圾收集的主要管理区域[docs.oracle.com](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#:~:text=The Java Virtual Machine has,instances and arrays is allocated)。当对象需要分配内存而堆已满且垃圾收集无法提供更多空间时,会抛出 OutOfMemoryError[docs.oracle.com](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#:~:text=The following exceptional condition is,associated with the heap)。
  • 方法区(Method Area):所有线程共享,用于存储类元数据、常量、静态变量、JIT 编译后的代码等docs.oracle.com。在 JDK8 以前,方法区也称为永久代(PermGen);在 JDK8+,改为元空间(Metaspace)并使用本地内存。方法区本质上也是堆的一部分或与堆分离,但很多实现中对方法区的垃圾回收不如堆频繁[docs.oracle.com](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#:~:text=The method area is created,not need to be contiguous)。如果方法区内存不足,可能抛出 OutOfMemoryError

以上各区中,堆和方法区是线程共享的,生命周期与 JVM 一致;程序计数器、虚拟机栈和本地方法栈则是线程私有的,在线程创建时分配,线程结束时销毁[docs.oracle.com](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#:~:text=The Java Virtual Machine defines,destroyed when the thread exits)。

1.3 执行引擎与即时编译(JIT)

JVM 执行引擎负责执行字节码指令。HotSpot JVM 采用解释器(Interpreter)+ 即时编译器(JIT)\的混合执行模式。初始时,字节码由解释器逐条解释执行;为提高性能,JIT编译器会将**热点代码**(频繁执行的方法或循环)编译成本地机器码,从而大幅提升后续执行速度[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/jrockit-hotspot/compilation-optimization.html#:~:text=HotSpot VM defaults to interpreting,either client or server compilers)[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/jrockit-hotspot/compilation-optimization.html#:~:text=The tiered compilation enhances the,The)。

HotSpot 实现了两种 JIT 编译器:

分层编译(Tiered Compilation)是 JDK默认启用的一项机制(JDK8及以后默认Server模式启用)[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/jrockit-hotspot/compilation-optimization.html#:~:text=Tiered compilation is the default,TieredCompilation flag)[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/jrockit-hotspot/compilation-optimization.html#:~:text=Tiered compilation is the default,TieredCompilation flag)。它将解释执行、C1和C2编译结合起来:HotSpot先用解释器收集Profiling信息,并可用C1将热点方法编译成本地代码以加速收集过程;随后若方法足够热,再由C2编译更高优化级别的代码替换之前的代码[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/jrockit-hotspot/compilation-optimization.html#:~:text=The tiered compilation enhances the,The)。通过这种分层方式,JVM在应用预热阶段即可获得一定的编译优化性能,同时最終仍能获得C2全面优化后的高性能代码[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/jrockit-hotspot/compilation-optimization.html#:~:text=compiled versions of methods that,profiling%2C which yields better optimization)。示例:默认情况下,64位Server VM会使用分层编译,如需强制关闭可添加参数 -XX:-TieredCompilation(一般不建议关闭,因为它能兼顾启动和优化)[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/jrockit-hotspot/compilation-optimization.html#:~:text=Tiered compilation is the default,TieredCompilation` flag)。

JIT 编译能带来数量级的性能提升,但也会带来代码缓存占用JVM停顿(安全点编译)等开销。HotSpot通过编译阈值控制JIT触发(如方法调用超过1万次则触发C2编译,具体阈值可通过 -XX:CompileThreshold 调整[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/jrockit-hotspot/compilation-optimization.html#:~:text=* `))。此外,JVM提供了一系列参数控制即时编译行为,如:

通过JIT,Java程序能接近甚至超过纯C/C++程序的性能,因为JIT可以在运行时利用实际参数和内存状况进行动态优化(如内联、逃逸分析等)。现代JVM(如HotSpot、OpenJ9等)还引入了分层JIT预编译AOT等技术。总之,即时编译器是JVM实现高性能的重要组件,使得“Java慢”成为历史。

1.4 垃圾回收原理概览

垃圾回收(Garbage Collection, GC)\是 Java 一大特性,它自动管理堆内存,对不再使用的对象进行回收释放,避免内存泄漏。JVM 并未规定使用哪种GC算法,HotSpot JVM采用**分代收集**思想:将堆划分为年轻代(Young Generation)和老年代(Old Generation),分别采用不同算法优化回收效率。

  • 分代假说:大多数对象生命周期短暂,应快速回收;少部分对象存活较久,且存活越久越难死亡。因此,将新生对象与长期存活对象分开管理可以提升GC效率。
  • 年轻代:存放新创建对象,垃圾回收频率高但每次只扫描少量存活对象(所谓“朝生夕死”)。采用复制算法:将存活对象复制到另一空间,清空原空间,一次GC即回收大量短命对象,速度快。年轻代又可细分 Eden 和 Survivor 区(Survivor通常有2个,交替充当复制的From/To空间)。
  • 老年代:存放经历多次GC仍存活的对象和大对象。老年代占堆的大部分容量,GC频率低但耗时长。常用标记-清除(Mark-Sweep)标记-整理(Mark-Compact)算法:先标记存活对象,再清除未标记的(或整理压缩碎片)。这些算法停顿时间较长,因此后来发展出并发标记、增量收集等机制减少停顿。

HotSpot JVM 根据不同场景提供了多种垃圾收集器(详见第3章),但其根本原理仍基于上述分代设计。一般来说,年轻代GC称为Minor GC,老年代GC称为Major GC,清理整个堆(包括年轻代和老年代)的称Full GC。GC发生时会暂停应用线程(Stop-The-World暂停),收集器通过安全点机制挂起所有应用线程再执行GC。降低GC停顿对应用性能至关重要,因此现代GC算法都在努力缩短暂停或者实现更多并发收集

本节只是概览,后续章节将详细介绍各收集器的机制与实现。在调优实践中,我们需要根据应用特性选择合适的GC算法,并合理调整相关参数,以在吞吐量延迟之间取得平衡(详见第2章)。

练习:思考下列问题:

  1. 为什么 JVM 采用分代收集而非对堆空间整体使用一种算法?
  2. 什么是 Stop-The-World 事件?在 GC 过程中为何需要暂停所有应用线程?
  3. 写一段代码验证对象进入老年代的条件(提示:可调整 Survivor 大小和晋升阈值,反复触发 Minor GC)。

第2章 常见 JVM 调优目标与思路

在不同应用场景下,JVM 调优可能有不同侧重面。本章讨论常见的性能调优目标以及相应的优化思路。

2.1 延迟 (Latency) 与吞吐量 (Throughput)

延迟指响应请求的时间延迟,强调单次操作的快速完成;吞吐量指单位时间内处理的任务数量,强调总体处理效率。这两者往往是权衡关系:

  • 面向低延迟的调优目标:希望缩短 GC 停顿、降低99%响应时间。例如在线交易、游戏服务器等对每次请求延迟敏感的系统,需要减少每次GC暂停时间,哪怕牺牲部分吞吐量。常用策略包括使用并发收集器(如 G1、ZGC、Shenandoah 等低暂停GC)和较小新生代(减少Minor GC暂停时长),甚至牺牲部分CPU空闲以换取更短停顿。
  • 面向高吞吐的调优目标:允许较长的偶尔停顿,但追求每秒处理尽可能多的事务或批量作业总量最大。例如批处理、大数据离线任务更注重吞吐。调优时可选择并行收集器(Parallel GC)以多线程提高GC效率,且倾向较大堆和新生代来减少GC频率,从而提高CPU利用率。此时偶尔的长停顿是可以接受的。

HotSpot 提供参数方便设置侧重方向。例如 -XX:MaxGCPauseMillis 期望最大停顿时间,-XX:GCTimeRatio 设定 GC 时间与应用运行时间比例。需要注意,这些都是软目标,JVM会尝试满足但不保证绝对满足。实际调优需根据监控数据反复试验。

2.2 内存占用与稳定性

另一个角度的调优目标包括内存占用系统稳定性

  • 内存占用:在内存资源有限或需要与其他进程共享资源时,应降低 JVM 内存消耗。一方面可通过减小堆大小(Xmx)限制最大占用,另一方面使用更紧凑的对象布局和开启压缩指针(64位JVM在heap <= 32GB时默认启用压缩OOP)。还可以使用一些参数控制元空间、栈空间大小(如 -XX:MaxMetaspaceSize 限制元数据内存,-Xss 设置每个线程栈大小等)。当然,减小内存也可能增加GC频率,需要权衡。
  • 稳定性:指 JVM 长时间运行的健壮性,避免频繁Full GC、内存泄漏、频繁 YGC等导致性能抖动甚至 OOM。调优时注重避免内存碎片(使用带压缩的收集器,如 G1、Parallel Old 等,CMS因不压缩老年代可能碎片过多导致 Full GC)、及时发现和解决内存泄漏(借助监控和分析工具),以及容错性(例如配置 -XX:+ExitOnOutOfMemoryError 在发生OOM时自动重启JVM)。

2.3 不同应用场景下的侧重

综合考虑,针对不同类型应用,可以有不同调优侧重点:

  • 高并发低延迟服务(如Web微服务、金融交易系统):延迟优先,GC算法宜选 G1 或 ZGC/Shenandoah 等低停顿GC。合理设置 -XX:MaxGCPauseMillis(如目标低于50ms甚至更低)。同时注意 young GC 频率和老年代并发收集调优,使99%响应延迟在可接受范围。
  • 批处理/大数据作业(如Hadoop/Spark驱动的离线任务):吞吐优先。可选 Parallel GC 获取最大吞吐率,容忍偶尔秒级停顿。提升新生代大小减少 Minor GC 次数,提高 GCTimeRatio 使GC占比更低。
  • 长生命周期服务器(如应用服务器、网关):注重稳定性和内存管理。选择成熟稳定GC(G1 默认即可)[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/gctuning/hotspot-virtual-machine-garbage-collection-tuning-guide.pdf#:~:text=• If ,then select a mostly concurrent)并监控 Full GC 迹象,避免内存泄漏。可能需要设置保守的堆上限,确保长时间运行不过度使用系统内存。
  • 嵌入式或小内存应用:内存有限,可能倾向 Serial GC 因为它占用内存小,实现简单[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/gctuning/hotspot-virtual-machine-garbage-collection-tuning-guide.pdf#:~:text=It's best,XX%3A%2BUseSerialGC)。同时精细控制堆和栈以适应小内存环境。

总之,明确调优目标是第一步。通常建议先让JVM自适应运行,观察性能瓶颈再决定调整GC算法或参数[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/gctuning/hotspot-virtual-machine-garbage-collection-tuning-guide.pdf#:~:text=Unless your application has rather,then select the serial)[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/gctuning/hotspot-virtual-machine-garbage-collection-tuning-guide.pdf#:~:text=time requirements%2C then select the,XX%3A%2BUseSerialGC)。除非有充分依据,否则不宜一开始就过多手工调整,因为 JVM 自带的内存管理具有大量内置优化。下面章节我们将具体分析各种GC收集器和参数,为不同目标的调优提供依据。


第3章 JVM 垃圾收集器机制与应用场景

HotSpot JVM 提供了多种垃圾收集器,每种都有不同的工作机制和适用场景。本章逐一介绍主要的垃圾收集器,包括 Serial、Parallel、CMS、G1、ZGC、Shenandoah 等,它们在不同版本JDK中的可用性也略有不同(JDK8~21的变化将在第10章讨论)。

3.1 Serial 收集器

Serial GC 是最基础的垃圾收集器,使用单线程进行垃圾收集工作。它在进行 Minor GC 和 Major GC 时都会暂停所有应用线程(Stop-The-World),依次使用复制算法(新生代)和标记-整理算法(老年代)回收[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/gctuning/hotspot-virtual-machine-garbage-collection-tuning-guide.pdf#:~:text=Serial Collector The serial collector,no communication overhead between threads)[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/gctuning/hotspot-virtual-machine-garbage-collection-tuning-guide.pdf#:~:text=hardware and operating system configurations%2C,used to speed up garbage)。由于只有一个线程,无线程协调开销,Serial GC 实现简单、内存开销小,在小堆内存场景下相当高效[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/gctuning/hotspot-virtual-machine-garbage-collection-tuning-guide.pdf#:~:text=Serial Collector The serial collector,no communication overhead between threads)。

应用场景:Serial 收集器适合单核处理器或小内存环境,以及对暂停时间不敏感的小应用。例如 GUI 桌面应用(Client模式)默认就使用 Serial GC,因为堆通常不大且单线程GC足够[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/gctuning/hotspot-virtual-machine-garbage-collection-tuning-guide.pdf#:~:text=It's best,XX%3A%2BUseSerialGC)。官方建议当应用数据集较小(比如堆<=100MB)时,可考虑Serial GC[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/gctuning/hotspot-virtual-machine-garbage-collection-tuning-guide.pdf#:~:text=It's best,XX%3A%2BUseSerialGC)。在现代多核服务器上,Serial GC因无法利用并行而性能欠佳,通常仅作为特定用途。可以通过参数 -XX:+UseSerialGC 强制使用 Serial 收集器[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/gctuning/hotspot-virtual-machine-garbage-collection-tuning-guide.pdf#:~:text=small data sets ,XX%3A%2BUseSerialGC)。

3.2 Parallel 收集器

Parallel GC (又称“吞吐量优先收集器”)在Serial的基础上使用多线程并行进行垃圾收集,以缩短停顿时间、提高总体吞吐量[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/gctuning/hotspot-virtual-machine-garbage-collection-tuning-guide.pdf#:~:text=Parallel Collector The parallel collector,sized data sets)[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/gctuning/hotspot-virtual-machine-garbage-collection-tuning-guide.pdf#:~:text=to the serial collector,XX%3A%2BUseParallelGC option)。年轻代采用Parallel Scavenge算法,多线程复制回收;老年代在JDK1.5引入并行压缩(Parallel Old),通过 -XX:+UseParallelOldGC 开启,在JDK7之后Parallel Old默认开启使Parallel GC成为完全并行的收集器[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/gctuning/hotspot-virtual-machine-garbage-collection-tuning-guide.pdf#:~:text=XX%3A%2BUseParallelGC option,UseParallelOldGC option)。

应用场景:Parallel GC 适合多核服务器、批处理等对吞吐量要求高而对响应延迟要求不高的场景[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/gctuning/hotspot-virtual-machine-garbage-collection-tuning-guide.pdf#:~:text=the parallel collector has multiple,the parallel collector to perform)。例如科学计算、ETL作业等。Parallel GC 会尽量利用所有CPU核心提高回收效率,其目标是最大化应用运行时间占比,典型参数 -XX:GCTimeRatio(默认值99意味允许1%时间用于GC)。当需要尽可能高的处理能力且能容忍偶尔较长停顿时,可采用Parallel GC[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/gctuning/hotspot-virtual-machine-garbage-collection-tuning-guide.pdf#:~:text=• If ,then select a mostly concurrent)。可以使用 -XX:+UseParallelGC 启用Parallel收集器[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/gctuning/hotspot-virtual-machine-garbage-collection-tuning-guide.pdf#:~:text=The parallel collector is intended,it by using the)。需要注意在多CPU机器上Parallel GC几乎总能胜过Serial GC,因为在2核上就已略胜,在更多核环境下性能优势显著[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/gctuning/hotspot-virtual-machine-garbage-collection-tuning-guide.pdf#:~:text=it generally outperforms the serial,serial collector when more than)。

3.3 CMS 收集器

CMS(Concurrent Mark-Sweep)\是早期的**低延迟垃圾收集器,其目标是减少老年代回收的停顿时间。CMS在老年代采用并发标记-清除算法,尽可能与应用线程同时工作,只在初始标记和最终重新标记阶段暂停应用线程,因而属于“Mostly Concurrent**”收集器(大部分操作并发)[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/gctuning/hotspot-virtual-machine-garbage-collection-tuning-guide.pdf#:~:text=be explicitly enabled using,ZGC performs all)。年轻代通常与ParNew(Parallel Scavenge的多线程新生代版本)搭配。

CMS 优点是在老年代回收时停顿较短,适用于需要较短GC暂停的应用[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/gctuning/hotspot-virtual-machine-garbage-collection-tuning-guide.pdf#:~:text=be explicitly enabled using,ZGC performs all)。然而缺点是:

  • 无法整理压缩:CMS仅清除不存活对象,不压缩内存,因此会产生内存碎片。当碎片过多导致大对象无法分配时会触发Full GC来Serial Old方式压缩整个堆,导致长暂停。
  • 并发开销:CMS在并发回收时会占用部分CPU资源。如果CPU不足或老年代存活率高,CMS可能“追不上”分配速度而触发concurrent mode failure,进而退化成Serial Full GC,出现长暂停。

应用场景:CMS曾广泛用于对延迟敏感的服务器,如Web服务器、电商网站等。在 G1 出现之前,CMS 是低暂停GC的主要选择。使用CMS需有相对充裕的CPU(以便GC线程并发工作),并需要调优如 -XX:CMSInitiatingOccupancyFraction(触发CMS的老年代使用率阈值)和 -XX:+UseCMSCompactAtFullCollection(在Full GC时压缩碎片)等。CMS通过 -XX:+UseConcMarkSweepGC 启用[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/gctuning/hotspot-virtual-machine-garbage-collection-tuning-guide.pdf#:~:text=collection pauses and can afford,deprecated as of JDK 9)。值得注意的是CMS在JDK9被标记为已弃用,并在JDK14被移除,因此在JDK14+上无法再使用CMS[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/gctuning/hotspot-virtual-machine-garbage-collection-tuning-guide.pdf#:~:text=pauses and can afford to,less than 10 ms)。

3.4 G1 收集器

Garbage-First (G1) GC 是面向服务端应用的低暂停收集器,引入于 JDK7u4并在JDK9成为默认收集器[kstefanj.github.io](https://kstefanj.github.io/2023/12/13/jdk-21-the-gcs-keep-getting-better.html#:~:text=* G1 ,millisecond pauses)。G1 将堆划分为多个大小相等的区域(Region),不再严格区分固定的年轻代和老年代,而是动态划分一部分区域作为 Eden/Survivor,其余为老年代。G1 的回收过程包含以下关键机制:

应用场景:G1 适合具有大内存且需要均衡延迟和吞吐的服务器应用,被设计为CMS的替代品[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/gctuning/hotspot-virtual-machine-garbage-collection-tuning-guide.pdf#:~:text=• Predictable pause,1)。官方描述G1适用于堆容量几十GB甚至更大、对象分配速率波动大以及碎片问题严重的场景[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/gctuning/hotspot-virtual-machine-garbage-collection-tuning-guide.pdf#:~:text=applications and environments whose features,longer than a few hundred)。G1默认目标停顿时间为不超过几百毫秒[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/gctuning/hotspot-virtual-machine-garbage-collection-tuning-guide.pdf#:~:text=live data,It is also the default)。相较CMS,G1不需要过多复杂调优即可获得良好性能,因而在JDK9后成为默认GC[kstefanj.github.io](https://kstefanj.github.io/2023/12/13/jdk-21-the-gcs-keep-getting-better.html#:~:text=* G1 ,millisecond pauses)。对于绝大多数服务端应用,G1 是首选收集器。启用G1在JDK8需指定 -XX:+UseG1GC(JDK9+默认即G1)。需要注意G1仍可能在极端情况下触发Full GC(如并发回收来不及、晋升失败等),但这种情况可通过增大堆或调整参数减少[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/gctuning/hotspot-virtual-machine-garbage-collection-tuning-guide.pdf#:~:text=A full heap garbage collection,finding the words Pause Full)[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/gctuning/hotspot-virtual-machine-garbage-collection-tuning-guide.pdf#:~:text=match at L2263 time to,the way these objects are)。

G1优势:实现了区域化的压缩收集,避免碎片;并发标记降低停顿;还能通过设置暂停时间目标自动调整行为。

3.5 ZGC 收集器

ZGC(Z Garbage Collector)\是JDK11引入的一款**可扩展的超低延迟收集器。ZGC的设计目标是在任意大小堆内存下都能将停顿时间控制在<10ms范围内[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/gctuning/hotspot-virtual-machine-garbage-collection-tuning-guide.pdf#:~:text=The Z Garbage Collector ,enable is by using the)[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/gctuning/hotspot-virtual-machine-garbage-collection-tuning-guide.pdf#:~:text=expensive work concurrently%2C without stopping,feature%2C starting with JDK 11)。为此,ZGC 做到了几乎所有耗时操作都并发化**,包括对象移动也在应用线程运行时完成,从而实现近乎无停顿的回收。

ZGC 的关键技术包括:

  • 着色指针(Colored Pointers):ZGC 在64位对象引用的高位嵌入元数据位,用于标记对象是否被移动等状态。通过指针着色和读屏障技术,ZGC可在应用运行时安全地移动对象,并将引用重定向到新位置,实现真正并发压缩(类似CPU内存模型下的读写屏障)。
  • 分区内存:将堆划分为若干区域(Region),大小可调(比如32MB或更大,以支持TB级堆)。ZGC每次只处理一部分区域,避免扫描全堆。
  • 并发标记和并发转移:ZGC的标记、对象转移、引用更新等过程几乎全部并发执行,只有很短暂的初始和最终标记暂停(典型仅几毫秒)。

应用场景:ZGC 适合超大堆内存(数百GB到TB级)应用,或对停顿时间极其敏感的场景,如电信交换系统、大型内存缓存等[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/gctuning/hotspot-virtual-machine-garbage-collection-tuning-guide.pdf#:~:text=expensive work concurrently%2C without stopping,time requirements%2C first run)。ZGC 能在这些场景下保持应用线程几乎不中断地运行。ZGC在JDK11作为实验性特性发布,需要开启 -XX:+UnlockExperimentalVMOptions -XX:+UseZGC;在JDK15提升为产品级(不再需要Unlock)。目前ZGC在JDK17+非常成熟,Netflix等公司甚至在考虑使用ZGC替代G1以进一步降低延迟[netflixtechblog.com](https://netflixtechblog.com/bending-pause-times-to-your-will-with-generational-zgc-256629c9386b#:~:text=Bending pause times to your,benefits of concurrent garbage collection)。不过ZGC相对传统GC需要更多内存(空间换时间)以及一些平台限制(只支持64位系统)。ZGC适合追求<10ms级别停顿和非常大内存的场景[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/gctuning/hotspot-virtual-machine-garbage-collection-tuning-guide.pdf#:~:text=ZGC is intended for applications,VM to select a collector)。

3.6 Shenandoah 收集器

Shenandoah 是由 Red Hat 开发的另一个低延迟GC算法,最初在 OpenJDK12 引入(JEP189),JDK15起成为正式可用。Shenandoah 的目标与ZGC类似,也是将GC停顿时间降至极低(<10ms),即使在大堆下也是如此[developers.redhat.com](https://developers.redhat.com/articles/2024/05/28/beginners-guide-shenandoah-garbage-collector#:~:text=fragmentation issues. ,ready)[developers.redhat.com](https://developers.redhat.com/articles/2024/05/28/beginners-guide-shenandoah-garbage-collector#:~:text=Shenandoah's original goal was to,product supported in production environments)。Shenandoah的主要特点:

应用场景:Shenandoah主要面向要求响应时间极低的大内存应用,与ZGC类似。不过ZGC由Oracle主导,Shenandoah由Red Hat推动,在OpenJDK各版本中都可用(Red Hat还将其 backport 到 JDK8/11的自家版本中[developers.redhat.com](https://developers.redhat.com/articles/2024/05/28/beginners-guide-shenandoah-garbage-collector#:~:text=concurrent phases%2C where the traversal phase,ready))。在选择上,如果使用 OpenJDK8/11 的某些发行版可能Shenandoah是可选项。JDK17+的OpenJDK里Shenandoah也已集成(Oracle JDK17也包含Shenandoah)。启用方法是 -XX:+UseShenandoahGC。对于追求亚毫秒级延迟且愿意牺牲部分吞吐的应用,Shenandoah是值得尝试的GC方案[developers.redhat.com](https://developers.redhat.com/articles/2024/05/28/beginners-guide-shenandoah-garbage-collector#:~:text=on the application in terms,product supported in production environments)。

Shenandoah vs ZGC:两者理念相似,均实现并发压缩,停顿很短。实现细节有所不同,比如ZGC使用指针着色而Shenandoah用指针转发。ZGC对极大堆(TB级)优化更好,而Shenandoah起初堆上限约2TB,在JDK17改进后也足够大多数场景。随着JDK21引入Generational ZGC预览,ZGC在低延迟同时也获得代际优势,而Shenandoah也在开发 generational 版本[developers.redhat.com](https://developers.redhat.com/articles/2024/05/28/beginners-guide-shenandoah-garbage-collector#:~:text=on the application in terms,product supported in production environments)。实际选择时,可根据JDK版本、对停顿和吞吐的要求、以及具体测试效果决定使用哪种。

小结:对于大多数应用,如果GC停顿并非主要瓶颈,使用默认的 G1 已能提供很好平衡;若对延迟要求更高,可以尝试 ZGC 或 Shenandoah。对于内存很小或极端局限场景,Serial 仍有用武之地;而注重吞吐的批处理场景,Parallel GC 是简单有效的选择。下面的表格简要总结各收集器特点:

收集器 线程模式 停顿性质 适用场景
Serial 单线程 Stop-the-world,全停顿 小型应用,单核或客户端模式
Parallel 多线程 Stop-the-world,停顿较长 高吞吐批处理,多核服务器
CMS 多线程 部分并发,停顿较短 低延迟服务(JDK14前),已淘汰
G1 多线程 部分并发,停顿可控 通用服务器应用,JDK9+默认
ZGC 多线程 并发,停顿极短 超低延迟+超大内存场景
Shenandoah 多线程 并发,停顿极短 超低延迟场景(RedHat主导)

练习:阅读 GC 日志片段,判断使用了哪种收集器,以及对应GC事件含义(日志实例见第4章)。尝试在相同程序上分别使用Parallel、G1、ZGC运行并比较吞吐量和最大停顿时间。


第4章 垃圾收集日志解析与调优实践

开启 GC 日志可以帮助我们了解垃圾收集行为,对定位性能问题和验证调优效果非常重要。本章将介绍如何启用和读取GC日志,以及根据日志信息进行调优的思路。

4.1 启用 GC 日志

在 JDK8 及之前,可通过如下 JVM 参数打开垃圾回收日志:

  • -XX:+PrintGCDetails:打印详细GC日志。
  • -XX:+PrintGCDateStamps:在日志中加入日期时间戳。
  • -Xloggc:<filename>:将 GC 日志输出到指定文件而不是控制台。

例如:

java -Xmx4g -XX:+UseG1GC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log MyApp

上述参数将使用G1收集器并将GC详细日志写入gc.log文件。

在 JDK9 及之后,引入了统一日志子系统,推荐使用 -Xlog 参数:

  • 基本 GC日志:-Xlog:gc*(等价于PrintGCDetails),例如 -Xlog:gc=info:file=gc.log:time 将GC信息级别日志带时间戳输出到文件。
  • 细粒度控制:如 -Xlog:gc+heap=debug 查看堆调整等细节;-Xlog:gc+cpu=info 查看GC线程CPU占用等。

注意:在生产环境开启过高详级别的GC日志可能影响性能,一般 info 级别足够。若排查问题需要,可临时提高详级别。

4.2 读取 GC 日志示例

以下是一段 G1 垃圾收集日志片段示例及解释:

[10.178s][info ][gc,start    ] GC(36) Pause Young (G1 Evacuation Pause)
[10.178s][info ][gc,task     ] GC(36) Using 28 workers of 28 for evacuation
[10.191s][info ][gc,phases   ] GC(36) Pre Evacuate Collection Set: 0.0ms
[10.191s][info ][gc,phases   ] GC(36) Evacuate Collection Set: 6.9ms
[10.191s][info ][gc          ] GC(36) Pause Young (G1 Evacuation Pause) 391M->254M(512M) 7.1ms

逐行解读:

  • GC(36):这是第36次 GC 的标识。后面跟随的是事件类型和阶段信息。
  • 第一行表示一次 Young GC 开始,类型为 "G1 Evacuation Pause"(G1 新生代转移暂停)。时间戳10.178秒。
  • 第二行显示GC使用了28个工作线程并行拷贝对象(因为本机有28核)。
  • 第三、四行是GC各阶段耗时,其中Evacuate Collection Set(真正对象复制阶段)耗时6.9ms。
  • 第五行总结这次GC:从391MB降到254MB,堆总容量512MB,用时7.1ms。

可以看出,这次年轻代GC暂停了应用线程约7.1毫秒,回收了约137MB内存。

再看一段 Full GC 日志示例(使用CMS或Serial Old时可能出现):

[GC (Allocation Failure) 1048576K->524288K(1536000K), 0.2506780 secs] 
[Full GC (Allocation Failure) 1300000K->900000K(1536000K), 1.2345678 secs]
  • 第一行 GC (Allocation Failure) 表示一次正常的年轻代GC(因为发生了内存分配失败触发),耗时0.25秒,堆使用从1G降到512M。
  • 第二行 Full GC (Allocation Failure) 表示由于分配失败触发了一次Full GC,耗时1.234秒,堆使用从约1.3G降到900M左右。Full GC通常时间长,这是需要重点关注的信号。

如果GC日志频繁出现 Full GC,尤其伴随 (Allocation Failure)promotion failed 等字样,则说明内存回收压力大且出现了老年代不足的情况[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/gctuning/hotspot-virtual-machine-garbage-collection-tuning-guide.pdf#:~:text=A full heap garbage collection,finding the words Pause Full)[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/gctuning/hotspot-virtual-machine-garbage-collection-tuning-guide.pdf#:~:text=(Allocation Failure) in the log,the application allocates too many)。这往往需要调整堆大小或使用不同GC算法。比如上例Full GC前可能有 "to-space exhausted"(G1日志)或 "concurrent mode failure"(CMS日志)提示,表示年轻代提升或CMS并发回收失败而退化成Full GC[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/gctuning/hotspot-virtual-machine-garbage-collection-tuning-guide.pdf#:~:text=A full heap garbage collection,finding the words Pause Full)[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/gctuning/hotspot-virtual-machine-garbage-collection-tuning-guide.pdf#:~:text=match at L2263 time to,the way these objects are)。

4.3 调优实践:根据日志优化

通过GC日志,可以获取许多有价值的信息用于调优决策:

案例:假设通过日志发现应用每5秒就发生一次Full GC,每次停顿2秒,严重影响响应。日志提示原因是 (Allocation Failure),且Full GC前有 "to-space exhausted"。这表明堆太小或晋升失败。调优步骤可能是:

  1. 增大堆:如从4G提高到6G,使老年代有更多空间,降低Full GC频率。
  2. 检查晋升和新生代:可能新生代过小导致对象过早晋升填满老年代,可适当增大新生代或调整 SurvivorRatio/MaxTenuringThreshold。
  3. 观察结果:调整后再收集日志,若Full GC减少且停顿降到可接受范围,则调优成功;否则可能考虑使用G1收集器取代Parallel/CMS,以自动区域化回收碎片。

4.4 GC日志分析工具

人工分析GC日志虽可行,但对复杂日志和长时间运行的数据处理不便。可以借助一些工具:

  • GCViewer:一个开源的GUI工具,加载GC日志文件后可可视化指标如吞吐、暂停时间分布等。
  • GCeasy:在线GC日志分析工具,将日志上传可以生成报告和调优建议。
  • JDK自带JFR:Java Flight Recorder可记录GC相关事件,用JMC分析内存和GC行为(第6章介绍)。
  • IBM GC Analyzer 等针对特定JVM日志的工具。

这些工具能自动解析日志并给出直观图表和潜在问题提示,对于不熟悉日志格式的调优工程师很有帮助。不过,无论如何,理解GC日志内容依然是调优人员应具备的技能,毕竟工具的结论也需要人工判断和验证。

练习:获取你自己应用的一份GC日志(可通过增加JVM参数生成),试着不借助工具来分析下面几个问题:

  • 应用运行X分钟内发生了多少次Minor GC和Full GC?
  • 最大停顿时间是多少?平均停顿又是多少?
  • 垃圾回收后老年代占用率趋势如何,是否在上升接近100%?
  • 根据日志信息,你认为当前GC策略是否需要调整,为什么?

通过实践分析GC日志,你将对应用的内存动态有更深入的了解,为下一步参数调整提供依据。


第5章 JVM 常用参数详解

JVM 提供了丰富的启动参数来自定义内存大小、垃圾收集器选择、JIT行为等。合理配置参数对性能调优至关重要。本章列出常用的JVM参数并进行解释,包括 heap大小设置、GC算法开关及细化调整参数等。

5.1 堆内存大小相关参数

  • -Xmx<size>:最大堆内存。指定JVM堆的最大大小。例如 -Xmx8g 表示最大堆为8GB。确保此值不超过机器物理内存并留有余量。太小会导致频繁GC甚至OOM,太大可能导致Full GC停顿变长(对G1/ZGC影响小一些)。可结合GC日志观察使用率来调整。
  • -Xms<size>:初始堆内存。JVM启动时分配的堆容量。通常调优中将 -Xms 设为等于 -Xmx,避免运行过程中动态扩展堆带来的额外开销。尤其在需要稳定性能时(堆扩容也会触发Full GC)。
  • -Xmn<size>:年轻代大小(仅对分代GC如Parallel/Serial/CMS适用)。也可用 -XX:NewSize-XX:MaxNewSize 控制。年轻代大,可减少Minor GC频率但每次STW耗时略增;年轻代小,Minor GC频繁但停顿短。需要结合应用对象生命周期测试。对G1/ZGC没有此直接参数,但G1可通过 -XX:NewRatio(老年代与新生代容量比)间接影响其Eden划分。
  • -XX:SurvivorRatio=<N>:Eden与Survivor区大小比值。例如8表示 Eden:每个Survivor = 8:1:1(两个Survivor各占1)。调整此比可以改变对象晋升策略。当Survivor过小导致对象很快晋升老年代时,可适当提高Survivor比例。
  • -XX:MaxTenuringThreshold=<N>:对象在年轻代经历Minor GC的次数阈值。超过此次数则晋升老年代。默认15(Parallel/CMS),G1自适应算年龄。调小该值会让对象更早晋升,可能减少Survivor压力但增加老年代占用;调大则对象更长留在年轻代,有助于短命对象被回收,但设置过大意义不大因为默认15已是最大值(一般JVM会将长期存活对象年龄提升到阈值上限)。
  • -XX:MetaspaceSize / -XX:MaxMetaspaceSize:JDK8+用于控制方法区元空间初始值和最大值。默认MaxMetaspaceSize无限制(受限于系统内存),如果类加载很多可能需要手动限制以防元空间耗尽系统内存。可以通过观察 Full GC 日志里是否有 Metaspace OOM 来调整。
  • -Xss<size>:每个线程栈大小。默认一般1MB(64位JDK)。如果应用很多线程且栈不深,可适当调小以降低内存占用;反之如果需要深递归而StackOverflowError,可调大些。要注意操作系统对单进程映射内存的限制以及过多线程总栈大小占用。

示例:下面是使用 jmap -heap 查看当前堆配置的输出节选,可帮助理解这些参数[docs.oracle.com](https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/tooldescr014.html#:~:text=Heap Configuration%3A MinHeapFreeRatio %3D 40,%3D 8)[docs.oracle.com](https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/tooldescr014.html#:~:text=PermSize ,0MB):

Heap Configuration:
   MinHeapFreeRatio = 40
   MaxHeapFreeRatio = 70
   MaxHeapSize      = 67108864 (64.0MB)
   NewSize          = 2228224 (2.125MB)
   MaxNewSize       = 4294901760 (4095.9375MB)
   OldSize          = 4194304 (4.0MB)
   NewRatio         = 8
   SurvivorRatio    = 8
   PermSize         = 12582912 (12.0MB)
   MaxPermSize      = 67108864 (64.0MB)

上例(JDK8)显示:

  • 最大堆64MB(MaxHeapSize)。
  • 新生代与老年代比 NewRatio=8,即年轻代约堆的1/9。
  • SurvivorRatio=8,意味着Eden:Survivor = 8:1:1。
  • PermSize/MaxPermSize为方法区永久代大小(仅JDK8前有意义,JDK8之后Perm被Metaspace取代)。

还有MinHeapFreeRatio/MaxHeapFreeRatio,它们决定JVM自动收缩/扩大堆的阈值,可保持堆中一定比例空闲。一般不必调整这些比率。

5.2 垃圾收集器选择参数

  • -XX:+UseSerialGC:使用串行收集器(新生代 Serial + 老年代 Serial Old)。适合小应用和单核环境[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/gctuning/hotspot-virtual-machine-garbage-collection-tuning-guide.pdf#:~:text=small data sets ,XX%3A%2BUseSerialGC)。
  • -XX:+UseParallelGC:使用吞吐量优先的并行收集器(新生代 Parallel Scavenge,老年代 Parallel Old)。这是JDK8默认服务器模式GC[javabetter.cn](https://javabetter.cn/jvm/garbage-collector.html#:~:text=Java 经典垃圾回收器详解 ,XX%3A%2BUseG1GC 说明JDK 9默认的垃圾回收器为G1。 经典垃圾回收器介绍)。JDK8下若开启ParallelGC,同时默认启用ParallelOldGC,可用 -XX:-UseParallelOldGC 禁用老年代并行(不推荐禁用)。
  • -XX:+UseConcMarkSweepGC:使用CMS收集器(新生代需配ParNew)。JDK9起已Deprecated[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/gctuning/hotspot-virtual-machine-garbage-collection-tuning-guide.pdf#:~:text=collection pauses and can afford,deprecated as of JDK 9),JDK14起无效。
  • -XX:+UseG1GC:使用G1收集器。JDK9+默认收集器[kstefanj.github.io](https://kstefanj.github.io/2023/12/13/jdk-21-the-gcs-keep-getting-better.html#:~:text=* G1 ,millisecond pauses)。
  • -XX:+UseZGC:使用Z垃圾收集器。JDK11需UnlockExperimental,JDK15+可直接使用。仅64位Linux/Windows/macOS支持。
  • -XX:+UseShenandoahGC:使用Shenandoah收集器。需基于支持Shenandoah的JDK(OpenJDK12+整合,Oracle JDK17+亦有)。使用前最好 -XX:+UnlockExperimentalVMOptions 在部分版本中。
  • -XX:+UseParallelOldGC:在ParallelGC下启用老年代并行压缩(默认为开启在Java7u4+)。这里列出是因为在少数场景下可以手动控制,比如希望Young用Parallel而老年代用Serial可关闭它。
  • -XX:+UseAdaptiveSizePolicy:Parallel收集器的自适应调优开关(默认开启)。若关闭,则NewRatio等需手工精调,一般保持开启。

GC选择策略:在大多数现代Java应用中,优先使用G1,因为它在延迟和吞吐上较均衡[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/gctuning/hotspot-virtual-machine-garbage-collection-tuning-guide.pdf#:~:text=avoiding long garbage collection pauses,1)且为JDK9+默认。如果对停顿<10ms要求,可尝试ZGC或Shenandoah,但需JDK版本支持和充分测试。只有在特殊情况下才使用Serial或Parallel,如嵌入式或批处理。下面几条来自Oracle官方的建议可供参考[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/gctuning/hotspot-virtual-machine-garbage-collection-tuning-guide.pdf#:~:text=a collector%3A • If the,XX%3A%2BUseSerialGC)[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/gctuning/hotspot-virtual-machine-garbage-collection-tuning-guide.pdf#:~:text=collector or select the parallel,XX%3AUseZGC):

5.3 各收集器调优参数

对于Parallel GC

  • -XX:ParallelGCThreads=<N>:GC线程数,默认与CPU核心数相关(Server模式下通常N≈cores)。可以限制它避免GC压垮CPU或调整以配合容器CPU配额(第9章讨论)。
  • -XX:GCTimeRatio=<N>:吞吐量目标,99表示GC时间占1%,即99:1的应用时间:GC时间比。[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/gctuning/hotspot-virtual-machine-garbage-collection-tuning-guide.pdf#:~:text=• If ,then select a mostly concurrent)值越大表示越追求应用吞吐(GC时间比例低)。Parallel GC会根据此比率调整年轻代大小等。
  • -XX:MaxGCPauseMillis=<ms>:Max pause目标,Parallel GC也会尝试满足此目标调整年轻代(不过Parallel主要优化吞吐,对pause控制不如G1灵敏)。

对于CMS(已过时,这里主要列出以理解老应用参数):

  • -XX:CMSInitiatingOccupancyFraction=<%>:老年代使用率达到该百分比时开始并发回收(默认典型值68%左右)。调低此值可更早启动CMS,减少并发失败风险,但也会增加GC频率。
  • -XX:+UseCMSInitiatingOccupancyOnly:禁用自适应阈值,只使用上述手工设定比例。
  • -XX:+CMSParallelRemarkEnabled:让CMS remark阶段也并行,加快停止时间。
  • -XX:CMSFullGCsBeforeCompaction=<N>:CMS默认不压缩老年代,设置每N次CMS后跟一次Serial Old压缩,以减少碎片(但会有长停顿)。
  • -XX:+CMSClassUnloadingEnabled:允许CMS周期回收掉无用的类元数据(默认开启的,禁用会导致Metaspace增长)。
  • -XX:+CMSScavengeBeforeRemark:在CMS remark前先触发一次年轻代GC,降低老年代引用压力,提高remark效率。

对于G1

  • -XX:MaxGCPauseMillis=<ms>:G1的设计目标停顿时间,默认200ms。可根据需求调低如50ms,但过低可能导致G1收集不到足够垃圾,反而频繁Mixed GC,需结合监控调整。
  • -XX:InitiatingHeapOccupancyPercent=<%>:整堆占用多少触发并发标记(默认45%)。调低会更早标记回收老年代,减少并发时长但增加频率。一般默认即可。
  • -XX:G1NewSizePercent / G1MaxNewSizePercent:G1年轻代最小/最大占整个堆的百分比(默认5%和60%)。如果应用年轻代需求大,可增大MaxNewSizePercent或固定年轻代大小范围。
  • -XX:G1ReservePercent:保留内存百分比避免年轻代过满(默认10%)。可适当调高以降低to-space exhausted风险。
  • -XX:+G1MixedGCLiveThresholdPercent:参与Mixed GC的老年代region存活率阈值,低于则认为含足够垃圾回收之划算。改变此值影响Mixed GC回收范围。
  • -XX:ConcGCThreads:并发GC线程数(默认与ParallelGCThreads相关联),可调节以控制并发标记对应用影响。

对于ZGC

  • 几乎无需手工调优。ZGC没有年轻代/老年代之分,也没有Pause target,因为停顿本就极低。可调的有并发线程数量(ConcGCThreads)及一些实验参数如 -XX:ZUncommitDelay=<s> 控制空闲内存多久返还操作系统等。
  • -XX:SoftMaxHeapSize=<size>:软最大堆大小,ZGC会尝试将堆收缩到这个大小以下[kstefanj.github.io](https://kstefanj.github.io/2023/12/13/jdk-21-the-gcs-keep-getting-better.html#:~:text=The second thing to highlight,the application improving its performance)。设置这个可在使用过峰值后释放内存给系统。

对于Shenandoah

  • 类似ZGC,大部分是自动的。可调整并发线程数(与ConcGCThreads同),-XX:ShenandoahGarbageThreshold=<%> 控制并发启动阈值。
  • -XX:+ShenandoahSATBBarrier 等内部开关通常不修改。
  • Shenandoah也有 ShenandoahUncommitDelay 类似参数释放空闲内存,可用于弹性场景。

其他实用参数

  • -XX:+PrintFlagsFinal:打印所有参数的最终值(包括默认或修改后的),可用于确认调优参数是否生效以及JVM默认值配置。
  • -XX:+PrintCommandLineFlags:打印显式或隐式开启的XX参数(JDK8 时有用)。
  • -XX:+HeapDumpOnOutOfMemoryError:发生OOM时自动生成heap dump文件,供事后分析内存问题。
  • -XX:HeapDumpPath=<file>:配合上者指定dump文件路径。
  • -XX:+UseCompressedOops:64位压缩普通对象指针(默认启用在heap < 32GB时)。一般不关闭,除非特别需要关闭压缩指针检查大堆优化效果。
  • -XX:ReservedCodeCacheSize=<size>:JIT代码缓存大小,默认240MB左右。若应用复杂JIT代码多出现CodeCache满,可增大之。
  • -XX:+AlwaysPreTouch:JVM启动时预先Touch分配的堆内存,避免首次使用时出现缺页中断影响响应。这会使启动变慢但运行更平稳,常用于大堆应用预热。
  • -Xnoclassgc:禁用类卸载。一般不需要,除非在特定场景下类卸载有问题。

参数调优经验法则:

  1. 先观察默认行为:Java的默认参数在大多数情况下已较合理,不建议一开始就大量修改。通过GC日志和监控确定瓶颈再对症下药修改特定参数。
  2. 逐步调整:一次只调整少数一两个参数,观察效果,避免多参数一起改混淆问题来源。
  3. 关注版本差异:某些参数在新版本中可能消失或默认值改变(例如JDK8的MaxPermSize在JDK8已无效,JDK9统一日志取代PrintGC等老参数,第10章详细说明)。
  4. 适配环境:调优参数在容器环境、不同操作系统上的效果可能不同,需要针对实际部署环境测试确认(如LargePage在容器内可能需要特殊配置等)。

练习:假设你的应用在运行中频繁发生FGC导致停顿,你可以尝试调哪些参数来改善?再比如应用在容器中发现堆用了不到一半但系统OOMKilled,可能需要关注哪些参数?尝试解释原因并提出解决方案。

通过对参数的理解和灵活运用,我们可以将JVM调优得更加贴合应用需求,为性能和稳定性保驾护航。


第6章 实用诊断与调优工具

除了依赖日志和经验,性能调优往往需要借助各种工具来诊断问题根源。Java 生态提供了一系列强大的工具用于监控、分析和排查 JVM 的性能瓶颈和异常情况。本章介绍常用的JVM诊断和调优工具,包括命令行工具和可视化工具。

6.1 jstat – JVM统计监视

jstat(Java Virtual Machine Statistics Monitor)是JDK自带的命令行工具,用于实时监控JVM性能统计信息,如垃圾收集、类加载、JIT编译等。它通过HotSpot内建的PerfData接口获取数据,无需预先启动参数[docs.oracle.com](https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/tooldescr017.html#:~:text=The ,system platforms supported by Oracle)。

常用用法:

jstat -gc <pid> <interval> <count>

-gc 选项显示垃圾回收相关统计,包括Eden、Survivor、Old区的容量和已用百分比、GC次数与时间等。<interval>为采样间隔毫秒,<count>为采样次数。如:

jstat -gcutil 2834 250 7

以上命令每250ms采样一次,连续7次,输出GC利用率摘要docs.oracle.com

 S0     S1     E      O      M     YGC    YGCT    FGC   FGCT    GCT
 0.00  99.74  13.49   7.86  95.82      3    0.124     0    0.000   0.124
 ...

各列含义:

  • S0/S1:Survivor0/1 已使用百分比
  • E:Eden已使用百分比
  • O:Old已使用百分比
  • M:Metaspace已使用百分比
  • YGC/YGCT:年轻代GC次数及耗时总计
  • FGC/FGCT:Full GC次数及耗时总计
  • GCT:GC总耗时

从中可以观察到实时的堆使用变化和GC触发。例如上例显示在7次采样中有一次Young GC发生(YGC增加到219->220次,YGCT增加)docs.oracle.com。通过比对前后数据可推算GC发生频率、停顿时间等[docs.oracle.com](https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/tooldescr017.html#:~:text=The output of this example,to 54.60)[docs.oracle.com](https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/tooldescr017.html#:~:text=In addition to showing the,lowered from 15 to 1)。

jstat 其他常用选项:

jstat 适合在线上轻量监控,因为它对目标JVM几乎没有性能影响[docs.oracle.com](https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/tooldescr017.html#:~:text=The ,system platforms supported by Oracle)。可写脚本定时调用监控内存曲线。相比GC日志,jstat提供更加即时的视角。例如可以用 jstat -gc <pid> 1000 连续观察堆使用随时间的变化。

示例:使用 jstat 识别 “GC风暴” 情况——如果看到 YGC 在很短时间内不断增长,而E(Eden)使用率频繁掉至0,说明应用频繁 Minor GC,可能需要增大 Eden。又如FGC计数上升则表明Full GC频发。通过 jstat 可快速捕获这些征兆然后进一步详查日志或dump。

6.2 jmap – 内存映像与统计

jmap 工具用于查询JVM内存相关的信息,甚至生成堆转储。常用功能:

值得注意:从JDK8起Oracle更推荐使用 jcmd 而非单独的 jmap/jstack 等[docs.oracle.com](https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/tooldescr014.html#:~:text=The release of JDK 8,diagnostics and reduced performance overhead)。jcmd 提供了类似甚至更多的功能且性能开销更小。

实际用法示例

  • 当怀疑内存泄漏时,先用 jmap -histo 看占用最多的对象类型。如果发现某种对象实例数持续上涨,可结合应用逻辑判断哪里可能有泄漏。进一步可触发heap dump,用MAT找出GC Roots路径。
  • 当遇到疑难的类加载问题,可用 jmap -clstats <pid>(一些JDK版本参数)或heap histo里查找ClassLoader实例了解类的计数和来源。

需要注意的是,jmap 附加到进程时,如目标JVM无响应(挂起),可以加 -F 强制模式尝试dump(Linux/Solaris),但有风险。常规情况下使用 jmap 不会显著影响目标进程,但获取heap dump可能导致停顿,最好在流量低谷或预先将实例摘除负载后操作。

6.3 jstack – 线程栈分析

jstack 用于生成JVM的线程快照(Thread Dump)。这是诊断死锁、长时间挂起、CPU飙高线程定位的利器。

运行 jstack <pid> 会输出所有Java线程的栈踪,以及锁等待情况。示例输出片段:

"main" #1 prio=5 os_prio=0 tid=0x... nid=0x... runnable [0x...]
   java.lang.Thread.State: RUNNABLE
        at com.example.MyClass.compute(MyClass.java:42)
        at com.example.MyClass.run(MyClass.java:30)
        ...
"Worker-1" #14 prio=5 os_prio=0 tid=0x... waiting on condition [0x...]
   java.lang.Thread.State: WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x000000076b8bf9f8> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
        at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
        ...

这里显示main线程在执行MyClass.compute,Worker-1线程处于WAITING,等待一个Condition(锁对象ID)。

jstack还会自动检测死锁:如果存在线程间互相等待,将在输出底部显示"Found one Java-level deadlock"并列出涉及的线程和锁[docs.oracle.com](https://docs.oracle.com/en/java/javase/17/troubleshoot/troubleshoot-process-hangs-and-loops.html#:~:text=Found one Java,Vector)docs.oracle.com。例如:

Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x000af398 (object 0xf819aa10, a java/lang/String),
  which is held by "Thread-2"
"Thread-2":
  waiting to lock monitor 0x000af400 (object 0xf819aa48, a java/lang/String),
  which is held by "Thread-1"

这表明Thread-1和Thread-2相互等待对方持有的锁,发生死锁[docs.oracle.com](https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/tooldescr016.html#:~:text=Found one Java)docs.oracle.com

jstack 用法小结:

调优诊断场景

  • 死锁排查:发生应用卡死无响应,第一步用 jstack 看是否Deadlock。如果如上输出,直接定位问题代码涉及的锁即可[docs.oracle.com](https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/tooldescr016.html#:~:text=Found one Java)。
  • CPU 100% 排查:当某Java进程占用CPU异常升高,jstack可抓线程栈,然后通过对比哪个线程处于RUNNABLE并长时间占用CPU。这通常结合 topps -mp <pid> -o THREAD,tid,time 找到消耗CPU最高的线程ID (tid),然后在jstack输出中搜寻该nid对应的线程,看它在执行什么代码。此方法常用于查找死循环热点方法。
  • 挂起卡顿:有时应用不完全死锁但似乎停滞,jstack多次(隔几秒多dump几次)观察线程栈变化。如果某些线程一直 BLOCKED 或 WAITING,看看它们等待的资源。可能是锁竞争严重(很多BLOCKED),或者外部调用卡住(线程卡在I/O读写)。
  • 线程泄漏:通过 jstack 可以数线程数、类型。如果发现大量类似线程(比如某自定义Thread未proper shutdown导致残留),可在dump中看到并统计。也可以 jcmd <pid> Thread.print 类似输出。

总之,jstack是Java诊断并发问题的第一神器,使用上也比较简单直接。现代运维系统也支持在web界面直接触发线程dump并分析死锁,大多是基于jstack原理。

6.4 jcmd – 通用诊断命令

jcmd 是从JDK7开始提供的综合诊断工具,可向JVM发送各类命令请求,功能涵盖 jmap/jstack/jstat 等并新增更多。使用格式:

jcmd <pid> <command> [options]

不带命令时可列出当前JVM支持的命令:

> jcmd 2125 help
2125:
The following commands are available:
JFR.stop
JFR.start
JFR.dump
...
Thread.print
GC.class_histogram
GC.heap_dump
GC.run_finalization
GC.run
VM.uptime
VM.flags
VM.system_properties
VM.command_line
VM.version
help

如上所示,2125进程支持众多命令,包括打印线程(相当于jstack)、类直方图(相当于jmap -histo)、heap dump、触发GC、JFR操作等等docs.oracle.com[docs.oracle.com](https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/tooldescr006.html#:~:text=,Print all threads with stacktraces)。

常用 jcmd 命令举例:

因为 jcmd 集成度高,现在Oracle官方文档多建议使用 jcmd 来替代 jmap, jstack 等[docs.oracle.com](https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/tooldescr014.html#:~:text=The release of JDK 8,diagnostics and reduced performance overhead)[docs.oracle.com](https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/tooldescr016.html#:~:text=The release of JDK 8,diagnostics and reduced performance overhead)。特别是在线上容器环境,有时进程PID会变化,用 jcmd <main class> 也可以匹配到JVM实例。另外 jcmd 还能输出更多底层信息,比如:

jcmd <pid> VM.native_memory

可以显示JVM本身各组件的本地内存使用(代码缓存、线程栈、DirectByteBuffer等),用于调查 native 内存分配问题。

使用案例

  • 在生产环境,一个Java进程突然发生长时间停顿,可以用 jcmd pid Thread.print 获取线程状态,再用 jcmd pid GC.class_histogram 获取内存分布,一条命令工具即可获取全面的信息,非常便捷。
  • 排查内存泄漏时,用 jcmd dump堆,然后 MAT 分析。同时可 jcmd histo对比几次的对象数量增减,锁定泄漏增长的类。

需要注意 jcmd 必须与目标进程用户相同(或root)。另外 jcmd 发送请求也会短暂停止JVM其他操作处理请求,一般开销很小,但像heap_dump会停应用较久(dump期间STW)。

6.5 VisualVM 和 Java Mission Control

VisualVM
VisualVM 是一款图形化的JVM监视和分析工具。早期JDK自带(JDK8及以前)叫 jvisualvm,后续独立发布。主要功能:

  • 实时监控 CPU、Heap 使用、GC情况、线程活动等。
  • 可以直接触发 threaddump、heap dump,并在界面查看分析(包括对象 histogram, 类加载等)。
  • 提供内置的 sampler 和 profiler,可对应用执行采样分析CPU热点、内存分配热点。
  • 可通过插件扩展,例如 VisualGC 插件可以更直观显示各代内存和GC事件。

使用VisualVM可以方便在开发或测试环境观察JVM行为。例如看到堆使用随时间曲线、线程数变化等。不需要在命令行敲很多指令,也整合了jstat/jstack/jmap的功能在UI上。

Java Mission Control (JMC)
JMC 是 Oracle 提供的用于分析 Java Flight Recorder (JFR) 数据的GUI工具。JFR是在JDK中内置的低开销事件收集框架,可记录诸如方法采样、锁争用、GC事件、线程状态变迁等丰富信息。Mission Control可以:

  • 启动/停止JFR录制(对JDK11+, JFR已开放给社区)。
  • 加载JFR生成的 .jfr 文件,并以各种页面展示分析结果:比如方法热点火焰图、GC暂停时间、内存分配来源、线程延迟分析等等。
  • 提供一些基于规则的自动分析提示,如 "某线程持有锁过久" 或 "GC频繁" 等警告。

JFR 好处是可在生产打开对性能影响极小(通常<2%开销),从而获取详细的运行期性能数据,比起只能看日志或有限采样更强大。在JDK11起JFR对所有版本开放使用。例如可以用命令:

jcmd <pid> JFR.start name=Profile settings=profile duration=60s filename=recording.jfr

录制60秒性能数据到文件,然后用JMC加载分析。

JFR示例:在JMC中我们可以看到一个垃圾回收分析页面,图表显示各次GC暂停分布,总暂停时间等,如果某段时间GC激增就会很明显。也可以看到内存泄漏分析(基于老年代增长趋势和OldObject样本事件)[docs.oracle.com](https://docs.oracle.com/en/java/javase/16/troubleshoot/troubleshoot-memory-leaks.html#:~:text=To identify a possible memory,following elements in the recording)。例如 JFR 有一个事件 jdk.OldObjectSample 可以采样老年代对象和引用链,帮助识别泄漏来源docs.oracle.com[docs.oracle.com](https://docs.oracle.com/en/java/javase/16/troubleshoot/troubleshoot-memory-leaks.html#:~:text=,field of the Application class)。Oracle文档展示了通过 JFR OldObject 分析 HashMap 内存泄漏的例子docs.oracle.com[docs.oracle.com](https://docs.oracle.com/en/java/javase/16/troubleshoot/troubleshoot-memory-leaks.html#:~:text=,field of the Application class)。

应用场景

  • 本地调优:使用VisualVM对应用跑压力测试时进行取样,迅速发现最大内存开销的对象类型、最消耗CPU的热点方法。
  • 生产问题排查:开启JFR录制几分钟,将结果文件下载,用Mission Control分析,可以得到比普通监控更细的洞察,例如确认一次GC暂停中哪个阶段耗时多,哪段代码分配内存过多等。
  • 线程死锁除外:JFR虽记录线程状态但死锁还是需要线程dump直接看,因为JFR不一定捕捉到卡死瞬间。

6.6 Async-Profiler – 低开销分析器

async-profiler 是社区流行的火焰图分析工具,支持对Java应用进行CPU、内存分配、锁争用等分析,并以火焰图(Flame Graph)\形式输出结果。它的特点是**低开销**,对运行时影响很小(采用Linux perf事件和AsyncGetCallTrace,不依赖安全点采样,避免 Safepoint bias[aws.amazon.com](https://aws.amazon.com/blogs/containers/analyzing-java-applications-performance-async-profiler-amazon-eks/#:~:text=Performance profiling in containerized Java,enables more accurate performance analysis)[aws.amazon.com](https://aws.amazon.com/blogs/containers/analyzing-java-applications-performance-async-profiler-amazon-eks/#:~:text=To avoid depending upon the,based on the HotSpot JVM)),适合线上环境短时间挂载进行性能诊断。

使用 async-profiler 典型步骤:

  1. 下载对应OS的二进制包,解压可得执行脚本如 profiler.sh 或单独的 libasyncProfiler.so

  2. 运行 profiling,如:

    sudo ./profiler.sh -d 30 -f cpu_flamegraph.html 

    这将对指定pid采样CPU 30秒,结果输出交互式HTML火焰图[github.com](https://github.com/async-profiler/async-profiler#:~:text=In a typical use case%2C,of a running Java process)。火焰图直观展示了CPU周期都耗在哪些方法上(越高越深的栈帧,越宽表示耗时比例大)。

  3. 打开输出的 flamegraph.html,可以浏览性能热点。各矩形块表示函数,宽度表示时间占比,纵向表示调用栈。

async-profiler 支持多种事件:

  • -e cpu(默认):CPU采样。
  • -e alloc:对象分配采样,生成内存分配火焰图,可找出大量内存分配的代码路径。
  • -e lock:锁竞争事件采样,用于分析锁热点。
  • 甚至可 -e cache-misses 等硬件事件,如果系统支持。

对Java来说,它无需插桩,原理是在VM安全点之外通过AsyncGetCallTrace获取栈,提高采样准确性[aws.amazon.com](https://aws.amazon.com/blogs/containers/analyzing-java-applications-performance-async-profiler-amazon-eks/#:~:text=Performance profiling in containerized Java,enables more accurate performance analysis)[aws.amazon.com](https://aws.amazon.com/blogs/containers/analyzing-java-applications-performance-async-profiler-amazon-eks/#:~:text=To avoid depending upon the,based on the HotSpot JVM)。

应用案例

  • 当线上服务CPU占用高但原因不明时,使用async-profiler采样几秒钟,得到CPU火焰图。往往可以清楚看到是否某算法方法占满,或者某框架底层调用耗时。例如可能发现某正则匹配方法占用了大量时间,就可定位问题。
  • 分析内存分配热点,找出哪些代码频繁创建对象。火焰图可以显示哪个函数(甚至库函数)在大量分配,比如ByteBuffer.allocate之类,然后可以考虑优化这些热点。
  • 在容器/K8s环境,它也可以attach。若需无侵入更长时间持续分析,可结合Linux perf或Google's ebpf等方案,但async-profiler作为on-demand诊断已经足够方便。

比较:JFR也能做类似CPU/Alloc分析,但async-profiler生成的火焰图是社区熟悉的直观格式,且不需要打开JMC等。两者可结合使用。

6.7 其他工具简述

  • JConsole:JDK自带简易GUI监视工具,通过JMX获取数据。可看Heap/Threads等粗略情况,能连远程(需开启JMX),也可执行MBean操作。功能不如VisualVM丰富。
  • MAT (Memory Analyzer Tool):Eclipse基金会的堆分析GUI,用于深入分析heap dump,找泄漏原因,查询对象引用链等。配合jmap dump使用,十分强大。
  • BTrace:一种动态跟踪工具,允许写Java脚本插入字节码运行时收集数据,能在不停止应用的情况下打印方法参数、返回值等。对性能稍有影响,但用于临时诊断逻辑问题很有效。
  • Arthas:Alibaba开源的Java诊断利器,以REPL交互方式,让你在运行Java应用上执行各种命令(查看线程、Heap、监视方法调用、修改日志级别等等)。Arthas对线上问题排查非常方便且安全,已被广泛采用。
  • Perf / SystemTap / ebpf:这些Linux原生工具可用于更底层的分析,比如检查JVM进程的系统调用、上下文切换、网络I/O等,对诊断JVM外部因素瓶颈(磁盘/IO)很有帮助。

综上,调优过程中工具的恰当使用能起到事半功倍的效果。建议熟练掌握至少jstack/jmap/jstat这些基础工具,再逐步学会使用VisualVM/JMC等高级工具,以应对不同深度的性能问题。

练习

  1. 启动一个示例Java程序,使用 jstat 每隔1秒监控其GC,每隔5秒取线程dump,练习识别GC和线程状态。
  2. 人为制造一个死锁程序,用 jstack 或 VisualVM 检测验证死锁信息。
  3. 使用 VisualVM 或 JFR 对一个Web应用进行5分钟监控,然后根据采集的数据写出该应用的几个性能特征(如CPU热点、内存占用、GC频率)。
  4. 如有条件,练习 async-profiler,对一个应用跑压测并采集CPU火焰图,识别最宽的几个方法。

第7章 性能瓶颈排查流程与案例分析

当系统出现性能问题(如响应变慢、吞吐降低、CPU飙升、内存不足等),需要系统性地排查原因并优化。JVM性能问题往往和应用自身逻辑交织在一起,需要综合考虑。下面介绍一个通用的排查思路,并通过案例说明如何应用之前学到的工具和知识。

7.1 性能问题排查的一般流程

  1. 明确问题现象:首先界定问题是延迟过高还是吞吐不足,或是OOM/崩溃等。收集相关指标:CPU利用率、GC频率/停顿、Load、IO等待等。这一步借助监控系统(如Prometheus/Grafana或云监控)非常重要。
  2. 区分系统瓶颈类型
    • CPU繁忙型:CPU使用率持续高企,应关注线程Dump和CPU分析。
    • 内存压力型:频繁GC甚至OOM,应检查GC日志、heap dump。
    • I/O受限型:CPU空闲但吞吐低,可能卡在外部IO(网络/磁盘),需要从线程状态(很多WAITING)和系统IO指标来判断。
    • 锁竞争型:CPU不满却吞吐低,线程Dump中大量BLOCKED或等待锁。
    • 外部依赖型:比如调用数据库慢,线程栈显示卡在JDBC调用。
  3. 使用合适工具深入分析:根据怀疑的瓶颈类型选择工具:
    • CPU问题:async-profiler 或 JFR 方法采样找热点;jstack多次捕捉RUNNABLE线程的栈。
    • 内存问题:检查GC日志判定是内存泄漏(占用持续上涨)还是堆太小;用 MAT 分析heap dump 定位占用最多的对象和GC Roots。
    • 线程/锁问题:jstack线程dump寻找死锁或长时间BLOCKED线程;JFR的锁分析或async-profiler锁火焰图寻找锁等待热点。
    • IO问题:系统工具如 iotop, dstat,以及应用层监控(数据库慢查询日志等)。
  4. 定位代码或配置:通过上步数据,定位具体的Java类/方法或配置项导致瓶颈。例如CPU热点在某算法函数,则检查算法实现;内存泄漏定位到某集合没清理;锁竞争发现某对象锁被多个线程频繁争夺。
  5. 制定解决方案:可能的手段:
    • 调优代码:优化算法、减少不必要的对象创建、修复泄漏、调整锁粒度等。
    • 调优JVM参数:增大堆或调整GC策略、合理设置线程池大小等。
    • 添加资源:如增加实例、加CPU/内存(如果问题确实是资源不足且应用已优化)。
    • 使用限流/缓存等架构措施缓解压力。
  6. 验证与迭代:应用优化方案后,在类似负载下测试指标是否改善;若有新瓶颈出现,继续重复上述步骤。性能优化往往是迭代过程。

7.2 案例分析:频繁Full GC导致吞吐下降

场景:某在线交易系统,部署在JDK11上,发现高峰期吞吐明显下降,响应延迟波动大。监控显示JVM堆使用率经常接近100%,CPU使用率下降(说明大段时间在GC而非执行业务)。

步骤1 明确现象:GC日志摘录:

[GC (Allocation Failure) ... 0.023s]
[GC (Allocation Failure) ... 0.030s]
[Full GC (Allocation Failure) ... 1.8s]
[GC (Allocation Failure) ... 0.025s]
[Full GC (Ergonomics) ... 2.1s]
...

每隔几十秒就有一次Full GC停顿1.5~2秒,期间应用几乎停顿。Minor GC频率也高。结合监控,Full GC时吞吐降为0,RT超时。

步骤2 判定瓶颈:明显是内存GC问题。Full GC频发通常老年代空间不足或碎片导致,需要检查堆配置和使用。

步骤3 工具分析

  • jstat -gcutil 连续观察:老年代O在Full GC后仍有较高占用,并迅速又涨满。推测老年代充满较多长生命周期对象。
  • Dump了一次heap(HeapDumpOnOOM触发或jcmd手动dump),用 MAT 分析发现大量 Order 对象保存在某 ArrayList ordersCache 静态列表中,占用了上百MB老年代。
  • 通过代码查找,发现这是应用为了快速访问最近订单而缓存的列表,但没有大小上限,导致一直增长。
  • 另查参数,JVM参数是 -Xmx4g,而系统给容器内存4g,无头间隙(容器OOM杀风险高)。

步骤4 定位问题:问题在于应用内存泄漏/不当缓存 + 堆配置偏小

  • 缓存无上限使对象堆积 -> 需要修改代码加上限或使用弱引用缓存。
  • 堆4g可能对高峰不够,且没留余量 -> 可以适当增大并确保容器内存限制匹配。

步骤5 制定方案

  1. 修复代码:将 ordersCache 改为容量有限的LRU缓存,每当超过10万条就移除旧数据,避免无限增长。
  2. 调整JVM参数:考虑将堆上限增至6g,同时在K8s容器中把Limit设为8g,或者使用 -XX:MaxRAMPercentage 控制在容器内占比如80%(这样JVM会自动设Xmx为0.8*8g=6.4g)。
  3. GC策略:当前JDK11默认G1,可继续用G1但调优,如 -XX:InitiatingHeapOccupancyPercent 降到40让并发标记更早,以及观察需要的话调高 G1ReservePercent 防止晋升失败。

步骤6 验证
部署修改后,观察运行:

  • ordersCache 大小稳定在10万上下,不再线性增长。堆老年代占用不再持续攀升。
  • GC日志显示Full GC几乎没有了。Minor GC仍有但停顿可接受(几十毫秒)。
  • 吞吐和延迟在高峰期保持稳定,没有之前的明显下降和抖动。
  • 容器内存使用约6.5g,未发生OOMKill。

通过此案例,可以看到调优常常是应用层面和JVM层面结合:光增加堆如果不解决泄漏,迟早内存还是会耗尽;光修代码但不给足堆空间也可能不够。因此需要综合施策。

7.3 案例分析:线程死锁导致CPU空闲但无响应

场景:某微服务偶尔出现请求堆积、无响应,但查看CPU和GC都很正常,线程也没有异常增长。怀疑发生了死锁或活锁。

步骤1 现象:服务无响应时,用 jstack 抓线程:

Found one Java-level deadlock:
=============================
"Scheduler-Thread-1":
  waiting to lock monitor 0x000fff10 (object 0x70c30ab0, a java.lang.Object),
  which is held by "Scheduler-Thread-2"
"Scheduler-Thread-2":
  waiting to lock monitor 0x000fff78 (object 0x70c30a80, a java.lang.Object),
  which is held by "Scheduler-Thread-1"

输出清楚表明了两个Scheduler线程互相等待对方持有的锁[docs.oracle.com](https://docs.oracle.com/en/java/javase/17/troubleshoot/troubleshoot-process-hangs-and-loops.html#:~:text=Found one Java,Vector)。涉及对象是两个不同的 java.lang.Object 锁。

步骤2 判定瓶颈:属于线程死锁问题,不是资源不够而是线程相互等待导致业务停滞。

步骤3 工具分析

  • jstack已经定位到死锁线程和锁对象的ID。进一步看线程栈:

    "Scheduler-Thread-1":
     at com.example.Scheduler.runTask(Scheduler.java:45)  (waiting on <0x70c30ab0>)
     at ...
    "Scheduler-Thread-2":
     at com.example.Scheduler.stopTask(Scheduler.java:75) (waiting on <0x70c30a80>)

    从栈可以推测在 Scheduler 类的 runTask 和 stopTask 方法中使用了不同的锁且没有一致顺序,导致死锁。

  • 查找代码,发现 Scheduler 对象里有两个锁 lock1lock2,runTask 先锁 lock1 再锁 lock2,而stopTask 先锁 lock2再锁 lock1,正是典型死锁情况。

步骤4 定位问题:调度程序的锁顺序不当,引入死锁隐患。

步骤5 方案

  • 修改代码:统一加锁顺序。例如不管runTask还是stopTask,都先锁lock1再锁lock2。或者更简单,用一个锁保护所有调度操作,减少细粒度锁反而避免问题(需评估性能影响)。
  • 加入死锁监控:可以在测试或生产用jconsole/JMX监控Threading的Deadlock检测,及时报警。

步骤6 验证
测试修正后的版本,再也没有出现 Scheduler 线程死锁问题。服务无响应现象解决。

该案例展示如何通过线程dump快速定位死锁,并验证代码逻辑问题。不同于性能调优,这是正确性问题,但排查过程类似,也是依赖JVM工具提供的信息。

7.4 案例分析:CPU飙高与热点优化

场景:某批量数据处理任务,在升级JDK版本或更换算法库后发现CPU使用率非常高,处理速度反而下降。

排查

  • 用 async-profiler 对作业运行进行CPU采样,得到火焰图。发现最大的一块是某正则表达式匹配相关的方法,占用了30%以上CPU时间。
  • 这个正则在代码中用于解析日志,之前JDK8版本Regex库性能一般,但JDK17的Regex似乎对某些复杂表达式使用回溯更多,导致效率低。
  • 解决方案可以是优化正则表达式本身(避免回溯过多)或者使用替代方案如预编译状态机。

实施

  • 改进了正则表达式模式,或采用了更高效的解析手段(比如简单切分替代正则)。
  • 再次运行分析,火焰图中该部分消耗大幅降低,总CPU下降,吞吐上升。

这个案例说明性能调优需要细致定位具体热点代码,有时升级环境可能导致不同表现,要具体分析。async-profiler等工具在这种情况下非常关键。

综合来说,性能瓶颈排查是一个需要多角度信息和经验判断的过程。要善于结合监控概览找到瓶颈方向,用专门工具深入细节定位根因,最后联想到代码和配置做出改进。经过一系列调整和验证,系统性能才能逐步达到理想状态。后续章节(第8章)将讨论一些常见问题的具体定位与优化方法。


第8章 常见问题定位与优化方法

本章汇总若干在JVM性能实践中经常遇到的问题类型,并提供定位思路和优化方法。这些问题包括内存泄漏、频繁Full GC、线程死锁、热点代码效率低下等。

8.1 内存泄漏

现象:应用运行越久占用内存越高,GC后堆使用不降反升,最终出现 java.lang.OutOfMemoryError: Java heap space。监控可见堆使用锯齿状上升趋势。

定位

  • Heap Dump 分析:首选在OOM发生时或接近OOM时获取堆转储(使用-XX:+HeapDumpOnOutOfMemoryError自动触发或jcmd手动),然后使用 MAT 打开,查看DominatorsTop Components报告。通常泄漏表现为某些对象大量存在且被GC Roots间接引用着。
  • 老年代增长:在GC日志中,老年代占用每次GC后都居高不下甚至增加,就是泄漏征兆(短期对象应在年轻代回收,不会进入老年代)。
  • JFR Leak Profiler:JFR自带泄漏探查,可通过 OldObjectSample 事件分析docs.oracle.com[docs.oracle.com](https://docs.oracle.com/en/java/javase/16/troubleshoot/troubleshoot-memory-leaks.html#:~:text=,field of the Application class)。JMC会列出怀疑泄漏的对象和引用链。
  • jmap -histo:多次dump histogram,如果某些类实例数持续增加,则可能泄漏。比如HashMap$Node数量不断增加但程序逻辑没清理Map,则怀疑Map泄漏。

常见原因

  • 缓存未清理:如用静态集合缓存数据却不清除或无大小限制。解决:用WeakHashMap或定期清理。
  • 监听器或回调未注销:导致对象一直被持有引用。
  • 静态变量/单例生命周期太长:如静态List不断添加内容。
  • 本地集合未及时清除:线程池线程本地变量(ThreadLocal)用完不remove,或者使用了不当的SoftReference缓存导致Old Gen积压。
  • JNI本地内存泄漏:表现为Heap不大但进程占用越来越高,可用 jcmd VM.native_memory 看各项分布并检查DirectByteBuffer分配是否过量等。

优化

  • 修复代码逻辑:找到未释放资源的代码,加上释放、清理逻辑。
  • 限制缓存大小:使用 LinkedHashMap LRU特性或Guava Cache,或用弱引用cache让GC自动回收。
  • 加强监控:对关键集合大小暴露指标,及时发现异常增长。
  • 如果一时无法彻底解决,可通过增加堆缓解,但治本还是要消除泄漏源,否则迟早耗尽。

注意:排查时,不一定所有增长就是泄漏,也可能只是负载升高正常占用上涨,应结合趋势和对象类型判断。真正泄漏一般会持续增长直到OOM,且对象类型无明确生命周期。

8.2 Full GC频繁

现象:应用频繁发生Full GC(比如一分钟多次),每次停顿较长,导致性能明显下降。Full GC在GC日志里通常以 "Full GC" 开头[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/gctuning/hotspot-virtual-machine-garbage-collection-tuning-guide.pdf#:~:text=A full heap garbage collection,finding the words Pause Full)。

可能原因

排查

  • GC日志查看Full GC触发原因后括号内标识,如 "(Allocation Failure)" 表示因为内存耗尽触发[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/gctuning/hotspot-virtual-machine-garbage-collection-tuning-guide.pdf#:~:text=A full heap garbage collection,finding the words Pause Full),"(System.gc())" 则是调用触发, "(CMS Failure)" 则CMS失败。根据原因采取对应措施。
  • 用 jstat -gcold 或 jmx监控老年代使用占比,是否Full GC后依然很高(指示泄漏或老年代不足)。
  • MAT分析Full GC前heap dump,看看老年代对象是否可回收但没被回收(也许是因为引用留存)。
  • 检查应用是否调用了System.gc(通过grep代码或用 -verbose:gc 日志)。

优化

  • 增大堆:最直接有效,减少Full GC频率,但要考虑内存物理限制。
  • 升级GC算法:如果是CMS问题且在新JDK,可切换G1或ZGC,它们更能处理碎片和并发。
  • 调整新生代:适当增大新生代,减少进入老年代对象数量。如果晋升失败频发,增大 Survivor 空间或提高 MaxTenuringThreshold 让对象多留在年轻代。
  • 减少中介对象:比如处理大批数据时,避免一下产生巨量中间对象冲击老年代,可分批处理或复用对象池。
  • 避免巨型对象:如将一个1GB数组拆分成多个小块处理,防止一次性大对象导致GC问题。
  • Tuning:G1下可以调高 G1ReservePercent 让更多空闲region作为晋升缓冲[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/gctuning/hotspot-virtual-machine-garbage-collection-tuning-guide.pdf#:~:text=(Allocation Failure) in the log,the application allocates too many),CMS下可以采用ParallelOldCompact代替纯CMS等。

8.3 线程死锁

现象:应用无响应,但进程仍在,CPU利用很低(线程都阻塞)。jstack输出"Found one Java-level deadlock"(或多个)[docs.oracle.com](https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/tooldescr016.html#:~:text=Found one Java)。

处理

  • 通过 jstack 找出死锁线程和锁,分析代码,修正锁获取顺序或合并锁。
  • 临时缓解:重启服务解除死锁,但不解决代码问题的话可能再次发生。
  • 养成锁顺序规则:确保多锁场景所有线程获取顺序一致;或者使用更高级并发结构(如ReentrantLock tryLock避免死等,或Lock组合在一起)。
  • 使用工具检查:FindBugs/SpotBugs等静态分析能发现潜在死锁,运行时Arthas等也有查看锁命令。

8.4 高CPU与热点优化

现象:CPU一直100%,响应慢但没有明显GC问题。可能是某业务代码计算繁重或陷入忙等。

诊断

  • 用 async-profiler 或 JFR CPU profiling,看火焰图中最大的占用在哪些方法。
  • jstack多次,找RUNNABLE线程的栈帧,如果每次都在某相同方法,则那个方法是热点。

常见原因

  • 算法效率低:如使用了嵌套循环处理大量数据,未优化。
  • 锁活锁:比如自旋锁实现不好导致空转消耗CPU。
  • 不必要的工作:轮询、sleep-0循环等。
  • JIT无优化:极少数情况下代码过于复杂导致JIT未能优化(可以通过-XX:+PrintCompilation或JFR看有哪些方法一直在解释执行)。

优化

  • 针对热点方法优化算法(例如用更优数据结构、减少重复计算、缓存结果)。
  • 使用并行:把串行计算改多线程并行(注意线程切换成本和任务是否能并行)。
  • 合理等待:避免用while(true)空转,可以用wait/notify或LockSupport.park。
  • 检查JIT:如果发现某关键方法始终未编译,可考虑手动激进编译选项或者拆分方法简化以利于JIT优化。
  • 本地代码问题:有时JNI调用消耗大,也要评估。
  • 最后,硬件上可以考虑升级CPU或者增加实例拆分负载,如果确实业务需要计算量巨大而优化空间有限。

案例:曾遇到JSON库在序列化超大对象时性能低,通过分析发现是频繁字符串拼接造成,改用StringBuilder或流式写入方式后CPU降低一半。

8.5 Full GC导致的停顿和Promotion Failed

这一问题前面Full GC频繁部分已覆盖。Promotion Failed在Parallel Scavenge日志中出现时,意味着 Survivor无法容纳晋升对象,需要直接晋升老年代但老年代不足,触发FGC。解决同Full GC频繁:加大或配置堆区域。

8.6 OutOfMemoryError 及其他 OOM

除了Java heap OOM,还有PermGen/Metaspace OOM、Direct buffer OOM、本地线程Stack OOM等:

  • Metaspace OOM:很多动态类(例如不停有新类加载)导致Metaspace满。可扩大MaxMetaspaceSize,但根本需检查类加载泄漏(典型如不断加载新插件却不卸载旧类)。
  • Direct buffer OOM:NIO直接内存耗尽,可能分配ByteBuffer没及时回收。参数-XX:MaxDirectMemorySize可限制。排查用 jmx MBean BufferPoolDirect 或 jcmd VM.native_memory看 DirectBuffer,用 Netty等注意配置池化。
  • Unable to create new native thread:线程数过多导致操作系统资源耗尽。这不是堆OOM而是native OOM。解决:减少线程创建(使用线程池,检查无限生成线程的Bug),或者提升OS限制ulimit(治标)。

OOM后的调优

  • 一定打开 HeapDumpOnOutOfMemoryError,把现场dump保存分析根因[docs.oracle.com](https://docs.oracle.com/en/java/javase/16/troubleshoot/troubleshoot-memory-leaks.html#:~:text=Understand the OutOfMemoryError Exception)。
  • 检查OOM message详细说明:如 "Java heap space" vs "Metaspace" vs "Direct buffer memory" vs "GC overhead limit exceeded"(后者表示99%时间在GC,近似heap耗尽)。
  • 有些情况(如GC overhead)可以通过调整JVM阈值避免JVM自杀,但治本还是要优化内存使用。

总而言之,JVM调优需要对症下药。本章列举的问题类型可能交织出现,需要综合运用各章知识。例如内存泄漏可能同时引起Full GC频繁和最终OOM,所以要逐层分析。

练习:针对你的应用,假想一种故障场景(比如高负载下响应变慢甚至超时),写出一个排查和调优的方案,包含使用什么工具、关注哪些指标、可能的改进措施。这个练习有助于加深对调优思路的理解。


第9章 容器化部署中的 JVM 调优

随着微服务盛行,Java 应用常运行在 Docker / Kubernetes 等容器环境中。容器的资源限制(CPU/Mem)会影响JVM行为,需要特别的调优考量。本章讨论Kubernetes等容器环境下JVM调优的注意事项和建议。

9.1 JVM 的容器感知

以往JVM默认认为可用内存是整个物理机,CPU也是所有核心。因此在容器限制下,必须让JVM意识到,否则可能出现:

  • JVM以为有大量内存,设置了过大的默认堆,超过容器限制导致被系统 OOM Killer 干掉。
  • JVM默认GC线程数按物理核数设置,容器只给2核却开了16个GC线程,过度竞争。

幸好,现代JDK已经支持容器感知

默认MaxRAMPercentage=25%,意味着如果容器限1GB,默认最大堆=0.25*1GB=256MB[developers.redhat.com](https://developers.redhat.com/articles/2022/04/19/java-17-whats-new-openjdks-container-awareness#:~:text=example%2C in a Kubernetes container,of that 800MB container)。这个比例在容器里往往不合适,因为容器通常跑单应用,JVM可用更多内存[developers.redhat.com](https://developers.redhat.com/articles/2022/04/19/java-17-whats-new-openjdks-container-awareness#:~:text=among other things,of that 800MB container)[developers.redhat.com](https://developers.redhat.com/articles/2022/04/19/java-17-whats-new-openjdks-container-awareness#:~:text=example%2C in a Kubernetes container,of that 800MB container)。因此调优时常显式设置:

-XX:MaxRAMPercentage=75.0

比如 1GB限制,则Xmx=768MB。另外 -XX:InitialRAMPercentage 可设置初始堆为一定比例(默认1/64=1.5625%)。

JVM也会感知 CPU配额:

可以用:

java -XshowSettings:system -version

来验证JVM识别的容器配置[developers.redhat.com](https://developers.redhat.com/articles/2022/04/19/java-17-whats-new-openjdks-container-awareness#:~:text=One quick way to show,specific.) Here's an example)。

9.2 内存调优在容器中的建议

  1. 明确Limits/Requests:K8s通常设置requests和limits,对于堆设置要考虑limit。如果JVM Xmx超过limit,则冒风险OOM Kill。
  2. Heap占比:推荐堆大小约为容器内存的70-80%,剩余留给线程栈、Metaspace、直接内存等。如果Metaspace大(很多类),还得预留多些。
  3. UseContainerSupport:确保JVM开启容器支持(JDK8u191+默认如此)。如使用更老版本需升级或加参数。
  4. Swap:容器内最好禁用swap或严格限制,否则JVM内存swap会严重影响性能。JVM有 -XX:+AlwaysPreTouch 预触页避免懒分配导致突发缺页。
  5. OOM行为:在容器中,更倾向让JVM自我终止而非耗尽被杀。可以加 -XX:+ExitOnOutOfMemoryError,一旦OOM立刻退出,这样K8s可快速重启pod。
  6. HeapDump:配置HeapDumpPath写到容器可写目录(如 /tmp)以便容器死后还能取日志/heap dump进行诊断(K8s可以用 emptyDir 等)。
  7. 直接内存:Netty等框架常用direct buffer,大小由 MaxDirectMemorySize 控制,默认等于Xmx(JDK8),JDK11默认无上限=MaxHeap外同大,需要看8u和11差异。可显式设比如 MaxDirectMemorySize=50M以免分配无度。
  8. Metaspace:如果应用动态加载类多,也要监控 Metaspace 使用,必要时调高 MaxMetaspaceSize,否则容器内也可能OOM(虽然Metaspace OOM一般抛异常而非进程退出,但可能伴随Crash)。

9.3 CPU调优在容器中的建议

  1. GC线程:Parallel GC默认线程=物理核XX%,在容器2核情况下默认仍可能开并行线程>2。G1默认并发线程=CPU核数0.25 (ConcGCThreads),ParallelGCThreads=核数。确保JVM识别到正确核数(JDK10+一般OK)。可通过 -XX:ParallelGCThreads=<n> 强制设置 = 容器核数或略多(如果容器核有burstable或超卖情况也注意别过高)。
  2. 编译线程:JIT编译也有线程池 CICompilerCount 默认2,问题不大。TieredCompilation在容器中一般正常,可不用动。
  3. 线程数:容器内内存有限,创建过多线程会浪费栈内存。控制业务线程数量(比如Tomcat线程池大小)以requests压力配置,别无限制。
  4. Limits vs Throttling:K8s CPU limit用CFS限频。如果JVM有24线程但limit=2核,则绝大部分时间线程会排队等待CFS配额,导致性能下降。最好requests=limits且与JVM线程配置匹配,避免CPU超售带来的不可预期抖动。简言之,在容器内避免让JVM以为有更多CPU。可用 taskset 绑核或者—简易—设置容器CPU limit=requests严格等于JVM核心需求。

9.4 实践建议

  • 调优案例:某服务container给了2 CPU, 4G RAM,但未设置MaxRAMPercentage。JDK11默认为25%,Xmx=1GB,只用到1G堆实际可用4G,结果Full GC频繁(因实际需要2-3G)。解决:设置 -XX:MaxRAMPercentage=75,使Xmx约=3G,问题解决[developers.redhat.com](https://developers.redhat.com/articles/2022/04/19/java-17-whats-new-openjdks-container-awareness#:~:text=example%2C in a Kubernetes container,of that 800MB container)。
  • 另例:容器2核,ParallelGCThreads默认可能=2,但如果JVM认为还有HT或其他,可能=4。实际观测GC时CPU100%(4 GC线程压2核)。解决:手工 -XX:ParallelGCThreads=2 或换G1(G1停顿小也可接受)。
  • 使用G1/ZGC在容器中也是不错选择,因为它们自调节,尤其ZGC对繁忙系统影响小,只是ZGC要确保有足够CPU空闲并发。如果容器CPU吃满,ZGC可能回收不及时导致涨内存。
  • 内存Headroom:给容器设置Limit时,可以稍多于Xmx+其他,别精确相等。比如Xmx=3G, MaxMetaspace=300M, 线程栈0.5G,总计3.8G,就别把limit也设3.8,最好有10-15%余量防止抖动(MemoryPressue margin)。
  • CGroup v2:注意有的Java版本在cgroupv2需要11.0.9+才完全支持[developers.redhat.com](https://developers.redhat.com/articles/2022/04/19/java-17-whats-new-openjdks-container-awareness#:~:text=If no cgroup v2 support,output would look like this)。确保基础镜像OpenJDK版本更新。

调试:推荐在Pod里运行 jcmd <pid> VM.flagsjcmd <pid> VM.info 看实际JVM参数和容器检测结果(VM.info包含container信息)。

小技巧

  • 在K8s YAML中,可以直接用 JAVA_TOOL_OPTIONS 环境变量,将 -XX:MaxRAMPercentage 等放进去,应用启动时自动带上参数,比较方便团队统一。
  • 对于使用Spring BootFat jar的,可以考虑通过Procfile或shell调整,确保HeapDump路径, GC log路径映射到卷,这样调优取证更容易。

9.5 Kubernetes环境整体调优

容器化Java还涉及:

  • 启动时间:容器通常要求快速弹性扩容。JVM启动慢可以通过开启class data sharing (AppCDS),JDK12+可用 -XX:ArchiveClassesAtExit 生成应用CDS,提高冷启动速度。还有使用TieredCompilation加速JIT warm-up,甚至考虑GraalVM Native Image(启动极快但运行性能不同取舍)。
  • 资源过载保护:K8s HPA或Vertical Pod Autoscaler配合监控,根据JVM指标(如GC时间、堆占用)触发扩容或request资源调优,避免单实例超载。
  • Sidecar监控:用Prometheus Java Agent导出JVM指标(Memory, GC, Threads),随时观测以调整容器配额更科学。
  • CI/CD:调优参数作为应用配置的一部分,放在配置中心或K8s ConfigMap中易于管理,不要写死在代码里。

总的来说,在容器时代进行JVM调优,需要结合容器的资源调度特点调整策略,使JVM行为与容器限制匹配,才能充分利用资源且保持稳定。

练习:设想在Kubernetes上部署一个Java服务,给它2 CPU、2 Gi内存limit。你会如何设置JVM参数以最佳利用这2Gi内存又不被OOMKill?如何配置GC线程?试写出你的配置并解释原因。


第10章 不同JDK版本的调优差异

Java长期支持版本(LTS)如JDK8, JDK11, JDK17, JDK21在JVM实现上有所演进。调优时需了解版本差异,如默认GC、参数是否变更或弃用、新GC可用性、性能改进等。此章总结各主要LTS版本之间的变化。

10.1 默认垃圾收集器变化

10.2 参数变化或废弃

  • 永久代:JDK8移除了PermGen,改用Metaspace。相关参数 -XX:PermSize, -XX:MaxPermSize 无效,被 MetaspaceSize, MaxMetaspaceSize 取代。
  • GC日志参数:JDK9统一日志,用 -Xlog 代替了 PrintGCDetails 等[developers.redhat.com](https://developers.redhat.com/articles/2022/04/19/java-17-whats-new-openjdks-container-awareness#:~:text=Note%3A Container awareness in OpenJDK,OperatingSystemMXBean)。旧flags多兼容到JDK10,11但建议迁移。例如 -XX:+PrintGCDateStamps 改为 -Xlog:gc*::time
  • CMS相关:JDK9标记CMS deprecated后,其细分参数可能在JDK14移除或忽略。如 CMSInitiatingOccupancyFraction 在15上已无意义。UseCMSCompactAtFullCollection这些没CMS后也无用了。
  • UseParallelOldGC:在JDK8默认ParallelOld=启用,flag保留控制;JDK9+Parallel GC总带Parallel Old,无需分flag(但flag应该仍被接受只是无效)。
  • TieredCompilation:JDK8 server默认已开启Tiered[docs.oracle.com](https://docs.oracle.com/en/java/javase/11/jrockit-hotspot/compilation-optimization.html#:~:text=Tiered compilation is the default,TieredCompilation flag)。JDK7默认关闭server tiered。因此在JDK8+几乎不需要 -XX:+TieredCompilation,除非关闭它。但经验是不建议关,除非诊断JIT问题时才 -XX:-TieredCompilation` 试。
  • 压缩指针UseCompressedOops 默认开启64-bit < 32GB堆。JDK8+ 均如此。JDK17开始支持更大堆(64GB)仍可压缩(class pointer分开压缩等JEP)。
  • String去重-XX:+UseStringDeduplication 在G1下JDK8u20+可用,用于减少重复字符串占堆。JDK11默认G1应已默认启用此功能(不100%确认默认值,但可以认为开启没坏处)。
  • JFR:JDK8商业版才有Flight Recorder,参数如 -XX:+UnlockCommercialFeatures -XX:+FlightRecorder。JDK11开源自带JFR,无需解锁,用 -XX:StartFlightRecording 等。
  • CompactStrings:JDK9引入字符串内部用byte[]表示优化(compact strings),无需参数控制,JDK8update也有类似特性 -XX:+UseCompressedStrings 但后来废弃。
  • G1参数:一些参数在版本间调整默认,比如JDK10提升G1 mixed GC时间比例target,JDK11增加G1UseDynamicRegionSize策略等等。这些对于调优主要确保用默认即可,大多不用手调。
  • ZGC参数:JDK11初ZGC标记实验,要用 -XX:+UnlockExperimentalVMOptions -XX:+UseZGC。JDK13+不需Unlock。JDK15+ZGC支持Windows/macOS,多平台。JDK17 ZGC进一步优化并增加可选并发栈处理。JDK21 GenZGC预览需 --enable-preview 以及特殊开启flag(具体参考JEP439)。
  • Shenandoah:JDK11未包含官方版shenandoah(只有RedHat版),JDK12-16需 -XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC,JDK17+直接 -XX:+UseShenandoahGC
  • GarbageCollectorMXBean name:在不同版本中,如果用代码查询默认GC,会发现JDK8返回 "PS Scavenge"/"PS MarkSweep",JDK11是"G1 Young"/"G1 Old" (两个名称),JDK17 ZGC会有 "ZGC"一个Collector。这对管理接口稍有不同,但一般不影响调优,只是使用监控工具时注意。
  • 安全机制:JDK17对某些内部API访问默认封闭(强封装),有时需要 --add-opens 参数才能让诸如jmap/jstack附加在某些环境下(特别安全策略)生效。但大多数情况下调优还是可以顺利使用工具。

10.3 不同版本可选 GC 支持状况

汇总:

  • JDK8:Serial, Parallel, CMS, G1 都可用。ZGC/Shenandoah不可用(除非使用特殊OpenJDK8 Shenandoah版)。
  • JDK11:Serial, Parallel, G1 默认。CMS可用但deprecated。ZGC (experimental Linux/x64), Shenandoah (在OpenJDK11由RedHat patch提供,但Oracle11未提供)。
  • JDK17:Serial, Parallel, G1 默认。CMS不可用。ZGC (production on all major OS), Shenandoah (production, in OpenJDK and Oracle).
  • JDK21:Serial, Parallel, G1 默认。ZGC, Shenandoah 稳定可用。新增Generational ZGC (preview,需要开启预览特性才能用)[kstefanj.github.io](https://kstefanj.github.io/2023/12/13/jdk-21-the-gcs-keep-getting-better.html#:~:text=JDK 21 and the other,Let’s see how it performs)。Epsilon (No-GC)仍可用实验。
  • OpenJ9:如果使用Eclipse OpenJ9 JVM则GC实现不同(gencon等),但本手册聚焦HotSpot,不多展开。

选择建议

  • JDK8环境旧项目可考虑切换G1(8u后期G1已很稳定),特别是大堆的应用CMS容易出问题的可改G1[blog.csdn.net](https://blog.csdn.net/qq_59708493/article/details/134653137#:~:text=JDK9之后默认的垃圾回收器是G1(Garbage First)垃圾回收器。 Parallel Scavenge关注吞吐量,允许用户设置最大暂停时间,但是会减少年轻代可用空间的大小 )。
  • 升级JDK11+时,让默认G1跑,看是否满足。如追求极低延迟,可尝试ZGC/Shenandoah。
  • JDK17开始ZGC完全成熟,heap几百GB应用可大胆用ZGC。Shenandoah更多在RedHat体系推广。
  • JDK21 generational ZGC值得关注测试,它有望提供接近Parallel的吞吐+ZGC的低延迟。
  • 具体每个应用场景不同,建议通过对比测试各GC(JDK自带的gcbench或业务仿真)做决策。

10.4 版本升级对调优策略的影响

  • 性能提升:新版本JVM往往带来GC和JIT优化。以GC为例,JDK17比8在G1算法上有明显改进[kstefanj.github.io](https://kstefanj.github.io/2023/12/13/jdk-21-the-gcs-keep-getting-better.html#:~:text=The progress)[kstefanj.github.io](https://kstefanj.github.io/2023/12/13/jdk-21-the-gcs-keep-getting-better.html#:~:text=The comparisons include JDK 8%2C,sense to look further back)。因此升级后以前需要调的参数可能不需要调或默认值已足够。例如JDK8时代可能需要调G1 Survivor等,而JDK17 G1自适应做得更好,尽量少改默认即可。
  • 延迟考虑:如果从8/CMS转到11/G1,调优应转变思路:CMS许多参数作废,要用G1的新参数体系,同时观察应用暂停时间是否改善,需要调整MaxGCPause目标等。
  • 内存占用:JDK17对G1的堆元数据占用减少[kstefanj.github.io](https://kstefanj.github.io/2023/12/13/jdk-21-the-gcs-keep-getting-better.html#:~:text=benchmark with a fixed load,efficient collector)(单标记位图),意味着同等设置下17可能比8节省heap overhead,可以稍微降低Xmx或在相同Xmx下装载更多对象。JDK21 generational ZGC将显著降低ZGC的内存占用和提升吞吐[kstefanj.github.io](https://kstefanj.github.io/2023/12/13/jdk-21-the-gcs-keep-getting-better.html#:~:text=JDKs for G1 and Parallel,more beneficial than right now)[kstefanj.github.io](https://kstefanj.github.io/2023/12/13/jdk-21-the-gcs-keep-getting-better.html#:~:text=The addition of generations to,slightly better compared to legacy),如果它稳定,可调整之前对ZGC“需要空闲40%堆空间”这样的策略。
  • 参数调优过渡:在升级JDK时,要审视原启动参数:
    • 去掉无效的(PrintGCDetails换Xlog等等)。
    • 针对默认变化调整(例如原来Manual UseG1GC在JDK11可去掉,因为默认就是G1)。
    • 关注警告:启动日志中JVM会warn不认识的参数。升级后这些警告需要处理,否则调优参数不起作用还以为生效。
  • 新特性应用:JDK17还有Epsilon可用于测试压力下最大吞吐理论值;JDK21 Loom(虚拟线程)对并发模型影响大,但那主要是应用层。对JVM调优来说,虚拟线程大量存在时GC和栈处理需要观察,但HotSpot已经对虚拟线程栈做特殊优化(栈随用随分配)。
  • 工具演进:高版本JDK自带JFR,对调优手段上更方便。JDK8时可能依赖外部profiler多些,JDK11+可以直接用JFR。Mission Control也在更新更好规则分析。
  • 安全与诊断:JDK17默认开启Sanitizer for metaspace, System.out early flush等,这些通常无需调优考虑,但升级文档中会提。
  • Log消息:Minor detail,JDK9+ Xlog GC日志格式不同,需要熟悉下以免误读日志(不过Unified Logging可定制格式甚至输出旧式样)。

表:JDK8, 11, 17, 21 主要GC特性比较

特性 JDK8 JDK11 JDK17 JDK21
默认GC Parallel (吞吐)[javabetter.cn](https://javabetter.cn/jvm/garbage-collector.html#:~:text=Java 经典垃圾回收器详解 ,XX%3A%2BUseG1GC 说明JDK 9默认的垃圾回收器为G1。 经典垃圾回收器介绍) G1 (低停顿)[kstefanj.github.io](https://kstefanj.github.io/2023/12/13/jdk-21-the-gcs-keep-getting-better.html#:~:text=* G1 ,millisecond pauses) G1 G1
CMS 可用性 可用(弃用警告) 可用(Deprecated) 移除,不可用 不可用
G1 改进 初始版本 并发提升,默认 更成熟,性能更佳 继续改进
ZGC Experimental (Linux) Production (跨平台) 有代际ZGC预览[kstefanj.github.io](https://kstefanj.github.io/2023/12/13/jdk-21-the-gcs-keep-getting-better.html#:~:text=JDK 21 and the other,Let’s see how it performs)
Shenandoah 无 (OpenJDK8 RH版有) Experimental (需RH版) Production (OpenJDK/Oracle) Production
其他GC - Epsilon (实验) Epsilon (实验) Epsilon (实验)
日志系统 PrintGC* flags Xlog 推荐[developers.redhat.com](https://developers.redhat.com/articles/2022/04/19/java-17-whats-new-openjdks-container-awareness#:~:text=Note%3A Container awareness in OpenJDK,OperatingSystemMXBean) Xlog Xlog
内存设置 不识别容器 容器感知 (需Flags) 容器感知 (自动) 容器感知优化 (cgroupv2)
JIT 编译 C1/C2, Tiered默认 同JDK8 引入Graal JIT可选(实验) JEP431(优化), Loom影响
线程模型 传统线程 传统线程 虚拟线程预览(JDK16/17) 虚拟线程正式 (JEP444)

(最后两行关于JIT和线程提及JDK21的新特性Loom,因为虚拟线程可能对调优(如栈内存)有影响,但超出本手册重点)

10.5 针对不同版本的调优侧重点

  • JDK8:老应用若不能升,可重点调CMS(或G1)参数、减少Stop the World影响。注意PermGen OOM问题。使用Parallel GC时偏重吞吐,不用刻意调MaxPause(Parallel对pause目标不敏感)。
  • JDK11:默认G1,关注pause目标满足情况。尝试ZGC如果对延迟要求高。调优参数主要围绕G1 (如暂停时间, IHOP)。容器化开始普遍,此时确保MaxRAMPercentage设置。
  • JDK17:更加免调优,因为JVM自适应改进多。例如G1垃圾周期更智能,字符串去重默认开等。调优策略可更保守——“少改参数,多信任默认”。但是可以尝试更先进GC如ZGC/Shen来达到特定目标。JDK17也是LTS,性能整体较JDK11有增益[kstefanj.github.io](https://kstefanj.github.io/2023/12/13/jdk-21-the-gcs-keep-getting-better.html#:~:text=If we look as far,making ZGC a generational collector)[kstefanj.github.io](https://kstefanj.github.io/2023/12/13/jdk-21-the-gcs-keep-getting-better.html#:~:text=When it comes to raw,more beneficial than right now)。
  • JDK21:若采用Generational ZGC,可调优的点会有所不同,例如ZGC也有年轻代了,那么还需要观察其行为决定是否调类似MaxGCPauseMillis之类,不过ZGC目标停顿本就极低,不需要MaxPause参数。目前JDK21调优新点在于虚拟线程使用场景增多,过多虚拟线程可能影响调度和GC root扫描,官方有优化,如ZGC对虚拟线程栈处理优化过。调优方面虚拟线程栈大小可调(在JDK Loom实现里栈是一种Continuation Stack,具体参数非常新)。总之JDK21开始Java在并发模式上的变化也许需要新的监控关注(例如过多活跃虚拟线程导致任务切换频繁)。
  • 向后兼容:调优配置在升级时要逐步验证。比如以前依赖CMS,升级17必须换GC,性能模式改变,要重新收集基准数据调参数。

结论:每次Java版本升级,都应复查JVM调优设置。新版本往往需要更少人工调优,因为HotSpot团队在不断优化自适应参数和GC算法。但也提供了新的能力选项(如ZGC/Shenandoah)供我们选择,帮助在不同工作负载下达到更好的性能。调优人员要跟上这些变化,善加利用官方资料(JEP文档、release notes)[cloud.tencent.com](https://cloud.tencent.com/developer/article/1429131#:~:text=为什么G1 GC从JDK 9之后成为默认的垃圾回收器? ,net%2Fjeps%2F 248),并且官方将CMS标记为丢弃(具)来调整策略。


结语

通过本手册系统的学习,我们从 JVM 内部原理入手,逐步掌握了如何为不同应用场景选择合适的垃圾收集器、如何读取和分析GC日志、如何运用各种工具诊断性能瓶颈,以及针对常见问题进行优化的方法。同时,我们关注了在容器化环境下JVM调优的新要点,以及Java各版本间的重要差异。

性能调优既是一门科学也有很多实践技巧。希望读者在掌握理论的基础上,多多结合自己的应用实际进行实验和验证。只有在真实场景中运用并观察效果,才能真正体会调优的艺术。

最后,调优是没有终点的过程。硬件环境、应用特性、Java版本都在不断变化,需要我们持续学习最新的JVM特性和社区经验(例如关注 OpenJDK JEP、新版性能测试报告等)。愿本手册能成为您日后 JVM 调优道路上的有力参考,帮助您从入门迈向精通,在实践中游刃有余地优化Java应用的性能和稳定性。

参考资料:(以下是部分引用及推荐阅读的官方文档和优秀博客)

握紧原理之剑,辅以工具之盾,相信你定能战胜一个又一个性能难题,让JVM为你的应用保驾护航!

Leave a Comment

您的电子邮箱地址不会被公开。 必填项已用*标注

close
arrow_upward