Java GC机制小结

毫无疑问,目前GC(垃圾回收)已经成为现代编程语言的标配。网上有关于各类JDK GC原理、优化的文章至少上万篇,但质量参差不齐,其中理解有误的文字以讹传讹,遍布各地。不仅仅是一些个人开发者的文章,甚至一些大厂的官方博客也有错误。本文只是在他们之中再增加一篇,仅供大家参考,欢迎批评指正。

GC中的一些概念

Java GC文章中常会见到一些和GC算法相关的概念,总结一些常见名词如下:

  • mutator:直译是变异子的意思,在GC中通常可以认为就是应用程序线程。
  • collector:用于进行垃圾回收的线程。
  • concurrent:并发,指垃圾收集线程(collector)和应用程序线程(mutator)可以并发执行。
  • parallel:并行,指垃圾收集线程(collector)是多线程的,可以利用多核CPU进行工作。
  • young/old:根据大多数对象“朝生夕死”的特点,现代GC大多把heap分为young区和old区,old区在很多资料也被称为tenured区。

需要注意的是一个GC算法可以同时具有concurrent/parallel的特性,或者只具有其中一个特性。

HotSpot VM支持的GC

目前Java 13已经发布GA版本,Java 14也马上要发布GC版本,但据统计Java 8仍然是目前的主流版本,因此本文仅总结HotSpot VM在Java 8中存在的GC算法,对于新推出的GC算法另起文章讨论:
在这里插入图片描述

新生代GC

Serial GC

Serial GC采用的收集方式十分简单,就是暂停所有的应用程序线程(也就是常说的Stop The World,STW),并使用一个线程串行的进行垃圾收集,这也就是该GC算法得名Serial的原因。Serial GC是复制算法,针对新生代来说,当应用程序启动后,应用程序线程会不断的在Eden区申请空间(可以使用TLAB方式,当然也可以不使用)用于存放对象,直到Eden区无法再分配空间时会停顿所有的应用程序线程并发生一次Serial GC(第一次Serial GC时,to survivor区和from survivor区都为空)。Serial GC后,原Eden区存活的对象或晋升到老年代或被复制到to survivor区,之后应用程序继续向Eden区申请空间,直到Eden区再次无法分配空间,此时无论to survivor区或者from survivor区是否有足够的空闲空间,都会再次触发Serial GC,Eden区及from survivor区(即原to survivor区)中存活的对象或晋升到老年代或复制到to survivor区(即原from survivor区)。也就是说应用程序只会向Eden区申请内存。

ParNew

根据R大的解释,原本HotSpot VM中并没有并行GC,当时就只有NewGeneration,后来要加入新生代的并行GC,就把原本的NewGeneration改名为DefNewGeneration,然后把新加入的并行版本GC叫做ParNewGeneration。DefNewGeneration就是Default New Generation,ParNewGeneration就是Parallel New Generation,也就是说ParNew就是并行新生代GC算法的意思,在ParNewGC算法中同样会暂停所有的应用程序线程,只是会使用多个垃圾收集线程进行GC,相对Serial GC来说更能够发挥多核CPU服务器的性能优势。ParNew GC算法也是复制GC算法。

Parallel Scavenge

ParNew和Parallel Scavenge都是并行GC,且都是并行收集young generation,目的和性能其实是差不多的。那么问题来了,为毛HotSpot VM团队会维护两个功能几乎一样但是各种具体实现细节不同的并行GC呢?这不是给自己找麻烦么?R大的解释是Hot SpotVM里有一个“分代式GC框架”(该框架在新版OpenJDK中已被删除),这个框架内的GC具体到源码里都是以XXXGeneration命名。HotSpot VM鼓励开发者尽量在这个框架内开发GC,但是后来有个开发就是不愿意被这个框架憋着,自己硬写了一个没有使用已有框架的新并行GC,并拉拢性能测试团队用这个并行GC来跑分,成绩也不错,于是这个GC就放进了HotSpot VM中了,这就是我们现在看到的Parallel Scavenge GC。
Parallel Scavenge GC和ParNew GC最明显的区别有如下几点:

  1. Parallel Scavenge GC以前是广度优先顺序来遍历对象图的,Java 6的时候改为默认使用深度优先顺序遍历,并留下一个UseDepthFirstScavengeOrder参数来选择使用深度还是广度优先。在JDK6u18之后这个参数被去掉,Parallel Scavenge GC变为只用深度优先遍历,ParNew则是一直都只用广度优先顺序来做遍历。
  2. Parallel Scavenge GC完整实现了adaptive size policy,而ParNew GC及“分代式GC框架”内的其它GC都没有实现完(倒不是不能实现,就是麻烦+没人力资源去做)。所以千万千万别在用ParNew GC+CMS GC的组合下用UseAdaptiveSizePolicy,请只在使用UseParallelGC或UseParallelOldGC的时候用它。
  3. 由于在“分代式GC框架”内,ParNew GC可以跟CMS GC搭配使用,而Parallel Scavenge GC不能。当时ParNew GC被从Exact VM移植到HotSpot VM的最大原因就是为了跟CMS GC搭配使用。
  4. 在Parallel Scavenge GC成为主要的throughput GC之后,它还实现了针对NUMA的优化;而ParNew一直没有得到NUMA优化的实现。

老年代

Serial Old

Serial Old GC是Serial GC算法的老年代版本,同样整个过程会Stop The World,且使用单线程串行的收集整个Heap。注意是收集整个Heap,即除了手机老年代还会收集年轻代及Metaspace或者Perm Gen(如果存在的话)。我们从图片上还可以看到Serial Old方框中还用括号包含了MSC,这个我们在接收CMS GC的时候在进行详细说明。
HotSpot VM中GC可以准确的分为两大类(Java 8中):

  • Partial GC:并不收集整个GC堆的模式
    Young GC:只收集Young Gen的GC
    Old GC:只收集 Old Gen的GC,只有CMS的concurrent collection是这个模式
    Mixed GC:收集整个Young Gen以及部分Old Gen的GC,只有G1是这个模式
  • Full GC:收集整个堆,包括Young Gen、Old Gen以及Metaspace(还有Perm Gen如果存在话)等所有部分的模式

Parallel Old

前面有说到Parallel Scavenge GC的一些历史,但Parallel Scavenge GC只是并行收集Young Gen,那Old Gen呢?其实最初Parallel Scavenge GC的目标只是并行收集Young Gen,而Full GC实际实现还是和Serial GC一样。只不过因为Parallel Scavenge GC没有使用HotSpot VM里的“分代式GC框架”,自己实现了一个该框架中CollectionHeap的子类ParallelScavengeHeap,里面都弄了独立的一套接口,因而跟HotSpot当时其它几个GC不兼容。真实有用的代码大部分就在PSScavenge(等于Parallel Scavenge的Scavenge)中,也就是负责收集Young Gen的收集器,而负责Full GC的收集器叫做PSMarkSweep(等于Parallel Scavenge的MarkSweep),就是在Serial Old GC的核心外面套了层皮而已,骨子里还是一样的LISP2算法实现的mark-compact收集器。因此,PSScavenge才是和ParNew对等的东西,Parallel Scavenge这个名字既指代整套GC算法(PSScavenge + PSMarkSweep),也可以指代真正卖点PSScavenge。
鬼魅的是不知道什么原因导致Full GC并行化并没有在原本的“分代式框架”上进行,而只是在Parallel Scavenge系上进行了,其成果就是使用LISP2算法实现的并行版Full GC收集器,名为PSCompact(等于Parallel Scavenge的MarkCompact)也就是所谓的Parallel Old GC。
Parallel Old GC另外一个名字是throughput GC,主要用在对延迟要求低,更看重吞吐量的应用上。相对Serial Old GC来说Parallel Old会使用多线程来进行整个堆回收,主要注意的是,Parallel Old GC运行时会Stop The World,因此不存在和mutator同步的问题,也就是说Parallel Old GC没有实现并发特性。Parallel GC实现了adaptive size policy。我们开发者能控制的选项:

  • -XX:MaxGCPauseMillis = N 应用停顿(STW)的最大时间
  • -XX:GCTimeRatio = N GC时间占整个应用的占比,默认99,需要注意的是,它是这么用的1 / (1 + N),即默认GC占应用时间1%

上面两个指标是软限制,GC会采用自适应策略来调整young/old大小来满足。

对于CMS GC及G1 GC我会在接下来的文章中继续总结。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值