- GC调优
- 1、预备知识
- 2、GC收集器的选择
- 3、最快的GC是不发生GC
- 4、新生代调优
- 5、老年代的内存调优
- 6、案例
- 6.1、案例1
- 6.2、案例2
- 6.3、案例3
调优需要掌握GC相关的VM参数,而具体参数可以查看官方文档
另外,我们还可以通过命令的方式在本地查看虚拟机运行参数:
"D:jdk1.8.0_121binjava" -XX:+PrintFlagsFinal -version | findstr "GC"
调优跟应用、环境有关,没有放之四海而皆准的法则
2、GC收集器的选择想要选择适合自己的GC收集器,首先要明白自己的应用程序是干什么的,是做科学运算,还是互联网项目,如果是科学运算,则追求的应该是高吞吐量,对于延长一点点响应时间,对我来说无关紧要,如果我们做的是互联网项目,则我们追求的是低响应时间,如果每次垃圾回收的时间太长,导致应用程序暂停时间太长,则会给用户带来不好的体验。
确定了自己的目标,我们才能去选择适合自己的GC收集器
- 对于高吞吐量而言,我们没有太多的选择,也就是ParallelGC
- 对于低延迟,也就是响应时间优先,可以选择的就有很多了,比如
- CMS:目前比较主流,但是JDK9已经不推荐了
- G1:JDK9推荐使用,在超大堆内存下有着很明显的优势,相当于是CMS和ParallelGC的结合,既可以做到低延迟,也可以像ParallelGC一样去确定一个吞吐量目标(单位时间控制暂停时间阈值)
- ZGC:JDK12引入
对于我们GC调优,我们最终目的是控制STW的时间,想要将GC调至最优,当然便是尽可能少的GC,如果你的应用程序经常发生GC,那么你就应该考虑如下几个问题:
- 数据是不是太多?是不是加载了太多不必要的数据?
- 数据表示是否太臃肿?
- 对象图
- 查询一个对象的时候,我们把它所有相关的数据都查出来了,但我们实际上只需要这个对象的一部分数据
- 对象大小
- 比如我们Java里面,最小的一个Object对象,都要占用16字节,我们在程序中经常使用一个包装器类型,如Integer,它的一个对象头就16字节,然后还有它真实表示的int类型的整数值4个字节,然后再做一个对齐,这就占用了24字节,我们如果只使用基本类型,那么它只占用Integer的六分之一大小
- 对象图
- 是否存在内存泄露?
- 比如我们声明一个static Map对象,我们不停的往里面放对象,但不移除,早晚有一天,我们内存就会内存溢出,我们对于这种长期存活的对象,可以使用软弱引用去处理,或者对于这种缓存的数据,我们不适用Java去处理,而是采用第三方的其他缓存实现去处理,比如Redis等
在上面,我们排除了自身代码的问题后,就可以开始对我们的内存进行调优,而内存调优,肯定是需要先从新生代调优走起
新生代的特点:
- 所有对象创建的内存分配效率非常高
- 对于每一个Java线程都会再伊甸园中分配一块属于自己的一块私有的区域,这个区域名为TLAB即thread-local allocation buffer,顾名思义也就是线程自己的分配缓冲区,线程在创建对象的时候,首先会去查看TLAB中是否有空余内存,如果有,则优先会在这里创建
- 为什么要在这里呢?因为对象创建也需要考虑线程安全的问题,多个线程同时在伊甸园中创建对象,需要考虑这块内存是否被其他线程指定,而JVM也提供了响应的保护措施,但是为了效率,我们要尽可能减少这种内存区域的保护 *** 作,所以对象创建才会首先去TLAB
- 死亡对象的回收代价为零
- 目前介绍的所有GC收集器,在这里都是采用的复制算法,即我们在GC的时候,会把所有伊甸园和幸存区from的存活对象复制到幸存区to中去,复制过去以后,伊甸园和幸存区from的内存就被完全释放出来了
- 大部分对象用过即死,也就是“朝生夕灭”
- 正是因为大部分对象存活时间很短,幸存对象很少,有因为新生代GC的时候采用的复制算法,所以Minor GC的时间远远低于Full GC
如何对新生代调优?直接通过参数-Xmn将新生代的内存加大就可以了吗?当然不行,我们看看官方文档如何介绍
- 翻译一下意思就是:如果新生代的内存太小,则很容易触发大量的Minor GC,从而增加STW的次数和时间。如果内存分配太大,相对而言老年代的内存空间就变小了,因为存活时间很长的对象会转移到老年代中,而老年代的内存空间很小,当老年代内存满了,则会直接触发Full GC,Full GC触发的次数太多,就需要更长时间的STW。Oracle建议将新生代的大小保持在总体堆大小的**25%至50%**以下。
那具体设置为多大合适呢?对于应用程序的一个理想情况是,新生代能容纳所有【并发量*单位请求响应过程所需要的内存大小】的数据,比如现在一次响应我们创建的对象大概占用了1M的内存,而并发量大概是1000,这个时候新生代需要的内存比较理想的就是1000M大小,因为对于新生代中创建的对象,绝大部分都是朝生夕灭,所以这个应用程序最终可以较好的避免或者是减少GC的次数
单独考虑一下幸存区,它也需要足够大,以便能够保留【当前活跃对象+需要晋升对象】,也就是说,其中的对象可以分为两类,一种是生存周期较短,可能几次GC就会被清理掉了,另一种就是长时间存活对象,肯定会被晋升到老年代,但是它的年龄暂时还不够,还只能存在于幸存区中,如果幸存区比较小,那么JVM会动态的调整晋升阈值,容易使本来存活期不那么长的对象,提前晋升到了老年代,导致本来早就应该被回收的对象,长期存活于老年代中,占用内存
合理设置晋升阈值,可以减少长时间存活的对象在新生代中复制的次数,毕竟对于新生代的标记复制算法,主要耗费的时间就在复制对象上
- -XX:+PrintTenuringDistribution:可以在每次Minor GC的时候,将幸存区中存活的对象详细信息打印出来
Desired survivor size 48286924 bytes, new threshold 10 (max 10) - age 1: 28992024 bytes, 28992024 total - age 2: 1366864 bytes, 30358888 total #1+2 - age 3: 1425912 bytes, 31784800 total #1+2+3 ...
- -XX:MaxTenuringThreshold=threshold:指定晋升阈值
-
以CMS为例,因为它是工作于老年代的一款GC处理器,GC线程可以和用户线程并发执行,这样会带来一定的问题,比如浮动垃圾,当浮动垃圾过多,内存又不足了,就会导致并发失败的问题,CMS便会退化为串行的Serial Old,它会直接暂停用户线程,因为Serial Old的效率很低,这时候程序STW的时间将会很长,所以对于老年代的内存规划,最好是能给大一些,预留更多空间避免浮动垃圾过多导致内存不足的问题
-
一般情况下,可以先尝试不做老年代调优,因为如果程序运行一段时间,没有触发Full GC,也就是说,不会因为老年代空间不足导致的垃圾回收,这就已经能说明老年代的内存很充裕,就算是触发了Full GC,我们也应该先尝试调优新生代
-
如果新生代调优已经做过了,还是容易内存不足,再回头来适当调整老年代内存大小,调大1/4~1/3
-
-XX:CMSInitiatingOccupancyFraction=percent:设置老年代空间占用比达到多少开始使用CMS开始垃圾回收,越低,表示CMS垃圾回收的时机越早
Full GC和Minor GC频繁
- 那么可以推断,我们的堆内存空间比较紧张,具体是哪一部分内存紧张呢,如果是新生代的空间紧张,当业务高峰期来了,大量对象被创建,很快就把新生代占满了,因为幸存区的晋升阈值会动态调整,导致很多生存周期很短的对象直接被晋升到了老年代,大量的垃圾存在于老年代,导致老年代空间也紧张了,所以这时候可以尝试先加大新生代的内存大小
请求高峰期发生Full GC,单次暂停时间特比长,由于业务要求低延迟,所以选择了CMS垃圾收集器
- 查看CMS的收集日志,可以判断是哪一个阶段耗时较长,对于四个阶段:初始标记,并发标记,重新标记,并发清理,最耗时的便是重新标记,这一步会扫描所有堆内存,如果此时新生代中对象太多了,那么这一步耗费的时间也就更长,又因为新生代中绝大部分对象都会直接被GC清理了,并不会存活太长时间,所以此时会耗费额外的时间去扫描这些对象,那么我们可以尝试在标记新生代对象的时候,先做一次Minor GC,这样可以减少很多不必要的标记,我们可以使用-XX:+CMSScavengeBeforeRemark参数来指定
老年代内存充裕的情况发生Full GC,采用的仍然是CMS,开发环境为1.7
- CMS可能是因为空间不足或者内存碎片太多导致并发失败,触发Full GC,但是这里老年代内存是充裕的,这时候可以考虑考虑JDK1.7以前方法区的实现是永久代,对于JDK1.7以前,如果永久代的空间不足,也会导致一次Full GC,而JDK1.8中,方法去实现改为了元空间,它直接使用了 *** 作系统的空间,所以可以尝试调整永久代的内存大小
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)