大纲
1.JVM的新生代垃圾回收器ParNew如何工作
2.JVM老年代垃圾回收器CMS是如何工作的
3.线上部署系统时如何设置垃圾回收相关参数
4.新生代垃圾回收参数如何优化
5.老年代的垃圾回收参数如何优化
6.问题汇总
1.JVM的新生代垃圾回收器ParNew如何工作
(1)JVM的核心运行原理梳理点
(2)最常用的新生代垃圾回收器------ParNew
(3)如何为线上系统指定使用ParNew垃圾回收器
(4)ParNew垃圾回收器默认情况下的线程数量
(5)单线程和多线程说明
(1)JVM的核心运行原理梳理点
一.对象在新生代分配,何时会触发YGC
二.YGC前会如何检查老年代大小,涉及哪些步骤条件
三.什么情况下YGC前会提前触发FGC
四.FGC的算法是什么
五.YGC过后可能对应哪几种情况
六.YGC后哪些情况对象会进入老年代
(2)最常用的新生代垃圾回收器---ParNew
一般在之前多年里,如果没有最新的G1垃圾回收器的话,通常线上系统都是用ParNew垃圾回收器作为新生代的垃圾回收器。当然后来即使有了G1,很多线上系统还是用ParNew。
一般运行在服务器上的Java系统,都能充分利用服务器的多核CPU优势。但4核服务器如果用单线程回收新生代垃圾,则没法充分利用CPU资源。
如上图示,在垃圾回收时:JVM会把系统程序所有的工作线程停掉,只剩一个垃圾回收线程在运行,那么此时4核CPU的资源根本没法充分利用。理论上4核CPU可以支持4个垃圾回收线程并行执行,可以提升4倍性能。
Serial和ParNew都是用于回收新生代垃圾的。这两者唯一区别就是单线程和多线程,它们的垃圾回收算法完全一样。新生代的ParNew垃圾回收器使用的是多线程垃圾回收机制,而新生代的Serial垃圾回收器使用的是单线程垃圾回收机制。
如下图示,ParNew垃圾回收器一旦开始执行Young GC,就会把系统程序的工作线程全停掉,禁止程序继续运行创建新的对象,然后就会使用多个垃圾回收线程去进行垃圾回收。
(3)如何为线上系统指定使用ParNew垃圾回收器
设置JVM参数有多种方式:
一.在IDEA中可以设置Debug JVM Arguments
二.使用"java -jar"启动项目时可在后面跟上JVM参数
三.项目部署到Tomcat时可以在Tomcat的catalina.sh脚本中设置JVM参数
四.使用Spring Boot部署项目时也可以在启动Spring Boot时指定JVM参数
启动系统时如果要指定ParNew回收,可用-XX:+UseParNewGC选项。只要加入该选项,JVM对新生代进行垃圾回收时,就是用ParNew了。
在ParNew垃圾回收器中,YGC时机、检查机制、垃圾回收过程、以及对象升入老年代的机制,都和前面介绍的一样,只不过ParNew会使用多个线程来进行垃圾回收。
(4)ParNew垃圾回收器默认情况下的线程数量
服务器一般都是多核CPU,为了在垃圾回收时充分利用多核CPU资源,指定使用ParNew后,默认会设置ParNew的垃圾回收线程数=CPU核数。
比如线上服务器用的是4核CPU、8核CPU、16核CPU,则ParNew的垃圾回收线程数分别是4个、8个、16个。这个垃圾回收线程数一般不用手动去调节,因为与CPU核数一样的垃圾回收线程数,可以充分进行并行处理。但如果一定要调节ParNew的垃圾回收线程数量,也是可以的。使用-XX:ParallelGCThreads参数可设置ParNew的垃圾回收线程数,但是建议一般不要随意改动该参数。
(5)单线程和多线程说明
是用单线程垃圾回收好,还是用多线程垃圾回收好?
是Serial垃圾回收器好还是ParNew垃圾回收器好?
一.启动系统时可区分服务器模式和客户端模式
如果启动系统时加入-server就是服务器模式。
如果启动系统时加入-cilent就是客户端模式。
二.服务器模式和客户端模式的区别
如果系统部署在4核8G的Linux服务器上,那么就应该用服务器模式。如果系统是运行在Windows上的客户端程序,那么就应该用客户端模式。
服务器模式通常运行网站系统、电商系统等大型系统,一般用多核CPU。如果要对这些大型系统进行垃圾回收,那么肯定是用ParNew更好。因为多线程并行垃圾回收,充分利用多核CPU资源,可以提升性能。反之如果部署在服务器上,但用了单线程垃圾回收,就浪费一些CPU了。
客户端模式通常运行一个客户端Java程序。比如某云笔记的Windows客户端,运行在Windows个人操作系统上,这种安装在个人操作系统上的应用程序很多是单核CPU。如果这些应用程序使用ParNew来进行垃圾回收,可能会导致一个CPU运行多个线程,增加开销,可能效率还不如单线程。因为单CPU运行多线程会导致频繁的上下文切换,有额外开销。
所以对于那些运行在Windows上的客户端程序,可采用Serial垃圾回收器,单CPU单线程垃圾回收即可,效率会更高。
不过现在一般很少有用Java写客户端程序,几乎很少见,Java现在主要是用来构建复杂的大规模后端业务系统。所以常用-server指定服务器模式,再配合ParNew进行多线程垃圾回收。
2.JVM老年代垃圾回收器CMS是如何工作的
(1)新生代垃圾回收总结
(2)CMS垃圾回收的基本原理
(3)如果Stop the World然后垃圾回收会如何
(4)如何实现JVM垃圾回收的同时让 应用也工作
(5)对CMS的垃圾回收机制进行性能分析
(1)新生代垃圾回收总结
新生代的垃圾回收是通过标记-复制算法来实现的,我们最希望的是:
新对象都在新生代的Eden区分配内存。然后每次垃圾回收后,存活对象都进入Survivor区。然后下一次垃圾回收后的存活对象都进入另外一个Survivor区。这样几乎很少对象会进入老年代,几乎不会触发老年代的垃圾回收。
但是理想很丰满,现实是在写代码时,很少会考虑垃圾回收。都是不停写代码然后上线部署,很少考虑所写代码对垃圾回收的影响。最多有经验的工程师在系统上线前,通过前面案例介绍的方法:估算一下系统的内存压力以及垃圾回收的运行模型,然后合理设置一下内存各个区域大小,尽量避免太多对象进入到老年代。
实际中,线上系统很可能因各种各样原因导致很多对象进入老年代,然后频繁触发老年代的Full GC。之前介绍的案例就演示过这种情况,比如Survivor区太小,容纳不了每次YGC后的存活对象,从而导致对象频繁进入老年代,最后频繁触发老年代Full GC。
类似的情况其实很多,所以不能过于理想化的期待永远没有老年代GC,还是要对老年代的垃圾回收器如何进行回收有一个充分的了解和认识。
(2)CMS垃圾回收的基本原理
一般老年代选择的垃圾回收器是CMS,它采用的是标记-清理算法。就是先标记出哪些对象是垃圾对象,然后就把这些垃圾对象清理掉。如下图示是一个老年代内存区域的对象分布情况:
现假设因老年代可用内存小于历次YGC后升入老年代对象的平均大小,判断出YGC有风险,于是就提前触发FGC回收老年代的垃圾对象。或者一次YGC后对象太多,都要升入老年代但空间不足,于是触发FGC。
总之就是要进行FGC,此时的标记-清理算法会如下处理:首先通过追踪GC Roots,看看各个对象是否被GC Roots给引用了。如果是的话,那就是存活对象,否则就是垃圾对象。接着将垃圾对象都标记出来,然后再一次性把垃圾对象都回收掉。这种标记-清理算法最大的问题,其实就是会造成很多内存碎片。如下图示:
(3)如果Stop the World然后垃圾回收会如何
假如要先STW,再采用标记-清理算法去回收垃圾,那会有什么问题?如果停止一切工作线程,然后慢慢去执行标记-清理算法,会导致系统卡死时间过长,很多响应无法处理。
所以CMS垃圾回收器采取的是:垃圾回收线程和系统工作线程尽量同时执行的模式来处理的。
(4)如何实现JVM垃圾回收的同时让 应用也工作
CMS在执行一次垃圾回收的过程共分为4阶段:
阶段一:初始标记
阶段二:并发标记
阶段三:重新标记
阶段四:并发清理
一.CMS进行垃圾回收时会 先 进入初始标记阶段
这个阶段会让系统的工作线程全部停止,进入Stop the World状态。如下图示:
所谓初始标记,就是标记出所有GC Roots直接引用的对象,比如下面的代码:
public class Kafka {
private static ReplicaManager replicaManager = new ReplicaManager();
}
public class ReplicaManager {
private ReplicaFetcher replicaFetcher = new ReplicaFetcher();
}
在初始标记阶段,会通过类静态变量replicaManager这个GC Roots,标记出它直接引用的ReplicaManager对象,不会管ReplicaFetcher对象,这就是初始标记大概过程。
因为ReplicaFetcher对象是被ReplicaManager类的实例变量引用的,方法的局部变量和类的静态变量是GC Roots,类的实例变量不是GC Roots。如下图示:
所以初始标记阶段虽然会造成STW暂停一切工作线程,但其实影响不大。因为它的速度很快,仅仅标记GC Roots直接引用的那些对象而已。
二.接着是并发标记阶段,该阶段系统线程可继续运行创建新对象
在并发标记运行期间,可能会创建新的存活对象,也可能会让部分存活对象失去引用变成垃圾对象。在这个过程中,垃圾回收线程会尽可能对已有的对象进行GC Roots追踪。
GC Roots追踪,就是看ReplicaFetcher之类的的对象是在被谁引用。比如这里ReplicaFetcher对象被ReplicaManager对象的实例变量引用了,接着看ReplicaManager对象被谁引用,发现被Kafka类的静态变量引用。那么此时可以认定ReplicaFetcher对象是被GC Roots间接引用的,所以就不需要回收ReplicaFetcher对象了。如下图示:
在进行并发标记的这个过程中,系统程序会不停的工作。此时系统程序可能会创建出各种新的对象,部分对象可能成为垃圾。如下图示:
并发标记阶段会对老年代所有对象进行GC Roots追踪,其实是最耗时的,因为需要追踪所有对象是否从根源上被GC Roots引用了。但是这个最耗时的阶段,并发标记线程是和系统程序并发运行的,所以并发标记阶段不会对系统运行造成太大影响。
三.接着会进入重新标记阶段
由于在并发标记阶段里:一边是JVM在标记存活对象和垃圾对象,一边是系统程序在不停运行创建新对象让老对象变成垃圾。所以并发标记阶段结束后,会有很多存活对象和垃圾对象没被标记出来。如下图示:
于是在重新标记阶段需要让系统程序停下来,再次进入STW。重新标记在并发标记阶段新创建的存活对象,以及失去引用的垃圾对象。如下图示:
重新标记阶段的速度是很快的,因为只是对并发标记阶段中系统程序运行变动过的少数对象进行标记。
四.接着恢复 运行 系统程序,进入并发清理阶段
这个阶段会让系统程序并发运行,然后CMS垃圾回收器会清理掉之前标记为垃圾的对象。这个并发清理阶段其实是很耗时的,因为需要进行对象的清理。但是它也会跟系统程序并发运行,所以其实也不影响系统程序的执行。如下图示:
(5)对CMS的垃圾回收机制进行性能分析
从CMS的垃圾回收机制可以发现,它已经尽可能的进行了性能优化了。
因为最耗时的是:一是并发标记阶段对老年代全部对象追踪GC Roots,标记可回收对象。二是并发清理阶段对各种垃圾对象先清除后整理。
由于并发标记阶段和并发清理阶段都是和系统程序并发执行的,所以基本上这两个最耗时的阶段对性能影响不大。
虽然初始标记阶段和重新标记阶段需要Stop the World,但是这两个阶段都是简单的标记而已,所以速度非常快,所以基本上这两个STW的阶段对系统运行响应也不大。
3.线上部署系统时如何设置垃圾回收相关参数
(1)CMS的基本工作原理总结
(2)并发回收垃圾导致CPU资源紧张
(3)Concurrent Mode Failure问题
(4)内存碎片问题
(5)为什么老年代的FGC要比新生代的YGC慢
(6)触发老年代GC的时机总结
(1)CMS的基本工作原理总结
为了避免长时间Stop the World,CMS采用了4个阶段来垃圾回收。其中初始标记和重新标记耗时很短,虽然会导致STW,但是影响不大。然后并发标记和并发清理耗时最长,但可以和系统的工作线程并发运行。所以并发标记和并发清理两个阶段对系统也没太大影响,这就是CMS的基本工作原理。
接下来介绍CMS垃圾回收期间的一些细节和常见的JVM参数设置。
(2)并发回收垃圾导致CPU资源紧张
如下图示:
CMS垃圾回收器有一个最大的问题:虽然能在垃圾回收的同时让应用程序也同时运行,但是在并发标记和并发清理两个最耗时的阶段,垃圾回收线程和应用程序工作线程同时工作,会导致有限的CPU资源被垃圾回收线程占用了一部分。
并发标记时要对GC Roots进行深度追踪,看所有对象里有多少是存活的。但因老年代里存活对象比较多,该过程又追踪大量对象,所以耗时较高。并发清理时要把垃圾对象从各种随机的内存位置清理掉,也是很耗时的。所以在并发标记和并发清理这两阶段,CMS的垃圾回收线程会特别耗费CPU。
CMS默认启动的垃圾回收线程的数量是:(CPU核数 + 3) / 4,下面用最普通的2核4G机器来计算一下。由于是2核的CPU,所以CPU资源本来就有限,结果CMS还需要"(2 + 3) / 4 = 1"个垃圾回收线程,占用宝贵的1个CPU。所以CMS这个并发垃圾回收的机制,最大的问题就是会消耗CPU资源。
(3)Concurrent Mode Failure问题
一.什么是浮动垃圾
在并发清理阶段,CMS只不过是回收之前标记好的垃圾对象。但这个阶段系统一直在运行,随着系统运行可能有些对象进入老年代。同时这些对象很快又失去引用变成垃圾对象,这种对象就是浮动垃圾。
上图的垃圾对象(新的)就是在并发清理期间,先被系统分配在新生代,然后触发一次YGC,一些对象进入了老年代,短时间内又没被引用了。这种对象,就是老年代的浮动垃圾。浮动垃圾在本次的并发清理阶段中,由于没有被标记,所以不能被回收,需要等到下一次GC执行到并发清理阶段时才能进行回收。
二.CMS垃圾回收的一个触发时机与预留空间
为了保证在CMS垃圾回收期间,能让一些对象可以进入老年代,JVM会给老年代预留一些空间。
CMS垃圾回收的一个触发时机就是:当老年代内存占用达到一定比例,就自动执行FGC。这个比例是由-XX:CMSInitiatingOccupancyFaction参数控制的,这个参数可以用来设置老年代占用达到多少比例时就触发CMS垃圾回收。
-XX:CMSInitiatingOccupancyFaction参数在JDK 1.6里默认的值是92%,也就是如果老年代占用了92%的空间,就会自动进行CMS垃圾回收。此时会预留8%的空间,这样在CMS并发回收期间,可让系统程序把一些新对象放入老年代中。
三.如果CMS垃圾回收期间,要放入老年代的对象已大于可用内存空间
这时就会发生Concurrent Mode Failure,即并发垃圾回收失败了。CMS一边回收,系统程序一边把对象放入老年代,内存不够了。此时就会自动用Serial Old替代CMS,直接强行对系统程序STW。重新进行长时间GC Roots追踪,标记全部垃圾对象,不允许新对象产生。最后再一次性把垃圾对象都回收掉,完成后再恢复系统程序。
所以在实践中:老年代占用多少比例时触发CMS垃圾回收,要设置合理。让CMS在并发清理期间,可以预留出足够的老年代空间来存放新对象,从而避免Concurrent Mode Failure问题。
(4)内存碎片问题
老年代的CMS垃圾回收器会采用"标记-清理"算法:每次都是标记出垃圾对象,然后一次性回收,这样会产生大量内存碎片。
内存碎片太多会导致对象进入老年代时找不到连续内存空间,触发FGC。所以CMS不能只用标记-清理算法,因太多内存碎片会导致频繁FGC。
"-XX:+UseCMSCompactAtFullCollection"这个CMS的参数,默认是打开的。意思是在FGC后要再次进行STW,停止工作线程,然后进行碎片整理。碎片整理就是把存活对象移动到一起,空出大片连续内存空间,避免内存碎片。
"-XX:CMSFullGCsBeforeCompaction"这个CMS的参数,意思是执行多少次FGC后再执行一次内存碎片整理的工作。该参数值默认是0,意思是每次Full GC后都会进行一次内存整理。
(5)为什么老年代的FGC要比新生代的YGC慢
为什么老年代的FGC要比新生代的YGC慢很多,一般在10倍+?其实原因很简单,下面分析它们的执行过程。
一.新生代Young GC执行速度很快
Young GC时首先从GC Roots出发就可以追踪哪些对象是存活的了。由于新生代存活对象很少,这个速度会很快,不需要追踪多少对象。然后直接把存活对象放入Survivor中,接着再一次性回收Eden和之前使用的Survivor。
二.CMS的Full GC执行速度很慢
首先在并发标记阶段,需要去追踪所有存活对象。老年代存活对象很多,这个过程就会很慢。
其次在并发清理阶段,不是一次性回收一大片内存,而是要找到分散的垃圾对象,速度也很慢。
最后在完成Full GC后,还得执行一次内存碎片整理,把大量的存活对象给移动到一起,空出连续内存空间,这个过程还得Stop the World,就更慢了。
此外万一并发清理期间,剩余内存空间不足以存放要进入老年代的对象,还会引发Concurrent Mode Failure问题,还得用Serial Old垃圾回收器,先进行Stop the World,再重新来一遍标记清理的过程,这就更耗时了。
所以,老年代的垃圾回收比新生代的垃圾回收慢。
(6)触发老年代GC的时机总结
时机一:
老年代可用内存 < 新生代全部对象大小 + 没开启空间担保,触发FGC。所以一般都会打开空间担保参数-XX:-HandlePromotionFailure。
时机二:
老年代可用内存 < 历次YGC后进入老年代的对象平均大小,触发FGC。
时机三:
新生代YGC存活对象 > S区(需进入老年代) + 老年代内存不足,触发FGC。
时机四:
参数-XX:CMSInitiatingOccupancyFaction可以设置CMS垃圾回收时的预留空间比例。进行YGC前的检查时,如果发现老年代可用内存大于历次新生代GC后进入老年代的对象平均大小,但老年代已使用的内存超过了这个参数指定的比例,就会触发FGC。
4.新生代垃圾回收参数如何优化
(1)案例背景
(2)特殊的电商大促场景
(3)抗住大促的瞬时压力需要几台机器
(4)大促高峰期订单系统的内存使用模型估算
(5)内存到底该如何分配
(6)新生代垃圾回收优化之Survivor空间够不够
(7)新生代对象躲过多少次垃圾回收后进入老年代
(8)多大的对象直接进入老年代
(9)指定垃圾回收器
(10)如何估算系统运行模型
(1)案例背景
下面通过一个案例分析如何在特定场景下:
一.预估系统的内存使用模型
二.合理优化新生代、老年代、Eden和S区的内存大小
三.优化参数避免新生代对象进入老年代,让对象留在新生代里被回收
这里的背景是电商系统,电商系统一般会拆分为很多子系统独立部署。比如商品系统、订单系统、促销系统、库存系统、仓储系统、会员系统等等,这里以比较核心的订单系统作为例子来进行说明,案例背景是每日上亿请求量的电商系统。
下面推算一下,每日上亿请求量的电商系统,每日会有多少活跃用户。按每个用户平均访问20次,那么上亿请求量,大概需要500万日活用户。
继续推算一下,这500万的日活用户中多少用户会下订单?假设按10%的付费转化率来计算,每天就会有50万用户支付订单。这50万订单假设集中在4小时高峰期内,那么平均每秒大概几十个订单。
在每秒几十个订单的压力下,其实根本就不需要对JVM过多关注。因为基本上就是每秒占用一些新生代内存,隔很久新生代才会满。然后一次YGC后垃圾对象清理掉,内存就空出来了,几乎无压力。但是如果考虑到特殊的电商大促场景,那么情况会怎样呢?
(2)特殊的电商大促场景
很多中小型的电商平台,平时系统压力不大,并发也不高。但如果遇到一些大促场景,比如双11什么的,情况就不同了。
假设在类似双11的节日里,零点开始大促活动,很大用户等待下单抢购。那么可能在大促开始的短短10分钟内就会有50万订单,那么此时每秒就会有接近50万 / 600 = 1000的下单请求,所以下面就针对这种大促场景来对订单系统的内存使用模型进行分析。
(3)抗住大促的瞬时压力需要几台机器
那么要抗住大促期间的瞬时下单压力,订单系统需要部署几台机器呢?基本上可以按3台来算,就是每台机器每秒需要抗300个下单请求。这是非常合理的,假设订单系统部署的就是最普通的标配4核8G机器。从机器的CPU资源和内存资源看,抗住每秒300个下单请求是没问题的。
但是问题就在于需要对JVM有限的内存资源进行合理的分配和优化,包括对垃圾回收进行合理的优化,让JVM的GC次数尽可能最少,而且尽量避免FGC,这样能尽可能减少JVM的GC对高峰期的系统影响。
(4)大促高峰期订单系统的内存使用模型估算
接下来预估订单系统的内存使用模型,基本上可以按照每秒处理300个下单请求来估算。因为处理下单请求是比较耗时的,涉及很多接口的调用,基本上每秒处理100~300个下单请求是差不多的。
每个订单按1K的大小来估算,那么300个订单就会有300K的内存开销。然后算上订单对象连带的商品、库存、促销、优惠券等其他业务对象,一般按照经验需要对单个对象开销放大10到20倍。除了下单之外,这个订单系统还会有很多订单相关的其他操作。比如订单查询之类的,所以连带算起来,可以往大了估算,再扩大10倍。那么每秒钟大概会有300K * 20 * 10 = 60M的内存开销。
但是经过一秒后,可以认为这60M的对象就是垃圾了。因为300个订单处理完了,所有相关对象都失去引用,进入可回收状态。如下图示:
(5)内存到底该如何分配
假设现在有4核8G的机器:那么给JVM的内存一般是4G,剩下几个G会留给操作系统来使用。其中堆内存可以给3G,新生代可以给1.5G,老年代给1.5G。然后每个线程的Java虚拟机栈会设置1M,JVM里如果有几百个线程大概也会有几百M,然后再给永久代(方法区)256M内存,基本上这4G内存就差不多了。
同时还要记得设置一些必要的参数:比如说打开空间担保参数-XX:HandlePromotionFailure;
一.整个JVM参数会如下所示
-Xms3072M -Xmx3072M -Xmn1536M -Xss1M
-XX:PermSize=256M -XX:MaxPermSize=256M
-XX:HandlePromotionFailure
但是-XX:HandlePromotionFailure参数在JDK 1.6以后就被废弃了。在JDK 1.6以后,只要判断:老年代可用空间 > 新生代对象总和,或者老年代可用空间 > 历次YGC升入老年代对象的平均大小,两个条件满足一个,就可以直接进行YGC而不需要提前触发FGC。
所以如果用的是JDK1.7或者JDK1.8,那么JVM参数就保持如下即可:
-Xms3072M -Xmx3072M -Xmn1536M -Xss1M
-XX:PermSize=256M -XX:MaxPermSize=256M
此时JVM内存如下图示:
二.订单系统的内存使用模型
接着就很明确了,订单系统在大促期间不停的运行,每秒处理300个订单,每秒会占据新生代60M的内存空间。但是1秒过后这60M对象都会变成垃圾,那么新生代1.5G的内存空间大概每25秒就会被占满,如下图示:
三.订单系统什么时候会进行YGC
订单系统在大促期间运行,大概25秒过后就要进行YGC了。此时因为有-XX:HandlePromotionFailure选项,所以需要进行FGC检查:比较老年代可用空间大小和历次YGC后进入老年代对象的平均大小,刚开始这个检查肯定是可以通过的。
所以YGC直接运行,一下子可以回收掉99%的新生代对象。因为除了最近一秒的订单请求还在处理,大部分订单早就处理完了。所以此时存活的对象可能有100M左右。
四.订单系统YGC后的存活对象进入Survivor区
如果-XX:SurvivorRatio参数是默认值8,那么此时新生代Eden区占1.2G内存,每个Survivor区占150M内存。如下图示:
因此大概只需要20秒,就会把Eden区占满,就要进行YGC了。然后YGC后存活对象大概在100M左右,会放入S1区域内。如下图示:
然后再次运行20秒,把Eden区重新被占满。再次垃圾回收Eden和S1中的对象,存活对象还是100M左右,进入S2区。如下图示:
此时JVM参数如下:
-Xms3072M -Xmx3072M -Xmn1536M -Xss1M
-XX:PermSize=256M -XX:MaxPermSize=256M
-XX:SurvivorRatio=8
(6)新生代垃圾回收优化之Survivor空间够不够
进行JVM优化时首先要考虑的问题是:新生代的Survivor区到底够不够。按上述逻辑,每次新生代垃圾回收的存活对象在100~150M左右。
一.如果YGC后的存活对象频繁突破150M
那么就会出现YGC后的对象无法放入S区,然后频繁让对象进入老年代。
二.如果YGC后的存活对象少于150M + 存活对象有大于100M进入S区
由于这一批都是同龄对象,且超过S区空间50%。根据动态年龄判断规则,此时也可能会导致对象直接进入老年代。所以根据这个模型,Survivor区的大小是明显不足的,这里建议调整新生代和老年代的大小。
这种业务系统的大部分对象都是短生存周期的,不应频繁进入老年代,没必要给老年代维持过大的内存空间,得先让对象尽量留在新生代里。所以可以考虑把新生代调整为2G,老年代调整为1G。那么此时Eden为1.6G,每个Survivor为200M。如下图示:
这时S区变大,大大减少了新生代GC后存活对象在S区放不下的情况,也大大减少了同龄对象超过S区50%的问题。这样就大大降低了新生代对象进入老年代的概率,此时JVM的参数如下:
-Xms3072M -Xmx3072M -Xmn2048M -Xss1M
-XX:PermSize=256M -XX:MaxPermSize=256M
-XX:SurvivorRatio=8
其实对任何系统:首先需要预估内存使用模型以及分配合理的内存,尽量让每次Young GC后的对象都停留在S区中,不要进入老年代,这是进行优化的一个地方。
(7)新生代对象躲过多少次垃圾回收后进入老年代
除了YGC后对象无法放入S区会导致一批对象进入老年代外,如果有些对象连续躲过15次垃圾回收也会自动升入老年代。
如果按照上述内存运行模型,基本上20多秒触发一次YGC。根据-XX:MaxTenuringThreshold默认值15,如果一个对象躲过15次GC,其实也就代表这个对象在新生代停留超过了15 * 20 = 几分钟,此时该对象进入老年代也是合情合理的。
有些博客会说:应该提高这个-XX:MaxTenuringThreshold参数。比如增加到20次或者30次,其实这种说法是不对的。因为对这个参数的考虑必须结合系统的运行模型,如果躲过15次GC都经过几分钟了,也就是一个对象几分钟都不能回收,说明这个对象肯定是要长期存活的核心组件(使用了类似@Service注解),那么这个对象就应该进入老年代。何况这种对象一般很少,一个系统累计起来最多也就几十M而已。所以提高-XX:MaxTenuringThreshold参数的值,没有什么用。在上述系统的运行模型下,最多只能让这些对象在新生代里多留几分钟。因此要结合运行原理,根据运行模型的推演,不同业务系统是不一样的。其实甚至可以降低这个参数的值,比如降低到5次。也就是一个对象如果躲过5次Young GC,在新生代里停留超过1分钟。那么就尽快就让它进入老年代,别在新生代里占着内存。
总之,-XX:MaxTenuringThreshold参数务必结合系统具体运行模型考虑。此时,JVM参数如下:
-Xms3072M -Xmx3072M -Xmn2048M -Xss1M
-XX:PermSize=256M -XX:MaxPermSize=256M
-XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=5
(8)多大的对象直接进入老年代
大对象可以直接进入老年代 ,因为大对象,说明是要长期存活和使用的。比如在JVM里可能会缓存一些数据,这个可结合系统实际来决定。但一般来说,设置大对象为1M即可,因为一般很少有超过1M的大对象。如果有,可能是提前分配了一个大数组、大List等用来进行缓存的数据。此时JVM参数如下:
-Xms3072M -Xmx3072M -Xmn2048M -Xss1M
-XX:PermSize=256M -XX:MaxPermSize=256M
-XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=5
-XX:PretenureSizeThreshold=1M
(9)指定垃圾回收器
指定垃圾回收器:新生代使用ParNew,老年代使用CMS。此时JVM参数如下:
-Xms3072M -Xmx3072M -Xmn2048M -Xss1M
-XX:PermSize=256M -XX:MaxPermSize=256M
-XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=5
-XX:PretenureSizeThreshold=1M
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC
ParNew的核心参数就是新生代内存大小、Eden和Survivor的比例。只要设置合理,给新生代里的S区充足空间,避免YGC后对象放不下S区进入老年代或者动态年龄判定要进入老年代,那么YGC一般就没什么问题。
然后根据系统运行模型,合理设置-XX:MaxTenuringThreshold。让那些长期存活的对象,尽快进入老年代,别在新生代里一直占用空间。这样便完成了JVM新生代参数的初步优化。
(10)如何估算系统运行模型
根据如下问题估算系统运行模型:
一.每秒占用多少内存
二.多长时间触发一次YGC
三.一般YGC后有多少存活对象
四.S区能否放得下
五.是否会频繁出现因S区放不下导致对象进入老年代
六.是否会因动态年龄判断规则进入老年代
5.老年代的垃圾回收参数如何优化
(1)总结新生代的垃圾回收参数如何优化
(2)在案例背景下什么时候对象会进入老年代
(3)大促期间多久会触发一次FGC
(4)老年代GC可能会发生Concurrent Mode Failure
(5)如何设置CMS在GC后要进行内存碎片整理的频率
(1)总结新生代的垃圾回收参数如何优化
前面介绍了一个JVM新生代优化分析的案例,根据一个百万日活及上亿请求的中型电商在大促期间的高峰下单场景,推测出大促高峰期,每台机器每秒300个下单请求,每秒使用60M内存。
然后根据该背景推测应如何给4核8G机器的JVM,合理分配各区域内存。让每隔20秒一次YGC产生的100M存活对象进入200M的Survivor区,不会因Survivor内存不足或动态年龄判定规则让存活对象进入老年代。
同时还根据YGC的频率,合理降低了大龄对象进入老年代的年龄,尽快让一些长期存活的对象赶紧进入老年代,不要停留在新生代里。如下图示:
此时的JVM参数如下所示:
-Xms3072M -Xmx3072M -Xmn2048M -Xss1M
-XX:PermSize=256M -XX:MaxPermSize=256M
-XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=5
-XX:PretenureSizeThreshold=1M
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC
(2)在案例背景下对象什么时候会进入老年代
在目前背景下,什么情况会让对象进入老年代?
第一种情况:年龄超过了-XX:MaxTenuringThreshold的新生代对象
该参数会让在一两分钟内连续躲过5次Young GC的对象迅速进入老年代。这种对象一般是被@Service、@Controller等标注的系统业务逻辑组件。这种对象一般使用单例模式,全局一个实例,会长期被GC Roots引用。这种对象一般不会太多,大概一个系统最多就占用几十M。所以类似这样的、需要长期存活的组件对象就会进入老年代中。如下图示:
第二种情况:分配一个大小超过了-XX:PretenureSizeThreshold的新生代大对象
比如创建一个大数组或者是大List对象,就会直接进入老年代。但是这种大对象在这个案例里几乎是没有的,所以可以忽略不计。
第三种情况:存活的对象使Survivor内存不足或动态年龄判定规则生效
YGC后的存活对象超过200M放不进Survivor区,或者YGC后的存活对象超过Surviovr区的50%且年龄都一样,此时这些存活对象就会进入到老年代中。但是前面对新生代的JVM参数进行优化,就是为了避免这种情况。所以这种概率应该是很低的,但也不能完全避免这种情况。比如某次GC后刚好有超过200M存活对象,则这些对象就会进入老年代。
(3)大促期间多久会触发一次FGC
下面假设该订单系统在大促期间,每隔5分钟在YGC后就有一小批对象进入老年代,这一小批对象的大小大概是200M。如下图示:
那么FGC的触发条件有如下几种:
一.没有打开空间担保参数-XX:HandlePromotionFailure
老年代可用内存最多也就1G,新生代对象总大小最多可以有1.8G。这样导致每次YGC前检查都发现:老年代可用内存 < 新生代总对象大小。从而导致每次YGC前都触发FGC,当然JDK1.6+的版本废弃了这个参数。
二.老年代可用内存空间 < 历次YGC后升入老年代的对象的平均大小
其实按照目前设定的背景,要经过很多次YGC后才可能有一两次碰巧会有200M对象升入老年代。所以这个"历次YGC后升入老年代的平均对象大小",基本是很小的。
三.某次YGC后要升入老年代的对象有几百M,但老年代可用空间不足
四.满足设置的老年代空间使用比例-XX:CMSInitiatingOccupancyFaction
比如设定值为92%,那么此时可能前面几个条件都没满足。但刚好发现该条件满足了,即老年代空间使用超过92%,就会触发FGC。
实际上在系统运行期间,可能会慢慢地有对象进入老年代。但因为优化过新生代的内存分配,所以对象进入老年代的速度是很慢的。很可能是在系统运行半小时~1小时后,才会有接近1G对象进入老年代。此时可能因为满足上述二三四条件中的一个,才会触发了FGC。但是这三个条件一般都需要老年代近乎占满时,才有可能触发。所以可以预估在大促期间,订单系统运行1小时后,大促下单高峰期几乎都快过了,此时才可能会触发一次FGC。
注意:订单系统运行1小时后才会触发一次FGC的推论很重要。因为按照大促开始10分钟就有50万订单来计算,1小时可能有几万订单。这是一年难得罕见的节日大促才会有的,日常不会有这样的订单压力。等这个高峰期一过,订单系统访问压力就很小了,GC问题几乎没影响。
所以经过新生代的优化,可以推算出:基本上在大促高峰期内,可能1小时才出现1次FGC。然后高峰期一过,可能就要几个小时才有一次FGC。
(4)老年代GC可能会发生Concurrent Mode Failure
假设订单系统运行1小时后,老年代有900M对象,剩余可用空间只剩100M,此时就会触发一次FGC。如下图示:
此时在执行FGC时会有一个很大的问题:就是CMS在垃圾回收时的并发清理期间,系统程序是可以并发运行的。而此时老年代空闲空间仅剩100M,但系统程序又在不停地创建对象。万一这时系统运行触发了某个条件,比如又有200M对象要进入老年代。如下图示:
那么此时就会触发Concurrent Mode Failure问题。因为此时老年代没有足够内存来放这200M对象,所以会导致系统STW。然后切换CMS为Serial Old + 禁止程序运行 + 使用单线程回收老年代垃圾。当回收掉900M对象后,再让系统继续运行。
以上这种情况虽然可能会发生,但是概率是挺小的。因为需要在触发CMS的同时,系统运行期间还让200M对象进入老年代。这个概率本身就很小,但理论上是有可能的。对于这种小概率的事件,其实没有必要去调整参数,不需要针对小概率事件特意优化参数。
所以最终的JVM参数如下:
-Xms3072M -Xmx3072M -Xmn2048M -Xss1M
-XX:PermSize=256M -XX:MaxPermSize=256M
-XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=5
-XX:PretenureSizeThreshold=1M
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFaction=92
(5)如何设置CMS在GC后要进行内存碎片整理的频率
在完成CMS后,一般需要执行内存碎片的整理,可以设置多少次FGC后执行一次内存碎片整理。
但其实没必要去修改这个参数。因为通过前面分析,在大促高峰期,FGC可能也就1小时执行一次。然后大促高峰期过后,就没那么多的订单了,可能几小时才一次FGC。所以保持默认的设置,每次FGC后都执行一次内存碎片整理即可。
所以最终的JVM参数如下:
-Xms3072M -Xmx3072M -Xmn2048M -Xss1M
-XX:PermSize=256M -XX:MaxPermSize=256M
-XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=5
-XX:PretenureSizeThreshold=1M
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFaction=92
-XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0
综上所述,可以看出:FGC优化的前提是YGC的优化,YGC的优化的前提是合理分配内存空间,合理分配内存空间的前提是对系统运行期间的内存使用模型进行预估。
其实对很多普通的Java系统而言,只要对系统运行期间的内存使用模型做好预估,然后合理分配内存空间,尽量让YGC后的存活对象留在S区不去老年代,基本上其余的GC参数不需要做太多优化,系统性能基本都不会太差。
6.问题汇总
问题一:
一个机器能开多少线程?取决于什么?
答:如果是4核CPU。JVM本身有一些后台线程,还有使用的一些框架也会有一些后台线程。所以应用系统一般开启线程数量在几十个,比如50左右基本就差不多了。这样JVM进程所有线程加起来有100+,遇上高峰期100+的多线程同时工作,CPU基本就满负荷了。
问题二:
是不是Stop the World只在FGC时发生,在YGC时不会发生?
答:不管是老年代回收还是新生代回收,都要Stop the World。因为必须让程序别创建新对象,才能回收垃圾对象。Old GC和Major GC其实是一个概念,都是指的老年代的GC。只不过一般会带着一次Minor GC,也就是Young GC。
问题三:
采用ParNew + CMS垃圾回收器如何只做YGC?
答:实现只做YGC,其实和垃圾收集器没有什么关系。不同垃圾回收器,差别只在于性能和吞吐量,并不影响垃圾回收时机。根据对象生存周期特点,合理分配各区大小,尽量让对象在新生代回收。也就是:避免新生代对象进入老年代 + 避免FGC。
如何避免新生代对象进入老年代:
一.根据实际情况查看每次YGC后存活对象的大小,设置合适的S区大小,保证存活对象进入S区而不是老年代。
二.根据对象存活的时间以及YGC的间隔时间,确定年龄。比如:3分钟一次YGC,而对象可以存活1个小时。那就把对象年龄设置到20,避免对象15岁进入老年代。
三.大对象如果偶尔创建一个,那么可以调大大对象的阈值,使大对象分配至新生代。即可以设置-XX:PretenureSizeThreshold,使其分配至新生代。如果大对象创建销毁频繁,就让其直接进入老年代。此时可以利用对象池避免大对象频繁创建销毁。
如何避免FGC:
一.保证老年代的可用空间大于新生代的所有对象,避免YGC前进行FGC。
二.如果一可以保证,那么就不需要考虑参数-XX:HandlePromotionFailure、以及进入老年代的对象平均大小的比较。
三.保证YGC后存活对象的大小不大于Survivor空间。
问题四:
为什么YGC比FGC快?
答:除了FGC慢的原因,还有YGC快的原因。
YGC快的原因如下:
新生代垃圾回收存活对象很少,且采用复制算法,所以迁移内存很快。然后一次性清理垃圾对象,这个速度也很快,所以比标记整理效率高。
FGC慢的原因如下:
老年代要先移动对象压在一起,存活对象又那么多,涉及到漫长的对象追踪和移动过程,所以速度慢。
问题五:
都已经FGC了程序还并行运行,创建出的对象放那?会一直触发FGC吗?如果对象太多放不下,会等FGC完成吗?这时候也是Stop the World吗?
答:会继续放新生代,可能会同步触发YGC。所以有可能有新的对象进入老年代,还可能有些老年代对象失去了引用。所以CMS并发标记环节,标记出来的垃圾对象,可能是不准确的。如果对象太多放不下,会等着FGC完成,这时候也要Stop the World。
问题六:
为什么老年代垃圾回收比新生代慢很多?
答:新生代一般存活对象少,采用标记复制算法。首先从GC Root出发标记存活对象,然后直接把少量存活对象复制到另一块内存,之后再清除原来那块内存。
老年代一般存活对象多,采用标记整理算法。首先也是需要从GC Roots出发遍历标记存活对象和垃圾对象。从GC Roots出发查找,直接或间接引用到的对象,就是存活的、要标记。剩下的就是垃圾对象,在并发清除阶段需要被清除。然后再遍历清除垃圾对象,最后还要移动存活对象,避免太多内存碎片。
问题七:
Eden区内存大小超过老年代时:如果没开启允许担保失败参数,是否YGC前就FGC了?
答:是的,所以JDK1.6+默认开启担保机制。
问题八:
是否会存在上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况?
答:YGC会直接STW系统不能运行,必须等垃圾回收完才能再次YGC。老年代垃圾回收是有可能没执行完又被触发的,因为使用的是并发清理。一边垃圾回收一边系统运行,也许FGC没回收完就再次触发FGC。如果此时老年代内存又不够,就会进入STW,就会用Serial Old来回收。
问题九:
YGC其实也会STW,是不是在跟踪和复制阶段?
答:是的,新生代也要Stop the World,但是它速度很快。从GC Roots出发标记出少量存活对象,然后转移到空的Survivor区里,最后直接清空所有垃圾对象。
问题十:
是否有一个通用的JVM优化参数?
答:并没有,对JVM参数优化,必须基于每个系统的运行模型。
问题十一:
在Eden和Survivor区GC时也是从GC Root开始标记和跟踪对象,在新生代的对象数量更多为什么在新生代就不耗时?在老年代GC时第二步并发清理的GC Root跟踪就很耗时?
答:从GC Roots开始标记跟踪,只跟踪直接引用,新生代存活对象极少。所以新生代很快就追踪完毕了,而老年代存活对象太多,追踪很耗时。
问题十二:
为什么说CMS使用了标记清理和标记整理两个算法?
答:标记-清理是先标记再清理,标记-整理是先标记然后整理,最后才清理。CMS是先标记再清理,然后再整理,所以才说CMS使用了标记-清理和标记-整理两个算法。
问题十三:
新创建的对象,到底是往Eden区放,还是会往Suivivor区放?
答:新的对象只会放入Eden,而S区只是用于存放每次YGC后的对象,新创建的对象不会往S区存放的。
问题十四:
通过动态年龄进入到老年代的对象,这批对象的年龄是否最多是1岁?动态年龄判断规则具体是怎样的?
答:动态年龄判断规则:Survivor区中这个年龄以及低于这个年龄的对象占据超过Survivor区50%,那么等于或高于这个年龄的对象就直接进入老年代。
比如S区内,年龄1 + 年龄2 + 年龄3 + 年龄n的对象总和大于S区的50%,此时年龄n以上的对象会进入老年代,不一定要达到15岁。
所以动态年龄判断规则有个推论:如果S区中的同龄对象大小超过S区内存的一半,就要直接升入老年代。
问题十五:
哪些情况下对象会进入老年代?
一.大对象直接分配到老年代
二.YGC后对象的年龄到了15
三.YGC后存活对象大小大于Survivor区大小
四.动态年龄规则触发
五.YGC前检查发现没有配置空间担保参数
六.YGC前有配置空间担保参数 + 老年代可用内存小于历次晋升平均内存
七.老年代中已经被使用的内存空间达到了-XX:CMSInitiatingOccupancyFaction设置的比例
问题十六:
案例总结如下:
一.首先计算系统高峰的QPS
每秒内存开销 = QPS * 单个对象大小 * 扩大20倍 * 其他操作10倍。
二.根据系统可用内存分配堆大小
分配新生代大小根据每秒产生的内存开销来计算。比如每秒60M内存开销,则2G给新生代,默认Eden : S1 : S2为8 : 1 : 1。
三.Eden区有1.6G,25s左右会被占满而进行YGC
此时存活的对象大概有百分之10大概为160M,这些存活的对象进入s1。有可能新生代回收后的存活对象200M+,造成这些对象直接进入老年代。此时可继续往上调新生代大小,也可以调节Eden区和s1、s2的比例。
四.空间担保参数需要打开
避免检查时发现老年代可用空间小于新生代对象大小直接FGC。
五.系统中可能会在内存缓存大对象、大集合
这种对象一般都是要频繁使用或者要一直缓存的,这时要设置直接晋升到老年代对象的大小。
六.设置在S1和S2区的对象晋升到老年代的年龄,一般采用默认的即可
可以根据实际项目,调小晋升年龄来让长期存活的对象尽快进入到老年代。
七.垃圾的回收器设置为Parnew + Cms,可以充分发挥多核处理器的优势
整体来说,优化的思路是:尽量避免频繁FGC,降低系统STW的次数和时间。
问题十七:
(1)YGC前,新生代中对象的总大小会与老年代中可用内存进行比较。其中老年代的可用内存是指剩余内存空间还是指连续可用内存空间?
(2)老年代会默认预留8%的空间给并发回收期间进入老年代的对象使用,CMS在并发标记和清理期间需进入老年代的对象大于8%的空间会怎样?
答:(1)连续可用空间。(2)这种情况可能会触发Concurrent Mode Failure,此时CMS会被Serial Old替代进行垃圾回收,直接Stop the World。
问题十八:
一.公司系统介绍
目前系统16G内存,JVM 6G的内存,新生代5.5G,永久代设置128M,老年代就是512 - 128 = 384M。
系统每秒在Eden区占用12M,所以YGC大概每450秒(8分钟)运行一次,一个S区560M内存,每次YGC回收后大概占S区空间的40~60%。所以S区空间是够的,此外同年龄对象占比超过50%的概率很低。
之前晋升年龄为31,后来改成4,由于一次YGC要8分钟,4次约半小时。所以半小时后,需要长期存活的对象肯定会进入老年代。
目前系统一直线上运行,离上次更新已半个月,发生FGC3次。所以这就是堆内存进行了合理优化,实现了YGC后的存活对象不会那么快进入老年代。可见不是堆越大越好,而是要根据系统整体运行情况预估。如果预估不准确,就用工具检测,然后合理优化。
二.JVM优化总结
进行JVM优化的第一步,就是分析系统运行的内存使用模型。然后合理预估合理分配内存。保证对象都在新生代里,进入老年代的速度要很慢很慢。其中对堆内存的调整,应该是观察Survivor区:看看YGC后的存活对象在Survivor区的占比是否过多。如果超过70%时则可能需要加大堆内存,或者业务高峰期很快就占满Eden区,也要加大堆内存。
问题十九:
应该设置多少次FGC后才进行碎片整理?
答:如果FGC相对频繁一些,可以设置多次FGC再进行碎片整理。如果FGC不是很频繁,可以设置每次FGC都进行碎片整理。