前面我们学习了整个 JVM 系列,最终目标的不仅仅是了解 JVM 的基础知识,也是为了进行 JVM 性能调优做准备。这篇文章带领大家学习 JVM 性能调优的知识。
性能调优
性能调优包含多个层次,比如:架构调优、代码调优、JVM 调优、数据库调优、操作系统调优等。
架构调优和代码调优是 JVM 调优的基础,其中架构调优是对系统影响最大的。
性能调优基本上按照以下步骤进行:明确优化目标、发现性能瓶颈、性能调优、通过监控及数据统计工具获得数据、确认是否达到目标。
何时进行 JVM 调优
遇到以下情况,就需要考虑进行 JVM 调优了:
- Heap 内存(老年代)持续上涨达到设置的最大内存值;
- Full GC 次数频繁;
- GC 停顿时间过长(超过 1 秒);
- 应用出现 OutOfMemory 等内存异常;
- 应用中有使用本地缓存且占用大量内存空间;
- 系统吞吐量与响应性能不高或下降。
JVM 调优的基本原则
JVM 调优是一个手段,但并不一定所有问题都可以通过 JVM 进行调优解决,因此,在进行 JVM 调优时,我们要遵循一些原则:
- 大多数的 Java 应用不需要进行 JVM 优化;
- 大多数导致 GC 问题的原因是代码层面的问题导致的(代码层面);
- 上线之前,应先考虑将机器的 JVM 参数设置到最优;
- 减少创建对象的数量(代码层面);
- 减少使用全局变量和大对象(代码层面);
- 优先架构调优和代码调优,JVM 优化是不得已的手段(代码、架构层面);
- 分析 GC 情况优化代码比优化 JVM 参数更好(代码层面);
通过以上原则,我们发现,其实最有效的优化手段是架构和代码层面的优化,而 JVM 优化则是最后不得已的手段,也可以说是对服务器配置的最后一次“压榨”。
JVM 调优目标
调优的最终目的都是为了令应用程序使用最小的硬件消耗来承载更大的吞吐。jvm 调优主要是针对垃圾收集器的收集性能优化,令运行在虚拟机上的应用能够使用更少的内存以及延迟获取更大的吞吐量。
- 延迟:GC 低停顿和 GC 低频率;
- 低内存占用;
- 高吞吐量;
其中,任何一个属性性能的提高,几乎都是以牺牲其他属性性能的损为代价的,不可兼得。具体根据在业务中的重要性确定。
JVM 调优量化目标
下面展示了一些 JVM 调优的量化目标参考实例:
- Heap 内存使用率 <= 70%;
- Old generation 内存使用率 <= 70%;
- avgpause <= 1 秒;
- Full gc 次数 0 或 avg pause interval >= 24 小时 ;
注意:不同应用的 JVM 调优量化目标是不一样的。
JVM 调优的步骤
一般情况下,JVM 调优可通过以下步骤进行:
- 分析 GC 日志及 dump 文件,判断是否需要优化,确定瓶颈问题点;
- 确定 JVM 调优量化目标;
- 确定 JVM 调优参数(根据历史 JVM 参数来调整);
- 依次调优内存、延迟、吞吐量等指标;
- 对比观察调优前后的差异;
- 不断的分析和调整,直到找到合适的 JVM 参数配置;
- 找到最合适的参数,将这些参数应用到所有服务器,并进行后续跟踪。
以上操作步骤中,某些步骤是需要多次不断迭代完成的。一般是从满足程序的内存使用需求开始的,之后是时间延迟的要求,最后才是吞吐量的要求,要基于这个步骤来不断优化,每一个步骤都是进行下一步的基础,不可逆行之。
JVM 参数
JVM 调优最重要的工具就是 JVM 参数了。先来了解一下 JVM 参数相关内容。
-XX 参数被称为不稳定参数,此类参数的设置很容易引起 JVM 性能上的差异,使 JVM 存在极大的不稳定性。如果此类参数设置合理将大大提高 JVM 的性能及稳定性。
不稳定参数语法规则包含以下内容。
布尔类型参数值:
- -XX:+<option> ‘+’ 表示启用该选项
- -XX:-<option> ‘-‘ 表示关闭该选项
数字类型参数值:
- -XX:<option>=<number> 给选项设置一个数字类型值,可跟随单位,例如:’m’ 或 ’M’ 表示兆字节;’k’ 或 ’K’ 千字节;’g’ 或 ’G’ 千兆字节。32K 与 32768 是相同大小的。
字符串类型参数值:
- -XX:<option>=<string> 给选项设置一个字符串类型值,通常用于指定一个文件、路径或一系列命令列表。例如:-XX:HeapDumpPath=./dump.core
JVM 参数解析及调优
比如以下参数示例:
-Xmx4g –Xms4g –Xmn1200m –Xss512k -XX:NewRatio=4 -XX:SurvivorRatio=8 -XX:PermSize=100m -XX:MaxPermSize=256m -XX:MaxTenuringThreshold=15
上面为 Java7 及以前版本的示例,在 Java8 中永久代的参数 -XX:PermSize 和 -XX:MaxPermSize 已经失效。这在前面章节中已经讲到。
参数解析:
- -Xmx4g:堆内存最大值为 4GB。
- -Xms4g:初始化堆内存大小为 4GB。
- -Xmn1200m:设置年轻代大小为 1200MB。增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun 官方推荐配置为整个堆的 3 /8。
- -Xss512k:设置每个线程的堆栈大小。JDK5.0 以后每个线程堆栈大小为 1MB,以前每个线程堆栈大小为 256K。应根据应用线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在 3000~5000 左右。
- -XX:NewRatio=4:设置年轻代(包括 Eden 和两个 Survivor 区)与年老代的比值(除去持久代)。设置为 4,则年轻代与年老代所占比值为 1:4,年轻代占整个堆栈的 1 /5
- -XX:SurvivorRatio=8:设置年轻代中 Eden 区与 Survivor 区的大小比值。设置为 8,则两个 Survivor 区与一个 Eden 区的比值为 2:8,一个 Survivor 区占整个年轻代的 1 /10
- -XX:PermSize=100m:初始化永久代大小为 100MB。
- -XX:MaxPermSize=256m:设置持久代大小为 256MB。
- -XX:MaxTenuringThreshold=15:设置垃圾最大年龄。如果设置为 0 的话,则年轻代对象不经过 Survivor 区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在 Survivor 区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。
新生代、老生代、永久代的参数,如果不进行指定,虚拟机会自动选择合适的值,同时也会基于系统的开销自动调整。
可调优参数:
-Xms:初始化堆内存大小,默认为物理内存的 1 /64(小于 1GB)。
-Xmx:堆内存最大值。默认 (MaxHeapFreeRatio 参数可以调整) 空余堆内存大于 70% 时,JVM 会减少堆直到 -Xms 的最小限制。
-Xmn:新生代大小,包括 Eden 区与 2 个 Survivor 区。
-XX:SurvivorRatio=1:Eden 区与一个 Survivor 区比值为 1:1。
-XX:MaxDirectMemorySize=1G:直接内存。报 java.lang.OutOfMemoryError: Direct buffer memory 异常可以上调这个值。
-XX:+DisableExplicitGC:禁止运行期显式地调用 System.gc()来触发 fulll GC。
注意: Java RMI 的定时 GC 触发机制可通过配置 -Dsun.rmi.dgc.server.gcInterval=86400 来控制触发的时间。
-XX:CMSInitiatingOccupancyFraction=60:老年代内存回收阈值,默认值为 68。
-XX:ConcGCThreads=4:CMS 垃圾回收器并行线程线,推荐值为 CPU 核心数。
-XX:ParallelGCThreads=8:新生代并行收集器的线程数。
-XX:MaxTenuringThreshold=10:设置垃圾最大年龄。如果设置为 0 的话,则年轻代对象不经过 Survivor 区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在 Survivor 区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。
-XX:CMSFullGCsBeforeCompaction=4:指定进行多少次 fullGC 之后,进行 tenured 区 内存空间压缩。
-XX:CMSMaxAbortablePrecleanTime=500:当 abortable-preclean 预清理阶段执行达到这个时间时就会结束。
在设置的时候,如果关注性能开销的话,应尽量把永久代的初始值与最大值设置为同一值,因为永久代的大小调整需要进行 FullGC 才能实现。
内存优化示例
当 JVM 运行稳定之后,触发了 FullGC 我们一般会拿到如下信息:
以上 gc 日志中,在发生 fullGC 之时,整个应用的堆占用以及 GC 时间。为了更加精确需多次收集,计算平均值。或者是采用耗时最长的一次 FullGC 来进行估算。上图中,老年代空间占用在 93168kb(约 93MB),以此定为老年代空间的活跃数据。则其他堆空间的分配,基于以下规则来进行。
- java heap:参数 -Xms 和 -Xmx,建议扩大至 3 - 4 倍 FullGC 后的老年代空间占用。
- 永久代:-XX:PermSize 和 -XX:MaxPermSize,建议扩大至 1.2-1.5 倍 FullGc 后的永久带空间占用。
- 新生代:-Xmn,建议扩大至 1 -1.5 倍 FullGC 之后的老年代空间占用。
- 老年代:2- 3 倍 FullGC 后的老年代空间占用。
基于以上规则,则对参数定义如下:
java -Xms373m -Xmx373m -Xmn140m -XX:PermSize=5m -XX:MaxPermSize=5m
延迟优化示例
对延迟性优化,首先需要了解延迟性需求及可调优的指标有哪些。
- 应用程序可接受的平均停滞时间: 此时间与测量的 Minor
- GC 持续时间进行比较。可接受的 Minor GC 频率:Minor
- GC 的频率与可容忍的值进行比较。
- 可接受的最大停顿时间: 最大停顿时间与最差情况下 FullGC 的持续时间进行比较。
- 可接受的最大停顿发生的频率:基本就是 FullGC 的频率。
其中,平均停滞时间和最大停顿时间,对用户体验最为重要。对于上面的指标,相关数据采集包括:MinorGC 的持续时间、统计 MinorGC 的次数、FullGC 的最差持续时间、最差情况下,FullGC 的频率。
如上图,Minor GC 的平均持续时间 0.069 秒,MinorGC 的频率为 0.389 秒一次。
新生代空间越大,Minor GC 的 GC 时间越长,频率越低。如果想减少其持续时长,就需要减少其空间大小。如果想减小其频率,就需要加大其空间大小。
这里以减少了新生代空间 10% 的大小,来减小延迟时间。在此过程中,应该保持老年代和持代的大小不变化。调优后的参数如下变化:
java -Xms359m -Xmx359m -Xmn126m -XX:PermSize=5m -XX:MaxPermSize=5m
吞吐量调优
吞吐量调优主要是基于应用程序的吞吐量要求而来的,应用程序应该有一个综合的吞吐指标,这个指标基于整个应用的需求和测试而衍生出来的。
评估当前吞吐量和目标差距是否巨大,如果在 20% 左右,可以修改参数,加大内存,再次从头调试,如果巨大就需要从整个应用层面来考虑,设计以及目标是否一致了,重新评估吞吐目标。
对于垃圾收集器来说,提升吞吐量的性能调优的目标就是尽可能避免或者很少发生 FullGC 或者 Stop-The-World 压缩式垃圾收集(CMS),因为这两种方式都会造成应用程序吞吐降低。尽量在 MinorGC 阶段回收更多的对象,避免对象提升过快到老年代。
调优工具
借助 GCViewer 日志分析工具,可以非常直观地分析出待调优点。可从以下几方面来分析:
Memory, 分析 Totalheap、Tenuredheap、Youngheap 内存占用率及其他指标,理论上内存占用率越小越好;
Pause,分析 Gc pause、Fullgc pause、Total pause 三个大项中各指标,理论上 GC 次数越少越好,GC 时长越小越好;
原文链接:《JVM 性能调优详解》
本文参考:
(1)https://blog.csdn.net/jisuanj…
(2)https://juejin.im/post/59f02f…
《面试官》系列文章:
- 《JVM 之内存结构详解》
- 《面试官,不要再问我“Java GC 垃圾回收机制”了》
- 《面试官,Java8 JVM 内存结构变了,永久代到元空间》
- 《面试官,不要再问我“Java 垃圾收集器”了》
- 《Java 虚拟机类加载器及双亲委派机制》
- 《Java 内存模型 (JMM) 详解》
- 《Java 内存模型相关原则详解》
- 《JVM 性能调优详解》
<center>程序新视界:精彩和成长都不容错过 </center>