JVM - 聊聊调优那些事,调优我们需要注意啥?

JVM - 聊聊调优那些事,调优我们需要注意啥?

 大家一起学到这一章,其实对JVM就已经有了不错的认识并且可能已经产生了自己的见解。这篇文章我主要的目的是想以一种轻松的方式和大家分享一下我在这段时间学习JVM相关知识后自己的一些理解和看法,有问题希望大家指出,一起探讨交流。

1.传统项目和互联网项目有什么区别?

 其实我们大家平时工作的项目主要就分为传统项目和互联网项目,但是无论是哪一种其实最后都可能会存在JVM的问题。这里我们可以稍稍做一个比较来给大家分享下我的一些看法。

 首先是传统项目,我提几点我的理解,大多数接触过传统项目的小伙伴应该也有所感受。

  1. 用户群体比较固定:传统IT项目主要通过和某些其他单位或者个人有合作,会针对性地开发出一套系统以在特定范围内解决特定领域的问题。
  2. 以用户为中心的思想较弱:由于用户群体比较固定,比如开发某套系统仅供某客户公司内部员工使用,这也就导致了系统就算不好但也不得不用。传统IT项目更多时候以合作客户为中心而不是以使用用户为中心。
  3. 需求被动且明确:只要合作客户有需要,就会尽力把需求明确并开发。并且一旦需求明确之后,会尽可能按照严格要求开发,严格控制需求变更,需求上线前会经过多次测试验证,上线后则尽量不变动。

 这里针对以上几点我说一下我对互联网项目的理解。

  1. 用户群体不固定:互联网项目的目的主要是解决我们每一个身处互联网范围覆盖内用户的生活需求。比如我们公司主要开发互联网电视相关的业务,让每个使用我们公司产品的用户更快更好地去获取更优质的互联网影视资源,有可能在项目初期我们由于某种影视资源倾斜等各种原因主要面向少儿群体,但由于公司的发展策略会各种纵横横向扩展业务线,从而整个产品面向的用户群体就会不断扩散。
  2. 以用户为中心:互联网项目核心目的就是提供优质的服务,所以用户体验一定要好才能够吸引更多的用户,否则用户就会流失到其他同类产品中去。
  3. 需求主动且迭代快:互联网产品更多时候需要公司更主动更积极去挖掘用户新的需求开发新的功能提供新的体验。由于同类产品竞争大所以某项新功能从立项到上线时间很短,需要更快投入到市场上第一时间吸入用户。并且由于互联网产品的特性,其他公司产品也很快会上线同类型功能,所以需要通过快速迭代和竞争对手拉开差距。

2.项目如何进行调优?

2.1 项目可能出现的常见问题?

 小伙伴们都知道传统IT项目中接触比较频繁的就是上传下载。各种业务数据、员工数据、表单为了方便操作都会开发一个上传下载的功能。但是当操作的文件数据过大时,只要没考虑周全、处理得当,其实是很容易发生各种问题的,另外一个就是服务单点故障。

 而互联网项目中比较常见的就是服务接口性能瓶颈、多线程异常,内存溢出等问题。

 其实不管是哪一种项目,在排除了各个层面异常之后,都是可以通过JVM的调优对其进行一定程度的优化的。

2.2 回顾MionorGC和FullGC

 下面我会通过几个简单的示例来介绍如何对项目进行JVM相关的调优从而达到一个更理想的效果。在此之前,我们先来回顾一下MionorGCFullGC。因为在很多情况下,JVM相关的问题都离不开GC的关系。

Minor GC:当Eden区域不足分配时就会触发。

Full GC

  1. 调用System.gc()会建议JVM进行Full GC,虽然真正是否执行由JVM决定,但在很大程度上还是会触发,并且强行执行可能会影响到GC正常执行,所以建议尽量少使用。当然我们也可以通过-XX:+ DisableExplicitGC参数来禁止该方法的调用。
  2. 老年代空间不足时也会触发Full GC。之前我们知道了大对象会直接进入老年代,并且长期存活的对象也会进入老年代,若Full GC后老年代空间仍不足则会抛出OutOfMemoryError
  3. 空间分配担保失败时会触发Full GC。之前我们有介绍过使用复制算法在进行Minor GC阶段需要使用老年代的部分空间进行担保,如果担保后仍然分配失败则会触发一次Full GC

2.3 GC调优(示例1)

 关于JVM调优,可以通过选择合适的垃圾收集器、合适的垃圾算法,这里我们主要介绍如何通过JVM参数对项目进行调优。

 JVM调优其实主要就是调整两个指标:

  1. 停顿时间:GC中断应用执行的时间。
  2. 吞吐量:除去GC时间占总时间比例,例如GC时间1/(1+n)则吞吐量为n/(1+n)

2.3.1 输出GC日志

 要想对项目进行调优,就需要有依据。这里我随便找了一个之前的项目,在启动时加上VM参数-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:E://gc.log,这行参数主要就是将GC产生日志记录到一个文件中。

2.3.2 分析GC日志(GC Easy)

 我们找到输出的GC日志,通过日志我们可以很明显分析出这次Full GC是由于元空间不足导致的。
在这里插入图片描述
 之前给大家介绍过一款PerfMa分析工具,那款工具主要针对堆和线程快照进行分析。这里再给大家安利一款【GC Easy】,这款工具可以针对我们的GC LOG进行分析,更直观看到各项指标,如果大家愿意掏一点小钱的话他甚至还会给你一点意见,还是挺好用的。这里我就用免费版来带大家了解下这款工具。
在这里插入图片描述
 选中我们的GC日志点击分析,稍等一小会。
在这里插入图片描述
 整个分析过程还是挺快的,当然我这个GC文件也不大。这里我们可以看到有一个Recommendations,这个如果你是付费用户的话他就会根据GC日志分析的结果给出一定的建议,虽然建议还挺中肯,不过要针对特定业务的话还是需要自己去稍加分析。

 首先我们看到这个JVM memory size,这里给我们统计出了各代中内存分配及其峰值大小,还绘制了一张图更直观就能够进行查看和比较。
在这里插入图片描述
Key Performance Indicators这个对于我们调优来说是一个比较重要的东西,是一个关键指标。主要用来统计应用程序在执行过程中的一个吞吐量GC平均停顿时间GC最大停顿时间,这几个指标都是我们系统性能评判的一个标准,大家可以着重关注下。
在这里插入图片描述
Interactive Graphs是一个关系图谱,描述了包括Heap GC前后使用情况每次GC时长以及新生代/老年代 GC前后分配情况等。
在这里插入图片描述
GC Statistics是用来统计所有GC情况记录的,可以很清楚地看到每种GC总共发生了多少次、回收了多少空间、消耗了多少时间等各种GC相关信息。
在这里插入图片描述
GC Causes帮我们列举了我们应用GC造成的原因。例如这里告诉我们应用有6次元空间触碰到了阈值以及6次分配时间,还能够看到每种原因所花费的各种时间以及占比。

 当我们GC日志过大时,通过GC Easy其实对于我们分析整个系统还是更加高效地。大家可以
去导入一份GC日志尝试使用GC Easy去分析感受一下,不过对原生日志的分析技能我们也不能丢掉噢。

2.3.3 分析GC原因,调整参数进行调优

 我们通过上一步,得出了由于元空间分配不足引起了多次分配失败导致触发了Full GC。我们还通过GC Easy看到了我们应用的各项指标,很容易就确定了是由于元空间不足造成的GC。

 这里我们对JVM参数稍作调整-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:MetaspaceSize=128M -XX:MaxMetaspaceSize=128M -Xloggc:E://gc1.log,修改一下元空间的大小再来看一看情况怎么样。
在这里插入图片描述
 首先我们来看一下头部,和之前比较各个空间的内存分配都有了一定程度的下降,尤其是元空间。另外细心的小伙伴会发现Recommendations木有了,这说明通过对这次的GC日志进行分析并没有发现有不符合常理的情况。
在这里插入图片描述
 我们再来看看关键指标,这里可以看到我们的吞吐量已经从85%提升到了97.5%,而且GC停顿时间也大幅下降了。在这里插入图片描述
 拉到GC统计可以看到Full GC已经没有发生了,那么这就表明我们通过分析以及对JVM参数的调整,对整个应用程序起到了一个不错的调优效果。大家也可以试着找一些性能发生瓶颈的项目去自己进行分析调优,这样可以让大家对GC的调优有更深刻的认识。

2.4 GC调优(示例2)

2.4.1 分析原因

 这里我给大家再简单描述另一个场景,大家一起来跟着思考该如何去优化?

 假设我们有一个会员服务部署了集群,每个节点是4核8G的机器。现在有一个会员订单的业务,假设每秒会产生300个订单,以会员订单对象为例假设每个订单对象大小1KB,那么每秒就会产生300KB的订单对象。

 由于我们订单业务里面还涉及了库存、优惠券、积分、权益等各种其他相关对象,我们将数据量放大10倍(300KB x 10)。另外除了这些对象,我们还可能包括库存查询、优惠券计算、积分统计修改、权益匹配、会员订单数据同步推送等各种关联业务,将数据量再次放大20倍(3000KB x 20),此时我们每秒会产生60M的一个数据量。

 大家注意这里我们的各种放大只是因为我们后面需要达到这个数值,并非真实过程中就是这样的,大家不需要太注重这个数据量怎么膨胀的。

 这里先给大家看下我们启动时的JVM参数:-Xms3072M -Xmx3072M -XX:MaxMetaspaceSize=512M -XX:MetaspaceSize=512M。这样看好像并没有什么问题,但是我们程序跑一段时间后发现越来越慢,当我们把GC日志拉下来后发现总是隔一会就进行Full GC,这是怎么回事呢?

 为了更直观,我这里简单地画出了我们这个应用的堆分配情况方便大家理解。
在这里插入图片描述
 我们开始说了我们的程序每秒会产生大概60M的对象,那么我们也知道对象是会优先在Eden区域进行分配。这里Eden区800M的话大概我们每隔800/60≈13s就会触发一次Minor GC

 大家思考一下,其实当我们触发Minor GC时,如果我们这里以秒为单位去衡量的话,那么除了最后一秒产生的对象依然存活,之前的所有对象其实都已经无效了。那么Minor GC就会将最后一秒产生的60M对象放到S0区,然后将Eden区其他对象进行回收,如果仅仅是如此那么可能还没有关系,但是我们之前知道一个对象要进入到老年代有几种情况:大对象直接分配在老年代Survivor区中年龄达到阈值对象进入老年代相同年龄对象大于Survivor空间一半直接进入老年代

 这里60M对象就这样顺理成章进入老年代了,那么2G的老年代够我们玩多久呢?大概够我们来33次,那么33 x 13大概7分钟就会占满一次老年代触发一次Full GC。也难怪我们的程序总是会变慢了。

2.4.2 对症下药

 通过上面我们已经知道了当服务运行时间长了会变慢的原因,那么我们来看看怎么调优。

 我们适当调整一下VM参数:-Xms3072M -Xmx3072M -Xmn2048M -XX:MaxMetaspaceSize=512M -XX:MetaspaceSize=512M,这里我们将新生代大小设置为了2G,堆中的一个空间占比如下图。
在这里插入图片描述
 我们再来看看我们的服务会怎样去运行,是否达到了我们所要的效果呢?

 同样的每秒产生60M对象,每26s发生了一次Minor GC,此时最后一秒产生的60M对象进入S0不会进入老年代,并且Eden区中对象会被回收。

 下一次流程也就是26s后Eden区再次满了触发Minor GC,此时最后一秒产生的60M对象会分配到S1区,并会将Eden和另一块非空的SurvivorS0区进行垃圾回收。这个流程反复执行会发现,我们业务所进行的对象分配由于其性质,会完全在年轻代中分配和回收不会进入老年代,所以Full GC也就不复存在了。

3.JVM优化需要注意啥?

 上面我们通过两个小示例来对GC调优有了一个简单的认识。我们如果要避免上面原因引起的应用问题,其实就是尽量去规避Full GC,需要做的就是尽可能地让对象在Minor GC阶段被回收。让对象在新生代中多存活一段时间并且尽量避免创建过大的对象和数组。

 我们要熟练地对JVM进行调优,就一定要对对象在内存中的分配方式和回收策略熟悉。下面介绍一些我们在JVM调优时的通用思路策略。

3.1 让对象留在年轻代

 我们都知道Full GC的成本远高于Minor GC,我们的服务经常出现"卡"其实就很可能是因为应用在频繁进行Full GC

 JVM会优先在Eden区分配对象,但若由于空间分配不足触发Minor GC后会将存活的对象往Survivor区转移。倘若Survivor区也不足以分配或对象所需空间占总量超过50%,那么这批对象会直接放到老年代,久而久之就和我们上面示例一样,隔一段时间触发一次Full GC

 我们可以通过设置一个合理的Eden区和Survivor区,或者调整相应的空间比例尽可能去将对象留在年轻代中被回收掉,避免更多的对象进入老年代,从而可以有效地避免频繁Full GC。使用最多的VM参数就是-Xmn去调整年轻代的大小。

3.2 让合适的大对象进入老年代

 上面我们说了我们需要如果将对象留在年轻代,但是有一种情况是我们需要注意的,那就是大对象。不知道大家是否还记得在【JVM - 内功修炼之内存分配与回收策略】中,我给大家讲过一个大对象直接分配在老年代的情况,那么JVM会无缘无故弄出这样一个策略吗?

 答案当然是不会的。由于大对象占用内存较大,如果都直接分配在年轻代中,那么有可能会扰乱年轻代的内存分配频繁触发Minor GC。由于大对象占用空间很大,所以需要将大量小的对象从Eden区移到老年代,这些对象很快就会消亡,又会导致频繁Full GC

 所以我们可以通过XX:PretenureSizeThreshold=3145728(3MB)这种方式设置一个阈值,当分配对象大于这个数值时则会直接分配在老年代中。这里设置的3M只是一个示例,需要大家根据实际情况进行判断和设置才能达到最理想的效果。

 另外一个需要注意的是,如果我们使用的这些大对象又大多是生命周期很短的对象,那对于GC也是十分致命的。老年代本身是我们用来存放生存周期比较长对象的一块空间,若全被短命对象占领其实对整个GC的设计和运行机制都有影响。所以我们在设计和编写代码的过程中是需要尽量去避免使用短命的大对象的。

3.3 设置进入老年代的年龄

 一般情况下,对象会被分配在Eden区中。当进行了一次Minor GC后,对象依然存活,则会移至Survivor区中,并且会为其维护的年龄加1。当经历过很多次Minor GC后对象依然坚挺,年龄达到了一定的数值时,则会被移至老年代中。

 我们可以通过XX:MaxTenuringThreshold设置进入老年代最大年龄的阈值,实际上是否进入老年代的年龄还需要根据JVM在运行时动态计算的,这个设置只是确定一个最大边界。如果我们想让对象多留在年轻代一会,可以设置一个比较大的阈值,但这对于性能的好处也不是绝对的。

3.4 设置稳定的Java堆

 我们在项目中经常会看到有很多人会将-Xmx-Xms设置成相同的。这是因为当最大堆和初始堆大小设置成一样的情况下我们可以拥有一个稳定的堆,稳定的堆由于不会动态扩容在一定程度上可以减少GC的次数。

 但是我们可以思考一下,稳定的堆由于大小固定为最大值。虽然减少了GC次数,但是每次回收的堆空间都是固定那么大,所以会增加每次GC所需的时间。

 我们还可以通过-XX:MinHeapFreeRatio-XX:MaxHeapFreeRatio参数调整堆空间最大/最小空闲比例,大家有兴趣可以去了解一下,不过如果对自己应用和JVM不熟悉的话还是不要一顿操作为好。

 这里列举出来的情况大多是平时遇见比较多的一些调优方式,还有很多其他的方式大家可以通过其他博主博客或者网上文档之类的了解尝试,调优这种东西还是很靠经验的。

4.最后一些话

 首先还是十分感谢大家能够认真看完我写的博客,这其中有一些我也是通过书籍、其他博客学习然后自己尝试总结,最后结合自己工作中的经验和自己的理解整理出来分享给大家的。从工作年限来说的话,我还是很稚嫩的,还有很多要跟大家一起学习一起进步的地方。

 这章给大家介绍了一些我对JVM调优的理解,对于调优大家主要就是熟练掌握之前JVM的相关知识,然后善于利用类似VisualVMGC Easy这类工具提高查找问题效率,久而久之拥有丰富经验的你也会成为那个调优大牛的。

 到这里整个JVM基本也告一段落了。在这里和大家一起学习了JVM的整个内存结构、对象分配与回收策略、各种垃圾算法、各种垃圾收集器,我们还了解了JIT技术及逃逸分析,到最后我们掌握了如何使用虚拟机工具武装我们自己,然后就是我们这里的JVM调优了。这里会把这个过程中我使用到的代码和一些相关文件以及整理一份脑图附在第一篇文章【JVM - 进入Java虚拟机的真实世界】里,希望能够对大家有帮助,最后再次感谢大家的支持和包容!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章