JVM第四周 ParNew、CMS垃圾回收器

1,658 阅读24分钟

年轻代垃圾回收器ParNew工作原理

通常线上系统使用ParNew垃圾回收器作为新生代的垃圾回收器。(复制算法)

多线程垃圾回收机制

通常运行在服务器上的Java系统,都会充分利用服务器多核CPU的优势。(榨干CPU)

假设服务器是4核CPU,当发生垃圾回收时,仅仅使用单线程执行垃圾回收,就会导致没有充分利用服务器CPU资源。

为什么这么说呢?

执行垃圾回收时,系统的工作线程停止执行,只有垃圾回收线程在执行,如果此时仅有一个垃圾回收线程在执行,那么4核CPU的资源根本无法充分利用,理论上4核CPU可以支持4个垃圾回收线程并行执行,可以提升4倍的性能。

因此ParNew垃圾回收器就是采用的多线程垃圾回收机制,另外一种Serial垃圾回收器主打的是单线程垃圾回收,他们俩都是回收新生代的,唯一的区别就是单线程和多线程的区别,但是垃圾回收算法是完全一样的。

线上系统指定ParNew作为新生代垃圾回收器

使用"-XX:+UseParNewGC"选项即可

ParNew垃圾回收器默认情况下的线程数

一旦我们指定了使用ParNew垃圾回收器之后,默认给自己设置的垃圾回收线程的数量就是跟CPU的核数是一样的。

比如我们线上机器假设用的是4核CPU,或者8核CPU,或者16核CPU,那么此时ParNew的垃圾回收线程数就会分别是4个线程、8个线程、16个线程。

这个线程数一般不需手动调节。

但是如果你一定要自己调节ParNew的垃圾回收线程数量,也是可以的,使用“-XX:ParallelGCThreads"参数即可,通过他可以设置线程的数量。

垃圾回收器采用单线程还是多线程更好?

服务器模式和客户端模式

启动系统的时候是可以区分服务器模式和客户端模式的,如果你启动系统的时候加入"-server”就是服务器模式,如果加入"-cilent”就是客户端模式。

他们俩的区别就是,如果你的系统部署在比如4核8G的Linux服务器上,那么就应该用服务器模式,如果你的系统是运行在比如Windows上的客户端程序,那么就应该是客户端模式。

那么服务器模式和客户端模式的区别是啥呢?

服务器模式通常运行我们的网站系统、电商系统、业务系统、APP后台系统之类的大型系统,一般都是多核CPU。

所以此时如果要垃圾回收,那么肯定是用ParNew更好,因为多线程并行垃圾回收,充分利用多核CPU资源,可以提升性能。

反之如果你部署在服务器上,但是你用了单线程垃圾回收,那么就有一些CPU是被浪费了,根本没用上。

那么如果你的Java程序是一个客户端程序,比如类似百度云网盘的Windows客户端,或者是印象笔记的Windows客户端,运行在Windows个人操作系统上呢?

这种操作系统很多都是单核CPU,此时你如果要是还是用ParNew来进行垃圾回收,就会导致一个CPU运行多个线程,反而加重了性能开销,可能效率还不如单线程好。

因为单CPU运行多线程会导致频繁的线上上下文切换,有效率开销。

所以如果是类似于那种运行在Windows上的客户端程序,建议采用Serial垃圾回收器,单CPU单线程垃圾回收即可,反而效率更高

总结:

但是其实现在一般很少有用Java写客户端程序的,几乎很少见,Java现在主要是用来构建复杂的大规模后端业务系统的,所以常见的还是用"-server”指定为服务器模式,然后配合ParNew多线程垃圾回收器

老年代垃圾回收器 CMS工作原理

复习:什么情况下会触发Full GC

  1. Minor GC前判断 老年代可用内存大小 < 历次Minor GC进入老年代对象的平均大小,为了保险起见,先进行Full GC,再进行Minor GC
  2. Minor GC后,升入老年代的对象大小 大于老年代可用内存空间,要执行Minor GC

CMS垃圾回收器的基本原理

标记-清理算法

先标记处哪些对象是垃圾对象,然后再清理掉。

  • 先通过追踪GC Roots的方法,看看各个对象是否被GC Roots引用了,如果是的话,就是存活对象,否则就是垃圾对象(垃圾对象标记)

缺点:会产生大量内存碎片,不大不小的,断断续续的,一个内存碎片不能容纳一个对象,造成内存空间浪费

执行模式

Stop The World (停止一切工作线程)后采用“标记-清理”算法慢慢地执行垃圾回收,会导致系统卡死时间过长,很多响应无法处理。

因此CMS垃圾回收器采用的是垃圾回收线程和系统工作线程尽量同时执行的模式来处理的。

CMS如何实现系统一边工作的同时进行垃圾回收?

1. 初始标记

会让系统的工作线程全部停止,进入"Stop The World"阶段

“初始标记”: - 标记出来 GC Roots直接引用的对象。

方法的局部变量和类的静态变量是GC Roots对象,但类的实例变量不是GC Roots。

public class Kafka {
    private static ReplicaManager replicaManager = new ReplicaManager();
}

public class ReplicaManager {
    private ReplicaFetcher replicaFetcher = new ReplicaFetcher();
}

在初始标记阶段,仅仅会通过“replicaManager“这个类静态变量代表的GC Roots,去标记出来他直接引用ReplicaManager对象。

这里不会标记ReplicaFetcher对象,因为该对象是被ReplicaManager类的实例变量replicaFetcher所引用的,类的实例变量不是GC Roots。

所以第一个阶段,初始标记,虽然说要造成“Stop the World”暂停一切工作线程,但是其实影响不大,因为他的速度很快,仅仅标记GC Roots直接引用的那些对象罢了。

2. 并发标记

让系统线程随意创建各种对象,继续运行。

在运行期间可能会创建新的存活对象,也可能会让部分存活对象失去引用(垃圾对象)。

在这个过程中,垃圾回收线程会尽可能的对已有对象进行GC Roots追踪

所谓进行GC Roots追踪,意思就是对类似“ReplicaFetcher”之类的全部老年代里的对象,他会去看他被谁引用了? 比如这里是被“ReplicaManager”对象的实例变量引用了,接着会看,“ReplicaManager”对象被谁引用了?会发现被“Kafka”类的静态变量引用了。 那么此时可以认定“ReplicaFetcher”对象是被GCRoots间接引用的,所以此时就不需要回收他。

public class Kafka {
    private static ReplicaManager replicaManager = new ReplicaManager();
}

public class ReplicaManager {
    private ReplicaFetcher replicaFetcher = new ReplicaFetcher();
}

但是在运行并发标记时,系统程序会不停地工作,可能会创建新的对象,部分对象可能成为垃圾。

“并发标记“阶段,会对老年代所有对象进行GC Roots追踪,这是最耗时的。(不存在 Stop The World)

他需要追踪所有对象是否从根源上被GC Roots引用了,但是这个最耗时的阶段,是跟系统程序并发运行的,所以其实这个阶段不会对系统运行造成影响的。

3. 重新标记

因为在并发标记阶段,你一遍标记存活对象和垃圾对象,一遍创建新对象,让老对象成为垃圾;

所以在并发标记阶段执行之后,绝对会有很多存活对象和垃圾对象,是并发标记阶段中没有标记出来的

  1. 重新标记阶段需要让系统停止下来,Stop The World。

  2. 接着重新标记下在并发标记阶段中新创建的对象,还有一些已有对象可能失去引用成为垃圾的情况

这个重新标记的阶段,是速度很快的,他其实就是对在并发标记阶段中被系统程序运行变动过的少数对象进行标记,所以运行速度很快。

4. 并发清理

让程序随意运行,然后垃圾回收器 来清理掉 之前标记为垃圾的对象。

这个阶段其实是很耗时的,因为需要进行对象的清理,但是他也是跟系统程序并发运行的,所以其实也不影响系统程序的执行

CMS执行过程性能分析

耗费性能阶段

CMS设计技巧:在耗费性能阶段并发运行(系统工作线程和垃圾回收线程并发执行),不存在Stop The World;

对于不耗费性能阶段,Stop The World,因为执行速度很快,所以对系统运行响应影响不大。

并发标记阶段

对老年代全部对象进行GC Roots追踪,标记出哪些对象可以被回收掉。

并发清理阶段

将各种垃圾对象从内存中清理掉

CMS执行图示

CMS垃圾回收的细节问题

1. 并发回收垃圾导致CPU资源紧张

在并发标记和并发清理这两个最耗时的阶段,没有Stop The World,而是采用系统工作线程和垃圾回收线程同时工作的,这样会导致有限的CPU资源被垃圾回收线程占用一部分。

  • 并发标记:需要对GC Roots进行深度追踪,看所有对象,有哪些是存活的,但是老年代存活对象有很多,因此该过程会追踪大量的对象,所以耗时较多
  • 并发清理:清理全部垃圾对象,所以耗时很多

在这两个阶段,CMS垃圾回收线程是比较消耗CPU资源的

CMS垃圾回收器默认启动的垃圾回收线程的数量=(CPU核心数+3)/4

我们用最普通的2核4G机器和4核8G机器来计算一下,假设是2核CPU,本来CPU资源就有限,结果此时CMS还会有个“(2+3)/4"=1个垃圾回收线程,去占用宝贵的一个CPU。

CMS的并发垃圾回收机制,首要问题就是会消耗CPU资源。

2. Concurrent Mode Failure 问题

在并发清理阶段,CMS只是回收之前标记好的垃圾对象;但是这个阶段系统一直在运行,可能会随着系统运行让一些对象进入老年代,同时还变成垃圾对象,这些垃圾对象是**“浮动垃圾”**。

浮动垃圾不会被本地的垃圾回收线程回收掉,需要等到下一次垃圾回收的时候回收他们。

所以为了保证在CMS垃圾回收期间,还有一定的内存空间可以让一些对象能进入老年代中来,一般会预留一些空间。

CMS垃圾回收的触发时机,其中有一个就是当老年代内存占用达到一定比例了,就自动执行GC。

**“-XX:CMSInitiatingOccupancyFaction"**参数可以用来设置老年代占用多少比例的时候触发CMS垃圾回收,JDK1.6里面默认的值是92%。 也就是说,老年代占用了92%空间了,就自动进行CMS垃圾回收,预留8%的空间给并发回收期间,系统程序把一些新对象放入老年代。

如果CMS垃圾回收期间,系统程序要放入老年代的对象大于老年代可用内存了,该怎么办呢

此时会发生Concurrent Mode Failure,意思就是并发垃圾回收失败了,我一边回收垃圾,你一边把对象放入老年代,内存都不够了。

此时就会自动用“Serial Old”垃圾回收器替代CMS,直接强行将系统程序“Stop The World”,重新进行长时间的GC Roots追踪,标记出来全部垃圾对象,不允许新对象的产生。然后一次性将垃圾对象清除,然后再恢复系统工作线程。

Serial Old 采用标记-整理算法,不存在内存碎片。

所以在生产实践中,这个自动触发CMS垃圾回收的比例需要合理优化一下,避免“Concurrent Mode Failure”问题(“-XX:CMSInitiatingOccupancyFaction")

3. 内存碎片问题

老年代的CMS采用“标记-清理”算法,每次都是标记出来垃圾对象,然后一次性回收掉,这样会导致大量的内存碎片产生。 如果内存碎片太多,会导致后续对象进入老年代找不到可用的连续内存空间了,然后触发Full GC。

所以CMS不是完全就仅仅用“标记-清理”算法的,因为太多的内存碎片实际上会导致更加频繁的Full GC。

CMS有一个参数是**"-XX:+UseCMSCompactAtFullCollection"**,默认就打开了。

就是说在Full GC之后要再次进行"Stop the World”,停止工作线程,然后进行碎片整理,就是把存活对象挪到一起,空出来大片连续内存空间,避免内存碎片。

还有一个参数是**“-XX:CMSFullGCsBeforeCompaction",这个意思是执行多少次Full GC之后再执行一次内存碎片整理的工作,默认是0**,意思就是每次Full GC之后都会进行一次内存整理

老年代触发垃圾回收的时机

  1. 老年代可用内存小于新生代全部对象大小,并且没有开启空间担保参数,但一般这个空间担保参数默认是开启的

  2. 老年代可用内存小于 历次进入老年代对象平均大小,就Full GC

  3. Minor GC后的存活对象大小 大于Survivor区空间,同时大于老年代可用内存(老年代可用内存不够),执行Full GC

  4. **"-XX:CMSInitiatingOccupancyFaction"**参数

    如果老年代可用内存大于历次新生代GC后进入老年代的对象平均大小但是老年代已经使用的内存空间超过了这个参数指定的比例,也会自动触发Full GC

面试题:为什么老年代的垃圾回收速度比新生代的垃圾回收慢很多倍?慢在哪里?

新生代执行速度其实很快,因为直接从GCRoots出发就追踪哪些对象是活的就行了,新生代存活对象是很少的,这个速度是极快的,不需要追踪多少对象。

然后直接把存活对象放入Survivor中,就一次性直接回收Eden和之前使用的Survivor了。

但是CMS的Full GC呢?

  1. 在并发标记阶段,他需要去追踪所有存活对象,老年代存活对象很多,这个过程就会很慢;

  2. 其次并发清理阶段,他不是一次性回收一大片内存,而是找到零零散散在各个地方的垃圾对象,速度也很慢;

  3. 最后完事儿了,还得执行一次内存碎片整理,把大量的存活对象给挪在一起,空出来连续内存空间,这个过程还得**“Stop the World"**,那就更慢了。

  4. 万一并发清理期间,剩余内存空间不足以存放要进入老年代的对象了,引发了“Concurrent Mode Failure"问题,那更是麻烦,还得立马用“Serial Old”垃圾回收器“Stop the World”之后慢慢重新来一遍回收的过程,这更是耗时了。

案例实战:每日上亿请求量的电商系统,垃圾回收参数如何优化?

优化思路

  1. 分析在特定场景下,预估系统的内存使用模型
  2. 合理优化新生代、老年代、Eden和Survivor区的内存大小
  3. 尽量优化参数,避免新生代的对象进入老年代中,尽量让对象留在新生代中被回收掉

案例背景

电商系统分为如下子系统:

  • 商品系统
  • 订单系统
  • 促销系统
  • 库存系统
  • 会员系统

以订单系统为例,我们的背景是每日上亿请求量的电商系统。

一般按照每个用户访问20次,上亿请求量,大致需要500万日活用户;

按照10%的付费转化率,每天大概有50万人下单,大概是50万个订单

这50万订单算他集中在每天4小时的高峰期内,那么其实平均下来每秒钟大概也就几十个订单,大家是不是觉得根本没啥可说的? 因为几十个订单的压力下,根本就不需要对JVM多关注,基本上就是每秒钟占用一些新生代内存,隔很久新生代才会满,然后一次Minor GC后垃圾对象清理掉,内存就空出来了,几乎无压力。

特殊的电商大促场景

双11,618

假设在类似双11的节日里,零点的时候,很多人等着大促开始就要利手购物,这个时候,可能在大促开始的短短10分钟内,瞬间就会有50万订单。

那么此时每秒就会有接近1000的下单请求,我们就针对这种大促场景来对订单系统的内存使用模型分析一下。

抗住大促的瞬时压力需要几台机器?

基本上可以按3台来算,就是每台机器每秒需要抗300个下单请求。这个也是非常合理的,而且需要假设订单系统部署的就是最普通的标配4核8G机器。

从机器本身的CPU资源和内存资源角度,每台机器抗住每秒300个下单请求是没问题的。(单台机器QPS 几百还是没问题的)

但是问题就在于需要对JVM有限的内存资源进行合理的分配和优化,包括对垃圾回收进行合理的优化,让JVM的GC次数尽可能最少,而且尽量避免Full GC,这样可以尽可能减少JVM的GC对高峰期的系统新更难的影响。

大促高峰期订单系统的内存使用模型估算

订单系统,比如说3台机器,每台机器每秒接受300个下单请求。

一个订单1kb,300个订单300kb

然后算上订单对象连带的订单条目对象、库存、促销、优惠券等等一系列的其他业务对象,一般需要对单个对象开销放大10倍~20倍。

此外,除了下单之外,这个订单系统还会有很多订单相关的其他操作,比如订单查询之类的,所以连带算起来,可以往大了估算,再扩大10倍的量。

那么每秒钟会有大概300kb2010=60mb的内存开销。但是一秒过后,可以认为这60mb的对象就是垃圾了,因为300个订单处理完了,所有相关对象都失去了引用,可以回收的状态。

内存如何分配

机器配置:4核8G

分配给JVM 4G

  • 堆使用3G
    • 年轻代 1.5G
    • 老年代 1.5G
  • Java虚拟机栈 每个 1M,几百个线程 几百M
  • 永久代内存256M
-Xms3072M -Xmx3072M -Xmn1536M  -Xss1M  -XX:PermSize=256M   -XX:MaxPermSize=256M  - XX:HandlePromotionFailure

JDK 1.6后,-XX:HandlePromotionFailure参数已经废弃了。

在JDK1.6以后,只要判断**“老年代可用空间”>“新生代对象总和”,或者“老年代可用空间">“历次Minor GC升入老年代对象的平均大小"**,两个条件满足一个,就可以直接进行Minor GC,不需要提前触发Full GC了。

-Xms3072M -Xmx3072M -Xmn1536M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M

-XX:SurvivorRatio=8(默认值),Eden:Survivor1:Survivor2=8:1:1

Eden区为 1.5G*0.8=1.2G,Survivor区大小为0.15G

订单系统在大促期间,每秒钟处理300个订单请求,占据60MB内存,但是1秒以后,这60MB对象就会变成垃圾,过了20秒左右,Eden区满了,第一次判断老年代可用内存1.5G 并不大于 新生代全部内存大小,但是老年代可用内存刚开始一定大于历次MinorGC进入老年代中的平均对象大小,因为还没进行过Minor GC呢,此时直接进行Minor GC,将Eden区全部清空掉。

我们假设最近一秒的订单请求还在处理,等于大约100M的对象会在最近一次Minor GC后存活下来。

Minor GC后,100MB的存活对象进入S1,再次运行20秒,Eden区满了,就会回收Eden区和S1区,100MB左右的存活对象进入S2区。

-Xms3072M  -Xmx3072M  -Xmn1536M  -Xss1M   -XX:PermSize=256M -XX:MaxPermSize=256M  -XX:SurvivorRatio=8

新生代垃圾回收优化

Survivor区空间够不够

  • 调优分析:

    首先在进行JVM优化的时候,第一个要考虑的问题,就是你通过估算,你的新生代的Survivor区到底够不够按照上述逻辑,首先每次新生代垃圾回收在100MB左右,有可能会突破150MB,那么岂不是经常会出现Minor GC过后的对象无法放入Survivor中?然后岂不是频繁会让对象进入老年代?

    ​ MinorGC后的存活对象 是否可以放入Survivor中

    还有,即使Minor GC后的对象少于150MB,但是即使是100MB的对象进入Survivor区,因为这是一批同龄对象,直接超过了Survivor区空间的50%,此时也可能会导致对象进入老年代

因此 Survivor区是明显不够大的。

  • 调优措施:

    这里其实建议的是调整新生代和老年代的大小,因为这种普通业务系统,明显大部分对象都是短生存周期的,根本不应该频繁进入老年代,也没必要给老年代维持过大的内存空间首先得先让对象尽量留在新生代里

    可以将新生代内存设置为2G,老年代内存为1G,Eden区 1.6G,S1/S2分别占据 200MB。

  • 调优结果:

    Survivor区域变大,就大大降低了新生代GC过后存活对象在Survivor里放不下的问题,或者是同龄对象超过Survivor 50%的问题。

-Xms3072M -Xm×3072M -Xmn2048M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:SurvivorRatio=8 

新生代对象躲过多少次垃圾回收进入老年代?

默认是15次Minor GC,在我们这个场景下,15次就是300秒,大约5分钟,这样的对象一般为Spring Controller,Service Bean,属于长期存活的核心业务组件。

那么他就应该进入老年代,何况这种对象一般很少,一个系统累计起来最多也就几十MB而已

因此我们完全可以将15次改为5次,对于长期存活的对象,早点进入老年代就行。

-Xms3072M -Xm×3072M -Xmn2048M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:SurvivorRatio=8  -XX:MaxTenuringThreshold=5

多大的对象直接进入老年代

一般来说,给他设置个1MB足以,因为一般很少有超过1MB的大对象。

如果有,可能是你提前分配了一个大数组、大List之类的东西用来放缓存的数据。

-Xms3072M -Xm×3072M -Xmn2048M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:SurvivorRatio=8  -XX:MaxTenuringThreshold=5 -XX:PretenureSizeThreshold=1M

垃圾回收器指定

新生代使用ParNew,老年代使用CMS。

-Xms3072M -Xm×3072M -Xmn2048M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:SurvivorRatio=8  -XX:MaxTenuringThreshold=5 -XX:PretenureSizeThreshold=1M
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC

新生代垃圾回收优化总结

ParNew垃圾回收器的核心参数,其实就是配套的新生代内存大小、Eden和Survivor的比例,只要你设置合理,避免Minor GC后对象放不下Survivor进入老年代,或者是动态年龄判定之后进入老年代,给新生代里的Survivor充足的空间,那么Minor GC一般就没什么问题。

然后根据你的系统运行模型,合理设置“-XX:MaxTenuringThreshold",让那些长期存活的对象,抓紧尽快进入老年代,别在新生代里一直待着。

去看看自己生产系统的JVM参数了,看看你的新生代、老年代、Eden和Survivor的大小然后去估算一下你的系统运行模型:

  • 每秒占用多少内存?
  • 多长时间触发一次Minor GC?
  • 一般Minor GC后有多少存活对象?
  • Survivor能放的下吗?
  • 会不会频繁因为Survivor放不下导致对象进入老年代?
  • 会不会因动态年龄判断规则进入老年代?

老年代垃圾回收

-Xms3072M -Xm×3072M -Xmn2048M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:SurvivorRatio=8  -XX:MaxTenuringThreshold=5 -XX:PretenureSizeThreshold=1M
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC

以上是目前制定的JVM参数。

我们再来分析老年代

当前案例,什么情况下,对象会进入老年代

-XX:MaxTenuringThreshold=5

连续躲过5次Minor GC的对象直接进入老年代

  • @Controller
  • @Service

大对象

  • -XX:PretenureSizeThreshold=1M

    在我们的案例中,假定大对象不存在,或者忽略不计。

Survivor空间不足

此外就是Minor GC过后可能存活的对象超过200MB放不下Survivor了,或者是一下子占到超过Surviovr的50%,此时会有一些对象进入老年代中。

但是我们之前对新生代的JVM参数进行优化,就是为了避免这种情况,经过我们的测算,这种概率应该是很低的。

但是虽说是很低,也不能完全是是没有这种情况,比如某一次GC过后可能刚好机缘巧合有超过200MB对象,就会进入老年代里。

我们可以做一个假设,大概就是这个订单系统在大促期间,每隔5分钟会在Minor GC之后有一小批对象进入老年代,大概200MB左右的大小。

大促期间多久会触发一次Full GC

  1. 每次MinorGC之前,会比较老年代可用内存和历次MinorGC后升入老年代对象平均大小

    老年代可用内存 < 历次MinorGC后升入老年代对象平均大小

  2. 可能某次Minor GC后,要升入老年代的对象大小 > 老年代可用内存大小

  3. 设置了“-XX:CMSInitiatingOccupancyFaction"参数,比如设定值为92%,那么此时可能前面几个条件都没满足,但是刚好发现这个条件满足了,比如就是老年代空间使用超过92%了,此时就会自行触发Full GC

其实在真正的系统运行期间,可能会慢慢的有对象进入老年代,但是因为新生代我们优化过了内存分配,所以对象进入老年代的速度是很慢的。

所以很可能是在系统运行半小时~1小时之后,才会有接近1GB的对象进入老年代。 此时可能是触发了1,2,3中的某个条件,Full GC。

但是这三个条件一般都需要在老年代空间近乎沾满的时候,才可能会触发。

假设在大促期间,订单系统运行1小时之后,大促下单高峰期几乎都快过了,此时才可能会触发一次Full GC。

注意,这个推论很重要,因为按照大促开始10分钟就有50万订单来计算,其实大促开始后一堆用户等着下单剁手购物那么1小时候就可能有两三百万订单了,这是一年难得罕见的节日大促才会有的,然后这个高峰期过后,基本订单系统访问压力就很小了,那么GC的问题几乎就更不算什么了。

所以经过新生代的优化,可以推算出,基本上大促高峰期内,也就可能1小时才1次FullGC,然后高峰期一过,随着订单系统慢慢运行,可能就要几个小时才有一次Full GC

Concurrent Mode Failure问题

一边执行Full GC,一边Minor GC后生成的对象放不下Survivor区,直接进入老年代,老年代中可用内存不足了

这种情况概率很低,可以不考虑

-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

内存碎片整理

Full GC大约1小时一次吧,默认情况下,每次Full GC执行一次内存碎片整理,其实也不算啥的。

-Xms3072M-Xm×3072M-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

Full GC优化的前提是Minor GC的优化,Minor GC的优化的前提是合理分配内存空间合理分配内存空间的前提是对系统运行期间的内存使用模型进行预估。

线上系统思考

看看你们的线上系统是怎么设置的JVM垃圾回收参数?设置的合理吗?

直接去看看自己生产系统的JVM参数了,看看你的新生代、老年代、Eden和Survivor的大小,然后去估算一下你的系统运行模型,每秒占用多少内存,多长时间触发一次Minor GC。

一般Minor GC后有多少存活对象,Survivor能放的下吗?会不会频繁因为Survivor放不下导致对象进入老年代?会不会因动态年龄判断规则进入老年代?

根据案例的思路看看,你的各个内存区域应该给多大的大小,保证新生代gc后的存活对象尽量都留在Survivor里。

另外,对你的系统运行模型做出预估,看一般多久会塞满老年代触发Full GC?触发Full GC的时候是否需要优化CMS相关的各种参数?

结合案例学到的知识,自己动手画图,把你们系统的运行模型和内存分配,gc触发,全流程和模型都画出来,一定要自己去分析、自己思考、自己画图,这样才真正把知识给吸收了。