Java垃圾收集器实现

本博文对应《深入理解java虚拟机》第三章的第二部分

垃圾收集器

之前讲述了几种常见的java垃圾回收算法,只有算法显然是不行的,还需要具体的实现。这里介绍几种垃圾收集器的实现——三个新生代的垃圾收集器,三个老年代的垃圾收集器和一个全能的垃圾收集器G1。

Serial收集器

Serial意为“序列”之意,它是一个单线程的新生代区垃圾收集器。在它工作的时候,会停止所有的线程,直到它垃圾回收完毕。显然从用户的角度来说,这是不能接受的,好好运行的程序为什么非要停下来等待垃圾处理呢,但是没办法,直到目前人们也只是能缩短这个时间,而不能完全消除这个时间。Serial收集器虽然简单,但是它很简单高效,所以它仍然是client模式下非常好的一个选择(也是默认选择)。

该收集器对新生代采用标记-复制算法,而对老年代采用标记-整理算法,不过我们并不会让它去搜集老年代就是了…

ParNew收集器

简单理解这东西就是Serial收集器的多线程版本(单核情况下请使用Serial收集器)。它是运行于服务器端的新生代区垃圾收集器。

所以除了多线程,其它完全就是Serial收集器的复制版。

Parallel Scavenge收集器

第三个工作于新生代区域的收集器。使用的算法还是标记-复制算法。别的收集器的目的在于:尽可能缩短用户线程的等待时间,而这个收集器的目的在于:提高吞吐量。吞吐量的计算是 :运行用户的代码的时间/(运行用户的代码的时间+GC时间),想要提高吞吐量,要么缩短GC时间(这不太现实)要么提高运行代码的时间,所以这个收集器在对于与用户交互的程序上是非常不合适的,它非常适合那些高计算量的程序,因为它能够保证运行代码的时间尽可能的长。

新生代的三个垃圾收集器总结一下就是:一个单线程,一个多线程,一个多线程并且注重吞吐量。


Serial Old收集器

这东西就是Serial收集器的换皮版本,只不过用来收集老年代区域的垃圾,算法改成了标记-整理算法。

Parallel Old收集器

也是加了个old就从新生代到老年代里了。使用的算法是标记-整理算法。仍然为了吞吐量的提升而不懈努力。如果你的服务器比较在乎吞吐量,或者处理器的资源比较稀缺,可以使用Parallel Scavenge收集器+Parallel Old收集器的这一组合。

CMS收集器

Concurrent Mark Sweep,这个收集器的目的也很简单:“最小化停顿时间”,所以对于那些B/S系统来说,它用的非常广泛。它使用的是标记-清除算法。

它的垃圾回收分成了4个过程:

  • 初始标记(stop the world),从GC root开始走能直接走到的对象
  • 并发标记,持续追踪上一阶段打上标记的对象,这一阶段和用户的线程并发执行,并不要求用户线程停下来。
  • 重新标记(stop the world),因为程序持续运行所以标记对象会发生改变,需要做出调整。
  • 并发清除

整个过程耗时最长的是并发标记和并发清除,但是由于有两个并发的存在,所以其实这个东西效率很高,缺点也很明显:由于并发要占用CPU的资源,所以它会导致用户的程序变慢;由于是并发运行的,所以有些垃圾是标记不出来的,同时也导致了它不能等到老年代快满了才进行垃圾收集,而是老年代到60%的时候就需要启动了;最后就是它是标记-清除算法,所以可能会导致碎片过多而不得不进行Full GC。总的来说,并发确实带来了好的响应时间,但是也带来了不少问题,最终那肯定是利大于弊的。

老年代的三个垃圾收集器就是:一个单线程,一个多线程注重吞吐量,一个并发注重用户响应时间。


G1收集器

Garbage First收集器,G1,sun公司对其赋予厚望的一款垃圾收集器,目的是为了取代CMS收集器。在JDK9之后,在服务器端,G1代替了Parallel Scavenge收集器+Parallel Old收集器的这一组合,成为了默认的垃圾收集器。

G1最大的特点是,相比你也看出来了,别的收集器要么针对新生代,要么针对老年代,而它将整个堆区域(即老年代+新生代)看成整体,然后将整体分成了大小相等的N个区域(称为region),对于每个区域,它都跟踪这个区域能够回收的空间大小和所需时间(通过经验得出的),这样在垃圾回收的时候它就能够去回收最值得回收的那块区域。这也就是它为什么叫Garbage First的原因。

听上去简单,但是实则很难。这是因为之前为什么所有的这些收集器要么工作在老年代,要么工作在新生代呢?这是因为你每个region中的对象,还可能引用别的region中的对象呀,难不成我每次都要扫描整个堆来确认引用关系么?肯定需要一个解决办法来防止每次都进行一次完整的全堆扫描。

在G1中,用的是Remembered Set来解决的。每个region都有一个Remembered Set,当你要对一个引用对象的数据进行写操作的时候,会首先检查是否这两个对象处于不同的region,如果是的话,那就需要在被引用的对象所属的region的Remembered Set中记录,这样可以不用全局扫描。

G1的工作流程大致分成几个步骤:

  • 初始标记:标记下GC Roots能够直接关联到的对象
  • 并发标记:和用户线程并发运行,扫描整个堆
  • 最终标记:短暂停止用户线程。
  • 筛选回收:更新统计数据,并且对每个Region的回收价值、成本进行考量,得到回收计划并进行执行。

可以看到和CMS有几分相似,第一步是标记一下GC roots能够直接关联到的对象,然后修改TAMS(Next Top at Mark Start)的值,主要是为了接下来用户程序并发运行的时候,在正确的region中创建新对象。第二个阶段是并发标记,这个阶段是进行可达性分析,和用户的程序同时运行。第三部分就是为了修正在用户标记过程变化的。最后一阶段就是执行垃圾回收。

作者的经验之谈:在小内存应用中,CMS优于G1,而大内存中G1优于CMS。这个内存的平衡点普遍在6-8G。


商业的,成熟的垃圾收集器就这么几款,其实还有注重低延迟的实验性质的垃圾回收器,甚至还有不进行垃圾回收的垃圾收集器,这里略过了。

GC日志

对于垃圾回收来说,日志肯定是非常重要的,为了能够在日后进行调优等操作。但是由于收集器的不同,所以它们的日志方面会存在或多或少的差异,但是仍然是有一些规律可循的。

一般的GC日志会包含GC清理前区域的已使用的量,GC清理完之后区域所剩余的量,以及该区域总的量。

当然我们可以通过可视化工具更好的查看日志,这部分留到之后讲述。