JVM 调优实验

发布时间 2023-11-03 09:29:53作者: Ba11ooner

JVM 调优理论

前言

关于性能优化

The real problem is that programmers have spent far too much time worrying about efficiency in the wrong places and at the wrong times; premature optimization is the root of all evil (or at least most of it) in programming. — Donald Ervin Knuth

真正的问题是,程序员在错误的地方和错误的时间花了太多的时间担心效率问题;过早的优化是编程中所有(或者至少是大部分)罪恶的根源。

什么是 JVM 调优?

通过 JVM 参数的调整,优化 JVM 对于内存的管理,Java 应用性能提升的最后手段

为什么是最后手段?因为调整 JVM 的投入产出比往往低于对于应用本身的优化

为什么要进行 JVM 调优?

内存宝贵,性能重要,其他性能优化手段都已经用上,但仍未满足性能需求

知识补充

常见应用程序模型抽象
  • 任务视角:任务执行靠 CPU,多线程视角下的应用程序模型

    • CPU 密集型:CPU 忙碌,即使引入多线程也要等待,不利于提高计算效率,耗时可能不降反升
    • IO 密集型:CPU 空闲,引入多线程有助于接受更多的并发请求,有助于提升吞吐量,进而降低耗时
  • 对象视角:对象使用消耗空间,根据对象特点动态分析 GC 视角下的应用程序模型

    • 极致新生代(只创建新对象):新生代空间 ↑,GC 频率 ↑

      IO 交互型: 互联网上目前大部分的服务都属于该类型,例如分布式 RPC、MQ、HTTP 网关服务等,对内存要求并不大,大部分对象在 TP9999 的时间内都会死亡, Young 区越大越好。

    • 极致老年代(极限单例模式):老年代空间 ↑,GC 频率 ↓

      MEM 计算型: 主要是分布式数据计算 Hadoop,分布式存储 HBase、Cassandra,自建的分布式缓存等,对内存要求高,对象存活时间长,Old 区越大越好。

  • 供需视角:根据应用特点(垃圾生成速率和持久性)静态分析 GC 视角下的应用程序模型

    • 垃圾少:Serial GC,额外内存占用少
    • 垃圾多:看硬件环境限制
      硬件性能 ↓ ,选用:G1 > CMS GC > Parallel GC > Serial GC,从左到右对应的响应时间 ↑
JVM 常用参数

参考文档:JVM 常用参数

参数分类

知识补充:JVM 其实并不唯一,只要按照 Java 虚拟机规范,甚至可以自研 JVM,只不过现在最常用的还是Oracle 提供的官方实现(HotSpot JVM)

  • 标准参数(-),所有的 JVM 实现都必须实现这些参数的功能,而且向后兼容
    • 用法:java -help 查看当前机器所有java的标准参数列表
  • 非标准参数(-X),默认 JVM 实现这些参数的功能,但是并不保证所有 JVM 实现都满足,且不保证向后兼容
    • 用法:java -X 查看当前JVM支持的所有非标准参数列表(注意:X 要大写)
  • 非 Stable 参数(-XX),此类参数各个 JVM 实现会有所不同,将来可能会随时取消,需要慎重使用
    • 分类
      • 性能参数(Performance Options):用于 JVM 的性能调优和内存分配控制,如初始化内存大小的设置
      • 行为参数(Behavioral Options):用于改变 JVM 的基础行为,如 GC 的方式和算法的选择
      • 调试参数(Debugging Options):用于监控、打印、输出等 JVM 参数,用于显示 JVM 更加详细的信息
    • 用法
      • -XX:+<option> 启用选项
      • -XX:-<option> 不启用选项
      • -XX:<option>=<number> 给选项设置一个数字类型值,可跟单位,例如 32k, 1024m, 2g
      • -XX:<option>=<string> 给选项设置一个字符串值,例如-XX:HeapDumpPath=./dump.core
参数映射关系

img

img

常用参数列表
参数名称 示例用法 参数作用
-Xms -Xms256m 设置 JVM 堆的初始大小
-Xmx -Xmx512m 设置 JVM 堆的最大大小
-Xss -Xss256k 设置每个线程的栈大小
-XX:PermSize -XX:PermSize=128m 设置永久代的初始大小(在 JDK8 及以上版本,该参数已被废弃)
-XX:MaxPermSize -XX:MaxPermSize=256m 设置永久代的最大大小(在 JDK8 及以上版本,该参数已被废弃)
-XX:MetaspaceSize -XX:MetaspaceSize=128m 设置元空间的初始大小(在 JDK8 及以上版本,取代了 PermGen 和 MaxPermSize)
-XX:MaxMetaspaceSize -XX:MaxMetaspaceSize=256m 设置元空间的最大大小(在 JDK8 及以上版本,取代了 PermGen 和 MaxPermSize)
-XX:SurvivorRatio -XX:SurvivorRatio=8 设置新生代中 Eden 区与 Survivor 区的大小比例
-XX:NewRatio -XX:NewRatio=2 设置新生代和老年代的大小比例
-XX:MaxTenuringThreshold -XX:MaxTenuringThreshold=15 设置对象在新生代中经过多少次垃圾回收后进入老年代
-XX:G1HeapRegionSize -XX:G1HeapRegionSize=1m 设置 G1 垃圾回收器的堆区域大小,该参数影响堆中对象的分配及垃圾回收的速度
-XX:+UseParallelGC -XX:+UseParallelGC 启用并行垃圾回收器,该垃圾回收器采用多个线程同时进行垃圾回收,适用于多核服务器
-XX:+UseConcMarkSweepGC -XX:+UseConcMarkSweepGC 启用并发标记-清除垃圾回收器,该垃圾回收器在垃圾回收时不会暂停整个应用程序的执行,适用于服务器环境
-XX:+UseG1GC -XX:+UseG1GC 启用 G1 垃圾回收器,该垃圾回收器是一种新的垃圾回收器,适用于大内存、多核的服务器环境
示例

idea.vmoptions

-Xmx8192m
-XX:ReservedCodeCacheSize=512m
-Xms128m
-XX:+UseG1GC
-XX:SoftRefLRUPolicyMSPerMB=50
-XX:CICompilerCount=2
-XX:+HeapDumpOnOutOfMemoryError
-XX:-OmitStackTraceInFastThrow
-ea
-Dsun.io.useCanonCaches=false
-Djdk.http.auth.tunneling.disabledSchemes=""
-Djdk.attach.allowAttachSelf=true
-Djdk.module.illegalAccess.silent=true
-Dkotlinx.coroutines.debug=off
-Dsplash=true
-XX:ErrorFile=$USER_HOME/java_error_in_idea_%p.log
-XX:HeapDumpPath=$USER_HOME/java_error_in_idea.hprof
  • -Xmx8192m:设置Java堆的最大内存大小为8192MB。
  • -XX:ReservedCodeCacheSize=512m:设置保留代码缓存的大小为512MB。
  • -Xms128m:设置Java堆的初始内存大小为128MB。
  • -XX:+UseG1GC:启用G1垃圾回收器。
  • -XX:SoftRefLRUPolicyMSPerMB=50:设置软引用在内存不足时的最大存活时间。
  • -XX:CICompilerCount=2:设置并行编译器线程数为2。
  • -XX:+HeapDumpOnOutOfMemoryError:在内存溢出错误发生时生成堆转储文件。
  • -XX:-OmitStackTraceInFastThrow:不在快速抛出异常的情况下省略堆栈跟踪。
  • -ea:启用断言。
  • -Dsun.io.useCanonCaches=false:禁用Java IO规范的规范化缓存。
  • -Djdk.http.auth.tunneling.disabledSchemes="":启用所有的HTTP身份验证协议。
  • -Djdk.attach.allowAttachSelf=true:允许进程自我附加。
  • -Djdk.module.illegalAccess.silent=true:静默模式下不显示非法访问模块警告。
  • -Dkotlinx.coroutines.debug=off:关闭Kotlin协程的调试模式。
  • -Dsplash=true:启用启动时的启动画面。
  • -XX:ErrorFile=$USER_HOME/java_error_in_idea_%p.log:将错误日志写入到用户主目录下的java_error_in_idea_进程ID.log文件中。
  • -XX:HeapDumpPath=$USER_HOME/java_error_in_idea.hprof:将堆转储文件写入到用户主目录下的java_error_in_idea.hprof文件中(用于内存溢出错误时)。
JDK 常用工具
工具名称 示例用法 工具功能
jstat jstat -<option> <pid> [<interval> [<count>]] jstat 用于监视 JVM 的各种统计信息,例如垃圾回收信息、类加载信息、JIT 编译信息等。
jmap jmap [-<option>] <pid> jmap 用于生成 JVM 的堆转储快照(heap dump),可以用于分析内存泄漏问题。
jstack jstack [-l] <pid> jstack 用于生成 Java 进程的线程转储快照(thread dump),可以用于分析线程死锁、死循环等问题。
jconsole jconsole jconsole 是 JDK 提供的图形化监控工具,可以实时查看 JVM 的各种运行信息,例如内存使用情况、线程状态、GC 情况等。
jcmd jcmd <pid> <command> [<arguments>] jcmd 可以执行一些诊断命令,例如 GC 相关操作、线程分析、JIT 编译等。
jvisualvm jvisualvm jvisualvm 是 JDK 提供的增强型图形化监控工具,集成了 jconsole、jstack、jmap 等多个工具,并提供了插件支持。
JDK 8 之后不再集成,需要额外下载
jmc jmc jmc 是 JDK 提供的 Java Mission Control 工具,提供了全面的性能分析、故障诊断和优化调整功能,适用于生产环境的性能监控。
不一定集成,可能需要额外下载
垃圾回收器评价指标
  1. 内存回收效率:评估 GC 回收器的内存回收能力,即回收多少内存和回收多快。这可以通过监测内存回收的频率和时间来评估。
  2. 暂停时间:评估 GC 回收器对应用程序暂停的影响。较短的暂停时间可以提高应用程序的响应能力和用户体验。
  3. 吞吐量:评估 GC 回收器对应用程序可用时间和执行吞吐量的影响。较高的吞吐量表示应用程序能够在较短的时间内执行更多的工作。
  4. 内存占用:评估 GC 回收器的内存占用情况。较低的内存占用可以提高系统整体的资源利用率。
  5. 垃圾碎片:评估 GC 回收器在回收内存时会产生多少碎片。较少的垃圾碎片可以减少内存碎片化,提高内存使用效率。
常见垃圾回收器
  1. Serial(串行回收器):

    • 设计理念:串行回收器使用单个线程进行垃圾回收,适用于低内存和单核处理器的环境。
    • 内存回收效率:较低,只能利用单个线程进行垃圾回收。
    • 暂停时间:较长,由于只有单个线程执行垃圾回收,会导致暂停时间较长。
    • 吞吐量:相对较低,因为垃圾回收过程会占用较长时间。
    • 内存占用:相对较低。
    • 垃圾碎片:可能会产生较多的垃圾碎片。
  2. Parallel(并行回收器):

    补充:ParNew 垃圾回收器

    ParNew 与 Parallel 并没有本质上的区别,其主要是为了配合 CSM 的垃圾收集而提供的年轻代的垃圾收集器,其只有年轻代的收集版本,垃圾收集上与 Parallel 相同。

    目前仅有 Serial 和 ParNew 可与 CSM 进行配合垃圾收集。

    • 设计理念:并行回收器使用多个线程并行执行垃圾回收,适用于多核处理器的环境。
    • 内存回收效率:较高,可以利用多个线程并行执行垃圾回收。
    • 暂停时间:较长,尽管有多个线程并行执行垃圾回收,但仍然需要暂停应用程序的执行。
    • 吞吐量:较高,相对于串行回收器具有更高的吞吐量。
    • 内存占用:相对较高,需要多个线程执行垃圾回收,可能会占用更多的内存资源。
    • 垃圾碎片:可能会产生较多的垃圾碎片。
  3. CMS(Concurrent Mark Sweep,并发标记清除回收器):

    • 设计理念:CMS回收器通过并发执行标记和清除操作,尽量减少应用程序的暂停时间。
    • 内存回收效率:较高,通过并发执行来减少应用程序的暂停时间。
    • 暂停时间:较短,CMS回收器通过并发执行垃圾回收的阶段,将应用程序的暂停时间控制在较短的范围内。
    • 吞吐量:相对较高,因为暂停时间较短,应用程序可以更快地回到执行状态。
    • 内存占用:较高,CMS回收器需要为维护并发执行所需的数据结构而占用一部分内存。
    • 垃圾碎片:可能会产生较多的垃圾碎片。
  4. G1(Garbage-First,垃圾优先回收器):

    • 设计理念:G1回收器将堆内存划分为多个区域(Region),根据垃圾产生的情况进行回收,以最小化暂停时间。
    • 内存回收效率:较高,G1回收器可以根据垃圾产生的情况选择合适的区域进行回收,提高内存回收效率。
    • 暂停时间:较短,G1回收器可以在不暂停整个应用程序的情况下并发执行部分垃圾回收操作。
    • 吞吐量:相对较高,G1回收器通过并发执行和自适应的垃圾回收策略,可以在不同的情况下提供较高的吞吐量。
    • 内存占用:相对较高,G1回收器需要额外的内存来管理区域和维护相关数据结构。
    • 垃圾碎片:相对较低,G1回收器通过压缩回收和空闲区域整理等策略,可以减少垃圾碎片的产生。
回收器名称 设计理念 内存回收效率 暂停时间 吞吐量 内存占用 垃圾碎片
Serial GC 单线程
Parallel GC 多线程 中等 中等 中等
CMS GC 并发标记清除 中等
G1 GC 分代、区域化
评价整理
回收器名称 内存回收效率 暂停时间 吞吐量 内存占用 垃圾碎片
Serial GC ? ? ? ? ?
Parallel GC ? ?
CMS GC ? ? ? ?
G1 GC ? ? ? ? ?
分代收集理论

参考文档:分代收集理论

  • 弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的,生命很短
  • 强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡
  • 跨代引用假说(Intergenerational Reference Hypothesis),即跨代引用相对于同代引用来说仅占极少数。

基于弱分代假说和强分代假说,有必要针对两种不同的对象进行针对性的垃圾回收,生命周期短的多回收,生命周期长的少回收甚至不回收。即垃圾收集器应该将 Java 堆划分出不同的区域,然后将回收对象依据其年龄分配到不同的区域之中存储,这样,垃圾收集器就可以每次只回收其中某一个或者某些部分的区域。

具体放到现在的商用 JVM 里,设计者一般至少会把 Java 堆划分为新生代(Young Generation)和老年代(Old Generation)两个区域:

  • 在新生代中,每次垃圾收集时都会有大批对象死去
  • 而每次回收后存活的少量对象,将会逐步晋升到老年代中存放

这样的内存划分其实还存在一个明显的问题,那就是对象并不是孤立的,对象之间会存在跨代引用。

假如现在要进行一次只局限于新生代区域内的收集,但新生代中的对象是完全有可能被老年代所引用的,那么这个存储在新生代中被老生代引用的对象,就不应该被标记为死亡对象,所以,我们就不得不在固定的 GC Roots 之外,再额外遍历整个老年代中所有对象来确保可达性分析结果的正确性,反过来也是一样。

依据这条假说,我们就不必为了少量的跨代引用去扫描整个老年代,也不必浪费空间专门记录每一个对象是否存在及存在哪些跨代引用,只需在新生代上建立一个全局的数据结构,这个结构的作用就是把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。这个结构也被称为 记忆集

这样,当发生新生代 GC 时,对于跨代引用问题,就不需要遍历整个老生代加入 GC Roots 中,只需要把记忆集中包含了跨代引用的少量对象加入到 GC Roots 进行扫描就可以了。

不过记忆集只是一个逻辑概念,HotSpot 虚拟机的具体实现是 卡表

卡表最简单的形式是一个字节数组,数组的每一个元素都对应着老年代中一块特定大小的内存块,这个内存块被称作卡页,一个卡页中通常包含不止一个对象,只要卡页内有一个对象(或者更多个对象)的字段存在着跨代指针,那就将卡表中对应的数组元素的值标识为 1,称为这个元素变脏,没有则标识为 0。这样,在垃圾收集发生时,只要筛选出卡表中变脏的元素,就能轻易得出哪些卡页内存块中包含跨代指针,然后把它们加入 GC Roots 中一并扫描。

卡表具体是怎么维护的呢?换句话说,在对象赋值的那一刻,谁来把卡表元素变脏呢?

在 HotSpot 虚拟机里是通过 写屏障(Write Barrier)技术维护卡表状态的

写屏障可以看作虚拟机层面对 “引用类型字段赋值” 这个动作的 AOP 切面,在引用对象赋值时会产生一个环形通知,在赋值前的部分的通知叫作 写前屏障(Pre-Write Barrier),在赋值后的通知则叫作 写后屏障(Post-Write Barrier)。

使用写屏障后,其实会带来两个问题:

1)额外的开销:这个是很显然的,不过这个开销与 Minor GC 时扫描整个老年代的代价相比还是低很多的

2)高并发场景下的伪共享问题:现代中央处理器的缓存系统中是以缓存行(Cache Line)为单位存储的,当多线程修改互相独立的变量时,如果这些变量恰好共享同一个缓存行,就会彼此之间产生影响。假设处理器的缓存行大小为 64 字节,由于一个卡表元素占 1 个字节,64 个卡表元素将共享同一个缓存行,对吧。这 64 个卡表元素对应的卡页总的内存为 32KB(64×512字节),也就是说如果不同线程更新的对象正好处于这 32 KB 的内存区域内,就会导致更新卡表时正好写入同一个缓存行而导致伪共享问题。

为了避免伪共享问题,一种简单的解决方案就是更改下写屏障的执行逻辑,在将卡表元素变脏之前,加个判断,就是先检查下卡表标记,只有当该卡表元素未被标记过时才将其标记为变脏

JDK 7 之后 HotSpot 虚拟机增加了一个新的参数 -XX:+UseCondCardMark,用来决定是否开启卡表更新的条件判断。开启会增加一次额外判断的开销,但能够避免伪共享问题,两者各有性能损耗,是否打开需要根据实际情况来进行权衡

垃圾回收器的选择

针对性选择
根据垃圾回收器特点选择
  • Serial GC:适用于小型单线程的应用程序,不要求高吞吐量,对于GC暂停时间敏感。
  • Parallel GC:适用于多核机器且有大量处理器资源的应用程序,对于吞吐量要求较高,能接受较长的GC暂停时间。
  • CMS GC:适用于大型应用程序,要求 GC 暂停时间短,且应用程序具有较多的并发线程。
  • G1 GC:适用于大型内存、多核处理器且对于要求 GC 暂停时间短的应用程序,能够均衡吞吐量和暂停时间的需求,减少垃圾碎片的产生。
根据应用特点选择
  1. 内存大小:应用程序所需的内存大小对选择 GC 回收器有一定的影响。如果应用拥有足够大的内存空间,可以考虑使用 G1 GC,因为它能够更好地利用内存,并具有较高的吞吐量和较低的暂停时间。如果内存较小,则可以考虑使用 CMS GC,因为它具有较低的暂停时间。
    • 内存越大则基数越大,内存回收效率高的 GC 回收器能获取的效益更高
    • 内存大小与暂停时间正相关,内存越大,暂停时间短的 GC 回收器付出的成本更低
  2. 响应时间需求:不同的 GC 回收器对应用程序的响应时间有不同的影响。Serial GC 在单线程下工作,会造成较长的暂停时间,适用于对响应时间要求不高的后台任务。而 Parallel GC 和 CMS GC 在多线程下工作,可以实现较低的暂停时间,适用于对响应时间要求较高的应用程序。
  3. 垃圾生成速率和持久性:不同的应用程序生成垃圾的速率和垃圾的生命周期不同。如果应用程序生成的垃圾较少且生命周期短暂,可以考虑使用 Serial GC 或 Parallel GC。如果应用程序生成的垃圾较多且生命周期较长,可以考虑使用 CMS GC 或 G1 GC。
    • 垃圾少时,即使停顿,成本也不太高。此时停顿式垃圾回收器内存占用少带来的好处优于停顿带来的成本
    • 垃圾多时,如果停顿,成本会比较高。此时并发或分代回收器停顿时间少带来的好处优于内存占用的成本
  4. 系统硬件资源:系统硬件资源的情况也会对选择 GC 回收器产生影响。如果硬件资源较多,可以考虑使用 Parallel GC 或 G1 GC,因为它们能够充分利用多核处理器。如果硬件资源较少,则可以考虑使用 Serial GC 或 CMS GC。

说到底还是看应用内存大小、机器内存大小以及响应时间需求

  • 内存大,内存回收效率高 和 暂停时间短 是版本答案
  • 机器内存大时,无需考虑 GC 回收器内存占用的限制,选高性能的就好(力大砖飞)
  • 响应时间敏感时,有两个思考方向
    • 根据机器性能,如果机器性能好,就引入多线程(回归到机器内存大的情况)
    • 根据垃圾数量(具体包括垃圾生成速率和持久性),如果垃圾少,则使用停顿式也无所谓
垃圾收集器选择流程
需求优先

不能选择硬件时,先考虑应用需求(响应时间、应用内存、垃圾数量),再考虑环境限制(硬件内存)

GC 收集器选择

硬件优先

能选择硬件时,先考虑环境限制,再考虑应用需求

  • 硬件内存大时,直接无脑上 G1 GC 或 CMS GC(流放者大刀.jpg)
    流放者大刀

  • 硬件内存小时,根据垃圾数量多少选择 Serial GC 或 Parallel GC

JVM 参数调整

参考文档:JVM 参数调整

检测工具
  • JConsole:是 JDK 自带的监测工具,可以监测和管理 JVM 的运行状态,包括内存、线程、垃圾回收
  • VisualVM:也是 JDK 自带的监测工具,功能比 JConsole 更强大,可以通过插件扩展来监测各种应用。
  • Java Mission Control(JMC):也是 JDK 自带的工具,提供了对生产环境 JVM 的实时监控和分析功能,可以通过事件跟踪、飞行记录器等功能来定位性能问题。
核心评价指标

对于单台服务器而言

  • jvm.gc.time:每分钟的 GC 耗时在 1s 以内,500ms 以内尤佳
  • jvm.gc.meantime:每次 YGC 耗时在 100ms 以内,50ms 以内尤佳
  • jvm.fullgc.count:FGC 最多几小时1次,1天不到1次尤佳
  • jvm.fullgc.time:每次 FGC 耗时在 1s 以内,500ms 以内尤佳
运行时数据
  • CPU 指标
    • 占用次数
    • 占用时间
      • 单次
      • 总体
  • 内存指标
    • 占用情况
    • 变化过程
  • GC 指标
    • 查看每分钟 GC 时间是否正常
    • 查看每分钟 YGC次数是否正常
    • 查看 FGC 次数是否正常
    • 查看单次 FGC 时间是否正常
    • 查看单次 GC 各阶段详细耗时,找到耗时严重的阶段
    • 查看对象的动态晋升年龄是否正常
优化流程

参考文档:Java 中 9 种常见的 CMS GC 问题分析与解决

通用

⚠️ 警惕:避免过早优化

  1. 确定优化目标
  2. 制订优化方案
  3. 对比优化前后的指标,统计优化效果
  4. 持续观察和跟踪优化效果
GC 问题普适处理流程

img

根因鱼骨图

img

JVM 粗调

参考文档:粗调 JVM

无非就是调整 JVM 内存空间的三个参数(-Xmx -Xms -Xmn),使 GC 频率与 GC 停顿时间处于合理的区间

调整过程记得小步快跑,避免内存剧烈波动影响线上服务

  • 基于 GC 频率和停顿时间的调节

    如果一个应用的 GC 频率只有 0.02,即每秒 GC 0.02 次,那么需要 50 秒才 GC 一次,那么其 GC 频率是很低的。这时候很可能是分配了较大的新生代空间,这使得其很久才需要 GC 一次。这时候我们再看看其停顿时间,如果停顿时间也很短的话,那我们就可以判定该应用的内存有优化的空间。

    在这种情况下,一般都是缩小分配的新生代的空间。新生代空间一旦变小了,那么其分配完的时间就会缩减。一旦空间被分配完,那么就会启动进行 GC 操作。

  • 基于新生代与老年代内存占用的调节

    例如对于接口类型的系统来说,很多请求都是 1 秒中之内就结束。对于这种类型的请求,他们进入应用时会分配内存,结束时内存就会立刻被回收,留存下来的对象很少。这种应用的 JVM 内存情况大概是这样的:新生代消耗比较大,并且随着周期性回收内存,但老年代的内存消耗则更小。对于那些持续性处理的应用,例如持续时间长的应用处理。因为其存活时间较久,所以可能会有更多的对象晋升到老年代,因此老年代的内存消耗就比较大。

    通过观察 JVM 新生代与老年代的内存消耗情况,再结合应用本身的特性,我们可以发现应用中不合理的地方,再对应用进行针对性的优化。例如:应用某个地方每次都会存储大量的临时数据到内存中,这样就造成了 JVM 可能爆发 GC,从而导致应用卡顿。

    知识补充:爆发 GC

    爆发 GC(Garbage Collection)是指在短时间内连续触发多次垃圾回收的情况。具体来说,在某些情况下,垃圾收集器为了更快地释放大量的未使用的内存,会进行一次大规模的垃圾回收操作。在这种情况下,垃圾收集器会减少频繁的小规模垃圾回收,而是选择在内存使用达到一定阈值时,进行一次全量的垃圾回收。

调参流程

  • 基于 GC 频率和停顿时间:GC 频率低且停顿时间短 → 新生代内存空间分配过大

  • 基于新生代和老年代内存占用的调节:

    1. 先改代码

      • 改写代码,避免频繁的临时数据大量存储带来的爆发 GC
      • 使用对象池或缓存等技术,重复利用对象,减少对象的频繁创建和销毁
    2. 再调 JVM:增加新生代内存空间,用频繁的 Minor GC 替换 爆发 GC 的 Full GC

      作用机理:通过提升新生代的内存空间,可以让对象在新生代中更长时间地存活,减少对象进入老年代的频率,降低 Full GC 的触发次数。

JVM 调优实验

JConsole 的使用

简介

JConsole 是 Java 虚拟机自带的一款监控和管理工具,可以监控和管理本地或远程的 Java 应用程序。 JConsole 提供了多种功能,包括性能监控、线程监控、垃圾回收器的信息展示等,方便开发人员分析和调优 Java 应用程序的性能问题。JConsole 是基于 JMX(Java 管理扩展)技术实现的,可以通过远程连接方式连接到运行在远程服务器上的 Java 应用程序。 使用 JConsole,可以获取 Java 应用程序的各种信息,比如堆栈跟踪、内存使用情况、线程状态等,帮助开发人员分析和解决问题。

JConsole 的用途包括但不限于以下几个方面:

  • 监控应用程序性能,例如 CPU 利用率、内存使用情况、线程数量等
  • 识别应用程序的瓶颈,找出性能问题所在
  • 检测和解决死锁问题
  • 监控垃圾回收器的工作状态,优化内存使用

通过 JConsole,我们可以更好地了解 Java 应用程序的运行情况,并且能够针对性地进行优化和调试,以提升应用程序的性能和可靠性。

实验目的

熟悉 JConsole 的使用方法,掌握 JConsole 的主要功能和特性。

实验内容

通过使用 JConsole 进行死锁检测和运行监控

实验过程
  1. 编写一个 Java 程序,模拟出死锁的情况。
  2. 在命令行中启动该 Java 程序。
  3. 打开 JConsole 工具,在连接选项卡中选择正在运行的 Java 程序进程,并点击连接。
  4. 在 JConsole 工具中,切换到线程选项卡,可以看到当前的线程列表。如果存在死锁情况,JConsole 会自动检测死锁
  5. 在 JConsole 工具中,可以查看系统信息、内存信息、线程信息、监控垃圾回收情况等。
示例代码
public class DeadlockDemo {

    public static void main(String[] args) {
        Object lock1 = new Object();
        Object lock2 = new Object();

        Thread thread1 = new Thread(() -> {
            synchronized (lock1) {
                System.out.println("Thread 1 acquired lock1");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock2) {
                    System.out.println("Thread 1 acquired lock2");
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (lock2) {
                System.out.println("Thread 2 acquired lock2");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock1) {
                    System.out.println("Thread 2 acquired lock1");
                }
            }
        });

        thread1.start();
        thread2.start();
    }
}
jconsole

知识补充:远程连接

注意:您需要在启动 Java 进程时启用远程监视功能才能在 JConsole 中选择和连接到该进程。您可以使用以下命令启用远程监视功能(将 <port> 替换为您想要使用的端口号):

java -Dcom.sun.management.jmxremote.port=<port> -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -jar yourApp.jar

以上命令将在启动 Java 应用程序时启用了远程监视功能,并以指定端口号开放 JMX 连接,允许 JConsole 连接和监视该进程。

连接建立

image-20231030201924385

死锁检测

image-20231030202035689

性能监控

JVM 运行时参数调整

参考文档:JVM 运行时参数

参考文档:jinfo 可以实时调整的参数汇总

简介

jinfo 命令可以用于查看和调整 Java 虚拟机的运行时参数

#查看可以通过 jinfo 修改的属性
java -XX:+PrintFlagsFinal -version | grep manageable
实验目的

了解 jinfo 的基本用法,能够使用 jinfo 查询和修改 JVM 参数

实验内容

通过 jinfo 查看并修改 PrintGCDetails 属性

实验过程
  1. 复用 JConsole 使用实验中的 DeadlockDemo 代码,启动 DeadlockDemo 项目
  2. 在终端中输入 jps 以获取运行中的 Java 进程
  3. 通过 jinfo -flags <PID>jinfo -flags <ProcessName> 查看进程的基本属性
  4. 通过 jinfo -flag 查看某个属性
  5. 通过 jinfo -flag 修改某个属性
示例代码
# 获取运行中 Java 进程,加参数 -v 能看到详细信息
jps
#99735 Launcher
#98663 
#99736 DeadlockDemo
#682 Jps

# 查看基本属性
jinfo -flags DeadlockDemo
#VM Flags:
#-XX:CICompilerCount=4 -XX:InitialHeapSize=268435456 -XX:MaxHeapSize=4294967296 -XX:MaxNewSize=1431306240 -XX:MinHeapDeltaBytes=524288 -XX:NewSize=89128960 -XX:OldSize=179306496
#-XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC 

# 查看某个属性
jinfo -flag PrintGCDetails DeadlockDemo 
#-XX:-PrintGCDetails # 该属性为 false

# 修改 VM-Options
# 注意:并非所有属性都能修改
# 将属性修改为 true
jinfo -flag +PrintGCDetails DeadlockDemo

jinfo -flags DeadlockDemo
#VM Flags:
#-XX:CICompilerCount=4 -XX:InitialHeapSize=268435456 -XX:MaxHeapSize=4294967296 -XX:MaxNewSize=1431306240 -XX:MinHeapDeltaBytes=524288 -XX:NewSize=89128960 -XX:OldSize=179306496
#-XX:+PrintGCDetails # 多了这一项,此时该属性已经变成了 true
#-XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC 

JVM 堆内存调整

简介

jstat是Java Virtual Machine (JVM) 的一个性能监控和诊断工具。它可以用于收集JVM运行时的各种统计信息,包括堆内存使用情况、垃圾回收情况、类装载情况、线程情况等。通过使用jstat,可以更好地了解JVM的运行状况,优化应用程序的性能以及进行故障排除。

实验目的

了解 jstat 的基础用法,能通过 jstat 获取到垃圾回收相关信息

了解 VisualVM 的基础用法,能通过 Visual GC 插件直观查看垃圾回收相关信息

实验内容
  • 获取垃圾回收相关信息
  • 调整堆内存大小
实验过程
预备工作

参考文档:Visual GC 插件安装

高于 1.8 版本的 JDK 不再集成 VisualVM,需要额外安装

VisualVM 不带 Visual GC 插件,需要手动安装 Visual GC 插件

具体流程
  1. 复用先前的 Java 项目(DeadlockDemo)和 Spring 项目(stateless-backend

  2. 通过 jstat 查看垃圾回收相关信息

  3. 通过 VisualVM 查看垃圾回收相关信息

  4. 在 IDEA 中调整堆内存大小

    1. 设置启动配置
    2. 在修改选项中启用 VMOption
    3. 在 VMOption 中填入参数
    4. 在 VisualVM 中查看进程的 JVM 参数
  5. 基于 DeadlockDemo 编译后的 class 文件,调整堆内存大小

  6. 基于 stateless-backend 打包成的 jar 文件,调整堆内存大小

示例代码
利用 jstat 查看垃圾回收相关信息
# 查看进程 PID
jps
# 通过 PID 查看垃圾回收相关信息,注意此处不再支持按名查看
jstat -gc <PID>

image-20231031171802464

上述属性是通过运行命令 jstat -gc <PID> 获取的。

具体含义如下:

  • S0C: 年轻代中Survivor 0区的容量(KB)。
  • S1C: 年轻代中Survivor 1区的容量(KB)。
  • S0U: 年轻代中Survivor 0区的使用量(KB)。
  • S1U: 年轻代中Survivor 1区的使用量(KB)。
  • EC: 年轻代中Eden区的容量(KB)。
  • EU: 年轻代中Eden区的使用量(KB)。
  • OC: 年老代的容量(KB)。
  • OU: 年老代的使用量(KB)。
  • MC: 元数据区的容量(KB)。
  • MU: 元数据区的使用量(KB)。
  • CCSC: 压缩类空间容量(KB)。
  • CCSU: 压缩类空间使用量(KB)。
  • YGC: 年轻代垃圾收集的次数。
  • YGCT: 年轻代垃圾收集所消耗的时间(秒)。
  • FGC: 老年代垃圾收集的次数。
  • FGCT: 老年代垃圾收集所消耗的时间(秒)。
  • CGC: 压缩垃圾收集的次数。
  • CGCT: 压缩垃圾收集所消耗的时间(秒)。
  • GCT: 总垃圾收集所消耗的时间(秒)。

要查询 JVM 的堆内存大小,可以通过计算以下两个值之和得到:

  • 年轻代容量: S0C + S1C + EC
  • 年老代容量: OC

其中,单位为 KB。

利用 VisualVM 查看 垃圾回收相关信息

VisualVM 本质上是一个可视化的独立软件,安装完插件之后,启动软件,选择要监测的进程,再选择工具栏中的 Visual GC 即可

image-20231101091911152

在 IDEA 中调整堆内存大小
#样例数据
-Xms512m -Xmx2g  
  • -Xms: 设置堆内存的初始大小
  • -Xmx: 设置堆内存的最大大小

image-20231101093539823

基于 class 文件的堆内存大小调整
#注意
#1.此处要使用完整限定类名
#2.参数一定要放在前面,java DeadlockDemo -Xms512m -Xmx2g 无效
java -Xms512m -Xmx2g DeadlockDemo
基于 jar 文件的对内存大小调整
#注意:参数一定要放前面,java -jar stateless-backend-0.0.1-SNAPSHOT.jar -Xms512m -Xmx2g 无效
java -Xms512m -Xmx2g -jar stateless-backend-0.0.1-SNAPSHOT.jar
通过 VisualVM 查看虚拟机选项

image-20231101100056609