JVM垃圾收集器详解

1.垃圾收集器

F416DCBD-3249-41CD-BA39-7069ACA0C3F5.png

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
虽然我们对各个收集器进行比较,但并非为了挑选出一个最好的收集器。因为直到现在为止还没有 最好的垃圾收集器出现,更加没有万能的垃圾收集器,我们能做的就是根据具体应用场景选择适合 自己的垃圾收集器试想一下:如果有一种四海之内、任何场景下都适用的完美收集器存在,那么 我们的Java虚拟机就不会实现那么多不同的垃圾收集器了。

1.1 Serial收集器(-XX:+UseSerialGC -XX:+UseSerialOldGC)

Serial(串行)收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一 个单线程收集器了。它的 “单线程”的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃 圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( "Stop The World" ),直到它收集结束。
新生代采用复制算法,老年代采用标记-整理算法。

37AC8B05-2FE9-41EA-82C4-5EB5000C7AA8.png

虚拟机的设计者们当然知道Stop The World带来的不良用户体验,所以在后续的垃圾收集器设计
中停顿时间在不断缩短(仍然还有停顿,寻找最优秀的垃圾收集器的过程仍然在继续)。
但是Serial收集器有没有优于其他垃圾收集器的地方呢?当然有,它简单而高效(与其他收集器的单线程相比)。Serial收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。Serial Old收集器是Serial收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用 途:一种用途是在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用,另一种用途是 作为CMS收集器的后备方案。

1.2 ParNew收集器(-XX:+UseParNewGC)

ParNew收集器其实** 就是Serial收集器的多线程版本** ,除了使用多线程进行垃圾收集外,其余行为 (控制参数、收集算法、回收策略等等)和Serial收集器完全一样。默认的收集线程数跟cpu核数 相同,当然也可以用参数(-XX:ParallelGCThreads)指定收集线程数,但是一般不推荐修改。
新生代采用复制算法,老年代采用标记-整理算法。

image.png

它是许多运行在Server模式下的虚拟机的首要选择,除了Serial收集器外,只有它能与CMS收集器 (真正意义上的并发收集器,后面会介绍到)配合工作。

1.3 Parallel Scavenge收集器(-XX:+UseParallelGC(年轻代),- XX:+UseParallelOldGC(老年代))

Parallel Scavenge 收集器类似于ParNew 收集器,是Server 模式(内存大于2G,2个cpu)下的 默认收集器,那么它有什么特别之处呢?
Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU)。CMS等垃圾收集器的关注点
更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是CPU中用于运行用户代码的时 间与CPU总消耗时间的比值。
Parallel Scavenge收集器提供了很多参数供用户找到最合适的停顿 时间或最大吞吐量,如果对于收集器运作不太了解的话,可以选择把内存管理优化交给虚拟机去完 成也是一个不错的选择。
新生代采用复制算法,老年代采用标记-整理算法。

image.png

Parallel Old收集器是Parallel Scavenge收集器的老年代版本。使用多线程和“标记-整理”算 法。在注重吞吐量以及CPU资源的场合,都可以优先考虑 Parallel Scavenge收集器和Parallel Old收集器。

1.4 CMS收集器(-XX:+UseConcMarkSweepGC(old))

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它 非常符合在注重用户体验的应用上使用,它是HotSpot虚拟机第一款真正意义上的并发收集器, 它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
从名字中的Mark Sweep这两个词可以看出,CMS收集器是一种 “标记-清除”算法实现的,它 的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:
初始标记:暂停所有的其他线程,并记录下gc roots直接能引用的对象,速度很快 ;
并发标记: 同时开启GC和用户线程,用一个闭包结构去记录可达对象。但在这个阶段 结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新 引用域,所以GC线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引 用更新的地方。
重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记 产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍 长,远远比并发标记阶段时间短
并发清理: 开启用户线程,同时GC线程开始对未标记的区域做清扫。

image.png

从它的名字就可以看出它是一款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下面 几个明显的缺点:

  • 对CPU资源敏感(会和服务抢资源);
  • 无法处理浮动垃圾(在并发清理阶段又产生垃圾,这种浮动垃圾只能等到下一次gc再清理 了);
  • 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生,当然 通过参数-XX:+UseCMSCompactAtFullCollection 可以让jvm在执行完标记清除后再做整 理
  • 执行过程中的不确定性,会存在上一次垃圾回收还没执行完,然后垃圾回收又被触 发的情况,特别是在并发标记和并发清理阶段会出现,一边回收,系统一边运行,也许没回 收完就再次触发full gc,也就是"concurrent mode failure",此时会进入stop the world,用serial old垃圾收集器来回收
    CMS的相关参数
  1. -XX:+UseConcMarkSweepGC:启用cms
  2. -XX:ConcGCThreads:并发的GC线程数
  3. -XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩整理(减少碎片)
  4. -XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩一次,默认是0,代表每次 FullGC后都会压缩一次
  5. -XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发FullGC(默认 是92,这是百分比)
  6. -XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(- XX:CMSInitiatingOccupancyFraction设定的值),如果不指定,JVM仅在第一次使用设定 值,后续则会自动调整
  7. -XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc,目的在于减少 老年代对年轻代的引用,降低CMS GC的标记阶段时的开销,一般CMS的GC耗时 80%都在 remark阶段

亿级流量电商系统如何优化JVM参数设置(ParNew+CMS)
大型电商系统后端现在一般都是拆分为多个子系统部署的,比如,商品系统,库存系统,订单系 统,促销系统,会员系统等等。
我们这里以比较核心的订单系统为例

image.png

对于8G内存,我们一般是分配4G内存给JVM,正常的JVM参数配置如下:

‐Xms3072M ‐Xmx3072M ‐Xmn1536M ‐Xss1M ‐XX:PermSize=256M ‐XX:MaxPermSize=256M ‐XX:SurvivorRat io=8

image.png

系统按每秒生成60MB的速度来生成对象,大概运行20秒就会撑满eden区,会出发minor gc,大 概会有95%以上对象成为垃圾被回收,可能最后一两秒生成的对象还被引用着,我们暂估为 100MB左右,那么这100M会被挪到S0区,回忆下动态对象年龄判断原则,这100MB对象同龄而 且总和大于S0区的50%,那么这些对象都会被挪到老年代,到了老年代不到一秒又变成了垃圾对 象,很明显,survivor区大小设置有点小 我们分析下系统业务就知道,明显大部分对象都是短生存周期的,根本不应该频繁进入老年代,也 没必要给老年代维持过大的内存空间,得让对象尽量留在新生代里。 于是我们可以更新下JVM参数设置:

‐Xms3072M ‐Xmx3072M ‐Xmn2048M ‐Xss1M ‐XX:PermSize=256M ‐XX:MaxPermSize=256M ‐XX:SurvivorRat io=8

image.png

这样就降低了因为对象动态年龄判断原则导致的对象频繁进入老年代的问题,其实很多优化无非就 是让短期存活的对象尽量都留在survivor里,不要进入老年代,这样在minor gc的时候这些对象 都会被回收,不会进到老年代从而导致full gc。 对于对象年龄应该为多少才移动到老年代比较合适,本例中一次minor gc要间隔二三十秒,大多 数对象一般在几秒内就会变为垃圾,完全可以将默认的15岁改小一点,比如改为5,那么意味着对 象要经过5次minor gc才会进入老年代,整个时间也有一两分钟了,如果对象这么长时间都没被回 收,完全可以认为这些对象是会存活的比较长的对象,可以移动到老年代,而不是继续一直占用 survivor区空间。
对于多大的对象直接进入老年代(参数-XX:PretenureSizeThreshold),这个一般可以结合你自己系统 看下有没有什么大对象生成,预估下大对象的大小,一般来说设置为1M就差不多了,很少有超过 1M的大对象,这些对象一般就是你系统初始化分配的缓存对象,比如大的缓存List,Map之类的 对象。
可以适当调整JVM参数如下:

 ‐Xms3072M ‐Xmx3072M ‐Xmn2048M ‐Xss1M ‐XX:PermSize=256M ‐XX:MaxPermSize=256M ‐XX:SurvivorRa tio=8
2 ‐XX:MaxTenuringThreshold=5‐XX:PretenureSizeThreshold=1M‐XX:+UseParNewGC‐XX:+UseConcMarkSw eepGC

对于老年代CMS的参数如何设置我们可以思考下,首先我们想下当前这个系统有哪些对象可能会 长期存活躲过5次以上minor gc最终进入老年代。 无非就是那些Spring容器里的Bean,线程池对象,一些初始化缓存数据对象等,这些加起来充其 量也就几十MB。
还有就是某次minor gc完了之后还有超过200M的对象存活,那么就会直接进入老年代,比如突然 某一秒瞬间要处理五六百单,那么每秒生成的对象可能有一百多M,再加上整个系统可能压力剧 增,一个订单要好几秒才能处理完,下一秒可能又有很多订单过来。 我们可以估算下大概每隔五六分钟出现一次这样的情况,那么大概半小时到一小时之间就可能因为 老年代满了触发一次Full GC,Full GC的触发条件还有我们之前说过的老年代空间分配担保机制, 历次的minor gc挪动到老年代的对象大小肯定是非常小的,所以几乎不会在minor gc触发之前由 于老年代空间分配担保失败而产生full gc,其实在半小时后发生full gc,这时候已经过了抢购的最 高峰期,后续可能几小时才做一次FullGC。 对于碎片整理,因为都是1小时或几小时才做一次FullGC,是可以每做完一次就开始碎片整理。 综上,只要年轻代参数设置合理,老年代CMS的参数设置基本都可以用默认值,如下所示:

1 ‐Xms3072M ‐Xmx3072M ‐Xmn2048M ‐Xss1M ‐XX:PermSize=256M ‐XX:MaxPermSize=256M ‐XX:SurvivorRa tio=8
2 ‐XX:MaxTenuringThreshold=5‐XX:PretenureSizeThreshold=1M‐XX:+UseParNewGC‐XX:+UseConcMarkSw eepGC
3 ‐XX:CMSInitiatingOccupancyFaction=92‐XX:+UseCMSCompactAtFullCollection‐XX:CMSFullGCsBefore Compaction=0

你可能感兴趣的:(JVM垃圾收集器详解)