java 十 (二) 实战
阶段性复习:JVM运行原理和GC原理你真的搞懂了吗
JVM和GC的运行原理
JVM的内存区域划分,最核心的就是这么几块了:年轻代、老年代、Metaspace(也就是以前的永久代)。
其中年轻代又分成了Eden和2个Survivor,默认比例是8:1:1,如下图

接着我们来思考一下,我们写好的系统会不停的运行,运行的时候是不是就会不停的在年轻代的Eden区域中创建各种对象?

而且一般创建对象都是在各种方法里执行的,一旦方法运行完毕,方法局部变量引用的那些对象就会成为Eden区里的垃圾对象,就是可以被回收的状态,大家务必要清楚这个过程,如下图。

接着随着Eden区不断的创建对象,就会逐步的塞满,当然这个时候可能塞满Eden区的对象里大多数都是垃圾对象。一旦Eden区塞满之后,就会触发一次Young GC。
Young GC会采用复制算法,从GC Roots(方法的局部变量、类的静态变量)开始追踪,标记出来存活的对象。
然后把存活对象都放入第一个Survivor区域中,也就是S0区域,如下图所示。

接着垃圾回收器就会直接回收掉Eden区里剩余的全部垃圾对象,在整个这个垃圾回收的过程中全程会进入Stop the Wold状态,也就是暂停系统工作线程,系统代码全部停止运行,不允许创建新的对象。只有这样,才能让垃圾回收器专心工作,找出来存活对象,回收掉垃圾对象,如下图所示。

一旦垃圾回收全部完毕之后,也就是存活对象都进入了Survivor区域,然后Eden区都清空了,那么Young GC执行完毕,此时系统恢复工作,继续在Eden区里创建对象,如下图所示。

下一次如果Eden区满了,就会再次触发Young GC,把Eden区和S0区里的存活对象转移到S1区里去,然后直接清空掉Eden区和S0区中的垃圾对象
当然这个过程中系统是禁止运行的,处于Stop the World状态,如下图所示。

负责Young GC的垃圾回收器有很多种,但是常用的就是ParNew垃圾回收器,他的核心执行原理就如上所述,只不过他运行的时候是基于多线程并发执行垃圾回收的,大家只要记得这点就可以。
对象什么时候进入老年代
实际JVM运行过程中,有很多意外的情况会发生的,会导致对象进入老年代区域中,如下所述几种情况,反复给大家总结过,务必要记得很清晰:
一个对象在年轻代里躲过15次垃圾回收,年龄太大了,寿终正寝,进入老年代
对象太大了,超过了一定的阈值,直接进入老年代,不走年轻代
一次Young GC过后存活对象太多了,导致Survivor区域放不下了,这批对象会进入老年代
可能几次Young GC过后,Surviovr区域中的对象占用了超过50%的内存,此时会判断如果年龄1+年龄2+年龄N的对象总和超过了Survivor区域的50%,此时年龄N以及之上的对象都进入老年代,这是动态年龄判定规则
上面4个条件就是最常见的对象进入老年代的情况,那种长期存活的躲过15次Young GC的对象毕竟是少数的,大对象一般在特殊情况下会有,对于那种加载大量数据长时间处理以及高并发的场景,很容易导致Young GC后存活对象过多的。
所以对于这些情况,都会导致对象进入老年代中,老年代对象可能会越来越多,如下图所示。

老年代的GC是如何触发的
一旦老年代对象过多,就可能会触发Full GC,Full GC必然会带着Old GC,也就是针对老年代的GC
而且一般会跟着一次Young GC,也会触发永久代的GC。
大家还记得Full GC触发的几个条件吗?
老年代自身可以设置一个阈值,有一个JVM参数可以控制,一旦老年代内存使用达到这个阈值,就会触发Full GC,一般建议调节大一些,比如92
在执行Young GC之前,如果判断发现老年代可用空间小于了历次Young GC后升入老年代的平均对象大小的话,那么就会在Young GC之前触发Full GC,先回收掉老年代一批对象,然后再执行Young GC。
如果Young GC过后的存活对象太多,Survivor区域放不下,就要放入老年代,要是此时老年代也放不下,就会触发Full GC,回收老年代一批对象,再把这些年轻代的存活对象放入老年代中
触发Full GC几个比较核心的条件就是这几个,总结起来,其实就是老年代一旦快要搞满了,空间不够了,必然要垃圾回收一次。
老年代的垃圾回收通常建议走CMS垃圾回收器,回收机制比较复杂,此处建议大家自行复兴和总结一下
总之,Old GC的速度是很慢的,少则几百毫秒,多则几秒。所以一旦Full GC很频繁,就会导致系统性能很差,因为频繁要停止系统工作线程,导致系统看起来一直有卡顿的现象。
而且频繁Full GC还会导致机器CPU负载过高,导致机器性能下降,处理请求能力降低。
所以优化JVM的核心就是减少Full GC的频率。
正常情况下的系统
正常情况下的系统,会有一定频率的Young GC,一般在几分钟一次Young GC,或者几十分钟一次Young GC,一次耗时在几毫秒到几十毫秒的样子,都是正常的。
正常的Full GC频率在几十分钟一次,或者几个小时一次,这个范围内都是正常的,一次耗时应该在几百毫秒的样子。
所以大家如果观察自己线上系统就是这个性能表现,基本上问题都不太大。
当然,实际线上系统很多时候回遇到一些JVM性能问题,就是Full GC过于频繁,每次还耗时很多的情况,此时就需要一些优化了。
阶段性复习:JVM性能优化到底该怎么做?
一个新系统开发完毕之后如何设置JVM参数?
之前花费了很多的精力给大家介绍,在一个新系统开发完毕之后,到底该如何预估性的合理设置JVM参数?
毕竟直接用默认的JVM参数部署上线再观察,是非常的不靠谱的。很多公司也没有所谓的JVM参数模板。
首先大家应该估算一下自己负责的系统每个核心接口每秒多少次请求,每次请求会创建多少个对象,每个对象大概多大,每秒钟会使用多少内存空间?
这样接着就可以估算出来Eden区大概多长时间会占满?
然后就可以估算出来多长时间会发生一次Young GC,而且可以估算一下发生Young GC的时候,会有多少对象存活下来,会有多少对象升入老年代里,老年代对象增长的速率大概是多少,多久之后会触发一次Full GC。
通过一连串的估算,就可以合理的分配年轻代和老年代的空间,还有Eden和Survivor的空间
原则就是:尽可能让每次Young GC后存活对象远远小于Survivor区域,避免对象频繁进入老年代触发Full GC。
最理想的状态下,就是系统几乎不发生Full GC,老年代应该就是稳定占用一定的空间,就是那些长期存活的对象在躲过15次Young GC后升入老年代自然占用的。然后平时主要就是几分钟发生一次Young GC,耗时几毫秒。
在压测之后合理调整JVM参数
任何一个新系统上线都得进行压测,此时在模拟线上压力的场景下,可以用jstat等工具去观察JVM的运行内存模型:
Eden区的对象增长速率多块?
Young GC频率多高?
一次Young GC多长耗时?
Young GC过后多少对象存活?
老年代的对象增长速率多高?
Full GC频率多高?
一次Full GC耗时?
压测时可以完全精准的通过jstat观察出来上述JVM运行指标,让我们对JVM运行时的情况了如指掌。然后就可以尽可能的优化JVM的内存分配,尽量避免对象频繁进入老年代,尽量让系统仅仅有Young GC。
线上系统的监控和优化
系统上线之后,务必进行一定的监控,高大上的做法就是通过Zabbix、Open-Falcon之类的工具来监控机器和JVM的运行,频繁Full GC就要报警。
比较差一点的做法,就是在机器上运行jstat,让其把监控信息写入一个文件,每天定时检查一下看一看。
一旦发现频繁Full GC的情况就要进行优化,优化的核心思路是类似的:通过jstat分析出来系统的JVM运行指标,找到Full GC的核心问题,然后优化一下JVM的参数,尽量让对象别进入老年代,减少Full GC的频率。
线上频繁Full GC的几种表现
其实通过之前的各种案例,大家可以总结出来,一旦系统发生频繁Full GC,大概看到的一些表象如下:
机器CPU负载过高;
频繁Full GC报警;
系统无法处理请求或者处理过慢
所以一旦发生上述几个情况,大家第一时间得想到是不是发生了频繁Full GC。
频繁Full GC的几种常见原因
之前给大家分析过多个案例,通过那些案例的总结和归纳,可以得出下面几个常见的频繁Full GC的原因:
1.系统承载高并发请求,或者处理数据量过大,导致Young GC很频繁,而且每次Young GC过后存活对象太多,内存分配不合理,Survivor区域过小,导致对象频繁进入老年代,频繁触发Full GC。
2.系统一次性加载过多数据进内存,搞出来很多大对象,导致频繁有大对象进入老年代,必然频繁触发Full GC
3.系统发生了内存泄漏,莫名其妙创建大量的对象,始终无法回收,一直占用在老年代里,必然频繁触发Full GC
4.Metaspace(永久代)因为加载类过多触发Full GC
5.误调用System.gc()触发Full GC
其实常见的频繁Full GC原因无非就上述那几种,所以大家在线上处理Full GC的时候,就从这几个角度入手去分析即可,核心利器就是jstat。
1.如果jstat分析发现Full GC原因是第一种,那么就合理分配内存,调大Survivor区域即可。
2.如果jstat分析发现是第二种或第三种原因,也就是老年代一直有大量对象无法回收掉,年轻代升入老年代的对象病不多,那么就dump出来内存快照,然后用MAT工具进行分析即可
3.通过分析,找出来什么对象占用内存过多,然后通过一些对象的引用和线程执行堆栈的分析,找到哪块代码弄出来那么多的对象的。接着优化代码即可。
4.如果jstat分析发现内存使用不多,还频繁触发Full GC,必然是第四种和第五种,此时对应的进行优化即可。
一个统一的JVM参数模板
为了简化JVM的参数设置和优化,建议各个公司和团队leader做一份JVM参数模板出来,设置一些常见参数即可
核心就是一些内存区域的分配,垃圾回收器的指定,CMS性能优化的一些参数(比如压缩、并发,等等),常见的一些参数,包括禁止System.gc(),打印出来GC日志,等等。
这些常见的参数,之前基本都讲过了,建议大家自行整理出来一份模板即可。

浙公网安备 33010602011771号