JVM调优是一个系统而又复杂的过程,但我们知道,在大多数情况下,我们基本不用去调整JVM内存分配,因为一些初始化的参数已经可以保证应用服务正常稳定地工作了。 在应用服务的特定场景下,JVM内存分配不合理带来的性能表现并不会像内存溢出问题这么突出。一般你没有深入到各项性能指标中去,是很难发现其中隐藏的性能损耗。 压测工具AB Ab(ApacheBench)测试工具是Apache提供的一款测试工具,具有简单易上手的特点,在测试Web服务时非常实用。 ab一般都是在Linux上用。 安装非常简单,只需要在Linux系统中输入yumyinstallhttpdtools命令,就可以了。 安装成功后,输入ab命令,可以看到以下信息: ab工具用来测试postget接口请求非常便捷,可以通过参数指定请求数、并发数、请求参数等。 参数的含义: n:总请求次数(最小默认为1); c:并发次数(最小默认为1且不能大于总请求次数,例如:10个请求,10个并发,实际就是1人请求1次); p:post参数文档路径(p和T参数要配合使用); T:header头内容类型(此处切记是大写英文字母T); 输出中,性能指标参考: Requestspersecond:吞吐率,指某个并发用户数下单位时间内处理的请求数; Timeperrequest:上面的是用户平均请求等待时间,指处理完成所有请求数所花费的时间(总请求数并发用户数); Timeperrequest:下面的是服务器平均请求处理时间,指处理完成所有请求数所花费的时间总请求数; Percentageoftherequestsservedwithinacertaintime:每秒请求时间分布情况,指在整个请求中,每个请求的时间长度的分布情况,例如有50的请求响应在8ms内,66的请求响应在10ms内,说明有16的请求在8ms10ms之间。 JVM堆内存分配 JVM内存分配的调优案例 一个高并发系统中的抢购接口,高峰时5W的并发请求,且每次请求会产生20KB对象(包括订单、用户、优惠券等对象数据)。 我们可以通过一个并发创建一个1MB对象的接口来模拟万级并发请求产生大量对象的场景,具体代码如下: AB压测 对应用服务进行压力测试,模拟不同并发用户数下的服务的响应情况: 1、10个并发用户10万请求量(总) 2、100个并发用户10万请求量(总) 3、1000个并发用户10万请求量(总) abc10n100000http:127。0。0。1:8080jvmheap abc100n100000http:127。0。0。1:8080jvmheap abc1000n100000http:127。0。0。1:8080jvmheap 服务器信息 我本机起一台Linux虚拟机,分配的内存为2G,处理器数量为2个。具体信息如下图: GC监控 还有一句话,无监控不调优,所以我们需要监控起来。JVM中我们使用jstat命令监控一下JVM的GC情况。 统计GC的情况:jstatgc8404500020awk{print13,14,15,16,17}。 堆空间监控 在默认不配置JVM堆内存大小的情况下,JVM根据默认值来配置当前内存大小。 我们可以通过以下命令来查看堆内存配置的默认值:javaXX:PrintFlagsFinalversiongrepHeapSize。 这台机器上启动的JVM默认最大堆内存为480MB,初始化大小为32MB。 测试项目启动 使用jmapheap这种方式,我们看到这个JVM应用占据的堆空间大小。 压测结果 1、10个并发用户10万请求量(总) 使用AB进行压力测试:abc10n100000http:127。0。0。1:8080jvmheap。 统计GC情况 jstatgc9656500020awk{print13,14,15,16,17}。 测试结果显示: 用户的吞吐量大于在1426每秒左右; JVM服务器平均请求处理时间0。7ms左右; JVM服务器发生了2700多次YGC,耗时15秒,还有45次FGC,2。3秒左右,加在一起GC耗时17秒。 2、100个并发用户10万请求量(总) 使用AB进行压力测试:abc100n100000http:127。0。0。1:8080jvmheap。 测试结果显示: 用户的吞吐量大于在1262每秒左右; JVM服务器平均请求处理时间0。8ms左右; JVM服务器发生了2700多次YGC,耗时30秒,还有56次FGC,3秒左右,加在一起GC耗时33秒。 3、1000个并发用户10万请求量(总) 使用AB进行压力测试:abc1000n100000http:127。0。0。1:8080jvmheap。 测试结果显示: 用户的吞吐量大于在1145每秒左右; JVM服务器平均请求处理时间0。8ms左右; JVM服务器发生了2700多次YGC,耗时38秒,还有47次FGC,3秒左右,加在一起GC耗时42秒。 结果分析 GC频率 高频的FullGC会给系统带来非常大的性能消耗,虽然MinorGC相对FullGC来说好了许多,但过多的MinorGC仍会给系统带来压力。 内存 这里的内存指的是堆内存大小,堆内存又分为年轻代内存和老年代内存。堆内存不足,会增加MinorGC,影响系统性能。 吞吐量 频繁的GC将会引起线程的上下文切换,增加系统的性能开销,从而影响每次处理的线程请求,最终导致系统的吞吐量下降。 延时 JVM的GC持续时间也会影响到每次请求的响应时间。 调优方案 调整方案一 调整堆内存空间减少GC:通过分析,堆内存基本被用完了,而且存在大量MinorGC和FullGC,这意味着我们的堆内存严重不足,这个时候我们需要调大堆内存空间。 堆空间加大到1。5G javajarXms1500mXmx1500mjvm1。0SNAPSHOT。jar。 使用AB进行压力测试:abc10n100000http:127。0。0。1:8080jvmheap。 测试结果显示: 用户的吞吐量大于在1205每秒左右 JVM服务器平均请求处理时间0。83ms左右 JVM服务器发生了800次YGC,耗时33秒,还有1次FGC,1秒左右,加在一起GC耗时34秒 使用AB进行压力测试: abc100n100000http:127。0。0。1:8080jvmheap。 测试结果显示: 用户的吞吐量大于在989每秒左右 JVM服务器平均请求处理时间1。01ms左右 JVM服务器发生了800次YGC,耗时46秒,还有8次FGC,6秒左右,加在一起GC耗时52秒 使用AB进行压力测试: abc1000n100000http:127。0。0。1:8080jvmheap。 测试结果显示: 用户的吞吐量大于在749每秒左右 JVM服务器平均请求处理时间1。3ms左右 JVM服务器发生了800次YGC,耗时66秒,还有8次FGC,9秒左右,加在一起GC耗时75秒 调整方案二 javajarXms1500mXmx1500mXmn1000mXX:SurvivorRatio8jvm1。0SNAPSHOT。jar 使用AB进行压力测试: abc10n100000http:127。0。0。1:8080jvmheap。 测试结果显示: 用户的吞吐量大于在1780每秒左右 JVM服务器平均请求处理时间0。56ms左右 JVM服务器发生了400次YGC,耗时5。8秒,还有2次FGC,0。1秒左右,加在一起GC耗时6秒 使用AB进行压力测试: abc100n100000http:127。0。0。1:8080jvmheap。 测试结果显示: 用户的吞吐量大于在1927每秒左右 JVM服务器平均请求处理时间0。51ms左右 JVM服务器发生了400多次YGC,耗时11秒,没有FGC,加在一起GC耗时11秒 使用AB进行压力测试: abc1000n100000http:127。0。0。1:8080jvmheap。 测试结果显示: 用户的吞吐量大于在1657每秒左右 JVM服务器平均请求处理时间0。6ms左右 JVM服务器发生了400多次YGC,耗时14秒,还1次FGC,3秒左右,加在一起GC耗时17秒 内存优化总结 一般情况下,高并发业务场景中,需要一个比较大的堆空间,而默认参数情况下,堆空间不会很大。所以我们有必要进行调整。 但是不要单纯的调整堆的总大小,要调整新生代和老年代的比例,以及Eden区还有From区,还有To区的比例。 所以在我们上述的测试中,调整方案二,得到结果是最好的。在三种测试情况下都能够有非常好的性能指标,同时GC耗时相对控制也较好。 对于调整方案一,就是单纯的加大堆空间,里面的比例不适合高并发场景,反而导致堆空间变大,没有明显减少GC的次数,但是每次GC需要检索对象的堆空间更大,所以GC耗时更长。 方案二:调整为一个很大的新生代和一个较小的老年代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而老年代尽存放长期存活对象。 由于新生代空间较小,Eden区很快被填满,就会导致频繁MinorGC,因此我们可以通过增大新生代空间来降低MinorGC的频率。单次MinorGC时间是由两部分组成:T1(扫描新生代)和T2(复制存活对象)。 默认情况:一个对象在Eden区的存活时间为500ms,MinorGC的时间间隔是300ms,因为这个对象存活时间间隔时间,那么正常情况下,MinorGC的时间为:T1T2。 方案一:整堆空间加大,但是新生代没有增大多少,对象在Eden区的存活时间为500ms,MinorGC的时间可能会扩大到400ms,因为这个对象存活时间间隔时间,那么正常情况下,MinorGC的时间为:T11。5(Eden区加大了)T2。 方案二:当我们增大新生代空间,MinorGC的时间间隔可能会扩大到600ms,此时一个存活500ms的对象就会在Eden区中被回收掉,此时就不存在复制存活对象了,所以再发生MinorGC的时间为:即T12(空间大了)T20。 可见,扩容后,MinorGC时增加了T1,但省去了T2的时间。 在JVM中,复制对象的成本要远高于扫描成本。如果在堆内存中存在较多的长期存活的对象,此时增加年轻代空间,反而会增加MinorGC的时间。如果堆中的短期对象很多,那么扩容新生代,单次MinorGC时间不会显著增加。因此,单次MinorGC时间更多取决于GC后存活对象的数量,而非Eden区的大小。 这个就解释了之前的内存调整方案中,方案一为什么性能还差些,但是到了方案二话,性能就有明显的上升。 推荐策略 1。新生代大小选择 响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,新生代收集发生的频率也是最小的。同时,减少到达老年代的对象。 吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。 避免设置过小。当新生代设置过小时会导致:1。MinorGC次数更加频繁2。可能导致MinorGC对象直接进入老年代,如果此时老年代满了,会触发FullGC。 2。老年代大小选择 响应时间优先的应用:老年代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片,高回收频率以及应用暂停而使用传统的标记清除方式; 如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得: 并发垃圾收集信息、持久代并发收集次数、传统GC信息、花在新生代和老年代回收上的时间比例。 吞吐量优先的应用: 一般吞吐量优先的应用都有一个很大的新生代和一个较小的老年代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而老年代尽存放长期存活对象。 GC优化 GC性能衡量指标 吞吐量: 这里的衡量吞吐量是指应用程序所花费的时间和系统总运行时间的比值。我们可以按照这个公式来计算GC的吞吐量:系统总运行时间应用程序耗时GC耗时。如果系统运行了100分钟,GC耗时1分钟,则系统吞吐量为99。GC的吞吐量一般不能低于95。 停顿时间: 指垃圾回收器正在运行时,应用程序的暂停时间。对于串行回收器而言,停顿时间可能会比较长;而使用并发回收器,由于垃圾收集器和应用程序交替运行,程序的停顿时间就会变短,但其效率很可能不如独占垃圾收集器,系统的吞吐量也很可能会降低。 垃圾回收频率: 通常垃圾回收的频率越低越好,增大堆内存空间可以有效降低垃圾回收发生的频率,但同时也意味着堆积的回收对象越多,最终也会增加回收时的停顿时间。所以我们需要适当地增大堆内存空间,保证正常的垃圾回收频率即可。 分析GC日志 通过JVM参数预先设置GC日志,几种JVM参数设置如下: XX:PrintGC输出GC日志 XX:PrintGCDetails输出GC的详细日志 XX:PrintGCTimeStamps输出GC的时间戳(以基准时间的形式) XX:PrintGCDateStamps输出GC的时间戳(以日期的形式,如20130504T21:53:59。2340800) XX:PrintHeapAtGC在进行GC的前后打印出堆的信息 Xloggc:。。logsgc。log日志文件的输出路径 案例 比如:导出前面测试案例中,默认情况下的gc日志 javajarXX:PrintGCDateStampsXX:PrintGCDetailsXloggc:。gclogsjvm1。0SNAPSHOT。jar 1、进行1000个并发用户10万请求量的压力测试,得到gclogs日志 javajarXX:PrintGCDateStampsXX:PrintGCDetailsXloggc:。gc2logsXms1500mXmx1500mXmn1000mXX:SurvivorRatio8jvm1。0SNAPSHOT。jar 2、进行1000个并发用户10万请求量的压力测试,得到gc2logs日志 使用日志工具gcViewer,我们就暂停这项进行对比: 明显第一个暂停总耗时比第二个要多很多,一个是58秒,一个是15秒左右,相差很多,这个本质上也可以分析出来,对于系统来说,第二个的GC日志情况更加的好。 GC调优策略 降低MinorGC频率 由于新生代空间较小,Eden区很快被填满,就会导致频繁MinorGC,因此我们可以通过增大新生代空间来降低MinorGC的频率。单次MinorGC时间是由两部分组成:T1(扫描新生代)和T2(复制存活对象)。 情况1:假设一个对象在Eden区的存活时间为500ms,MinorGC的时间间隔是300ms,因为这个对象存活时间间隔时间,那么正常情况下,MinorGC的时间为:T1T2。 情况2:当我们增大新生代空间,MinorGC的时间间隔可能会扩大到600ms,此时一个存活500ms的对象就会在Eden区中被回收掉,此时就不存在复制存活对象了,所以再发生MinorGC的时间为:即T12(空间大了)T20。 可见,扩容后,MinorGC时增加了T1,但省去了T2的时间。 在JVM中,复制对象的成本要远高于扫描成本。如果在堆内存中存在较多的长期存活的对象,此时增加年轻代空间,反而会增加MinorGC的时间。如果堆中的短期对象很多,那么扩容新生代,单次MinorGC时间不会显著增加。因此,单次MinorGC时间更多取决于GC后存活对象的数量,而非Eden区的大小。 这个就解释了之前的内存调整方案中,方案一为什么性能还差些,但是到了方案二话,性能就有明显的上升。 降低FullGC的频率 由于堆内存空间不足或老年代对象太多,会触发FullGC,频繁的FullGC会带来上下文切换,增加系统的性能开销。 减少创建大对象:在平常的业务场景中,我们一次性从数据库中查询出一个大对象用于web端显示。比如,一次性查询出60个字段的业务操作,这种大对象如果超过年轻代最大对象阈值,会被直接创建在老年代;即使被创建在了年轻代,由于年轻代的内存空间有限,通过MinorGC之后也会进入到老年代。这种大对象很容易产生较多的FullGC。 增大堆内存空间:在堆内存不足的情况下,增大堆内存空间,且设置初始化堆内存为最大堆内存,也可以降低FullGC的频率。 选择合适的GC回收器 如果要求每次操作的响应时间必须在500ms以内。这个时候我们一般会选择响应速度较快的GC回收器,堆内存比较小的情况下(6G)选择CMS(ConcurrentMarkSweep),如果回收器和堆内存比较大的情况下(8G)G1回收器。 总结 GC调优是个很复杂、很细致的过程,要根据实际情况调整,不同的机器、不同的应用、不同的性能要求调优的手段都是不同的,这些都需要大家平时去积累,去观察,去实践。 如果本文对你有帮助,别忘记给我个3连,点赞,转发,评论, ,咱们下期见!答案获取方式:已赞已评已关