JVM之历代垃圾收集器讲解
总览
分类
一,采用分代理念的垃圾回收器:
1.年轻代:
Serial
PartNew
Parallel Scavenge
2.老年代:
CMS
Serial Old(MSC)
Parallel Old
二,不采用分代理念的垃圾回收器:
G1
ZGC
Shenandoah
可搭配使用的各个收集器之间关系图:
并行和并发
很多人经常把这两个搞混,当然笔者刚开始的时候也是傻傻分不清楚。其实只要记住并行说的是GC 线程之间的关系,而并发说的是GC和用户线程之间的关系。
并行:同一时间有多条这样的线程在协同工作,但是此时用户线程时等待状态
并发:同一时间GC和用户线程可以一起工作一起运行。因此程序依然能够响应用户线程的操作但是由于GC线程也占用了一部分系统资源,所以此时的用户线程处理的效率会下降
年轻代垃圾回收器
Serial收集器
特点
垃圾回收时需要STW,整个STW需要停止掉所有的用户线程来保证回收过程中引用关系不会发生变化。
但是并不是说垃圾回收的时候只会启用一个回收线程,更准确的描述应该是同一时间只允许一个垃圾回收线程工作,也就是不支持并行工作,多个GC线程之间串行工作。
优点
1.对于内存资源受限的机器来说比较友好:
由于回收时停止掉了所有的用户线程,因此他不必维护那些:用户线程和GC线程同时运行的时候在回收过程中为了保证引用关系发生变化的额外内存开销;比如上一篇文章说到的原始快照和增量更新。
2.而且由于GC线程不是并行的,所以没有线程之间的交互;对于处理器内核少(线程少)的机器来说,
第一点也就是线程串行执行一个线程完了之后才能执行下一个线程,而对于并行的来说其实本质上还是串行只不过各个线程间可以自由来回切换,所以需要对切换前后的资源进行额外的保存等等因此并发涉及到的这部分线程交互开销对于该款串行执行的GC线程时没有的
图示:
缺点
但是缺点也很明显:回收过程中停止掉所有用户线程,对用户肯定是不能容忍的
PartNew收集器
特点:
该款垃圾收集器和刚才讲的第一个Serial收集器其实最大的不同就是GC Thread可以并行的区别。
注意是并行,之后讲解的CMS才是可以实现GC Thread并发的收集器。
Parallel Scavenge收集器
特点
该款垃圾收集器同样和PartNew收集器一样,并行GC线程。只不过该款收集器重点是倾向于吞吐量。
老年代垃圾回收器
Serial Old收集器
区别于Serial回收器只是回收算法的不同
Parallel Old收集器
Parallel Scavenge收集器的老年代版本,支持多线程并发收集。
组合
吞吐量优先垃圾回收器组合:
新生代采用Parallel Scavenge收集器,老年代采用Parallel Old收集器
CMS收集器
全称:Concurrent Mark Sweep
特点
1.采用标记-清除算法实现
2.和Parallel 系列的收集器注重点不同:
Parallel 注重吞吐量 CMS注重STW的停顿时间
工作流程
1.初始标记
CMS inital mark
标记GC Roots直接关联的对象
需要STW
2.并发标记
CMS concurrent mark
根据上一步和GC Roots直接关联的对象进行遍历整个堆里面的对象图并进行标记,由于是并发的所以并不需要停顿用户线程,但是比较耗时因为遍历整个对象图。
3.重新标记
CMS remark
在并发标记期间由于和用户线程时并发的,所以这段期间用户线程可能更新了引用,所以需要进行一次修正(详见上一篇文章中讲到的增量更新)
需要STW,STW停顿时间比初始标记时间长,但是并没有并发标记运行时间长
4.并发清除
清除删掉被标记为垃圾的对象,由于采用的是标记-清除算法,所以并不需要移动存活对象因此是可以并发的。(当内存碎片严重到不足以分配对象时其实还是需要进行标记-整理算法的,这个时候就会提前触发FullGC。)
注意:在并发阶段产生的**“浮动垃圾”**(并发时用户线程产生的垃圾),需要等到下一次垃圾回收才能进行清理;而且并发的时候需要预留一部分内存空间供用户线程使用,所以不能等到老年代完全用完在进行清理。JDK5的默认设置是当老年代使用了65%的空间就会触发GC。
G1 收集器
特点
思维方式的重大转变:
G1之前的收集器都是分代收集的思想,根据不同的代采用不同的GC:新生代(Minor GC),老年代(Major GC),整个JAVA堆(Full GC)。
但是GC是根据哪块内存中垃圾数量多回收效益最大来区分的,面向的是堆内存中的任意一块内存来组成回收集(Collection Set)进行回收。
实现
基于Region的堆内存布局来实现该回收过程。不再以固定大小及固定数量来划分分代区域,而是把连续的堆内存区域进行划分为各自独立大小相等的区域(Region),每一个Region都可以根据需要扮演新生代中的Enen空间,Survivor空间,或者老年代。收集器根据扮演不同角色的Region采用不同策略去处理。
Region中有一类特殊的Humongous区域,专门用来存储大对象。
G1认为一个对象的大小超过了一个Region空间的一半就认为该对象是大对象,如果超过了整个Region空间的超大对象将会被存放在连续的Humongous Region中,因此该区域一般会被作为老年代看待。
优点
1.建立可预测的停顿时间模型:
由于采用的是Region,因此回收单元作为Region即每次回收都是Region大小的整数倍。
G1会跟踪Region区域里面垃圾堆,计算出价值(回收所获得的空间大小以及回收所需时间的经验值),接着在后台维护一个优先级列表,根据用户设置的允许停顿时间来进行回收价值最大的Region区域。也就是“Garbage First”
的由来。
2.内存碎片
整体上采用的是标记-整理,但是在两个Region中实际上还是采用的复制算法,所以不会出现内存碎片问题。
缺点
1.浪费额外内存来维护收集器工作
跨代引用避免全堆扫描之前说过是采用记忆集的方式来解决,在G1中也一样。
每个Region中都有自己的记忆集但是在G1中每个Region除了需要记录别的Region指向自己的指针,还需要标记这些指针分别在哪些卡页范围内。其实本质上说是哈希表,Key是别的Region的起始地址,Value是一个集合存储的元素是卡表的索引号。
因此实现起来比原有的记忆集要复杂,而且Region的数量比之前的分代数量要多得多,所以记忆集的维护占用了更高的内存
G1至少要耗费大约相当于JAVA堆容量的10%到20%来存储维持收集器正常工作
2.不仅用到写后屏障还用到了写前屏障
上一小点中已经讲到维护卡表是需要进行添加写后屏障来完成更新卡表的操作的,但是G1还用到了写前屏障:由于使用的是原始快照来保证可以进行并发标记的基础,对比与增量更新来说虽然能够减少最终标记的停顿时间,但是相比于收集器,这款收集器不仅采用了写前屏障也采用了写后屏障导致最终的效率降低
工作流程
与之前不同的是最后一处,这个步骤需要进行更新Region的统计数据,对所有的Region的回收价值和成本进行排序,然后根据用户设定的期望停顿时间进行决定选择哪几个Region构成回收集,然后将一部分的Region中存活对象复制到另外一个空的Region空间中,随后进行清理掉整个旧的Region空间。是不是复制算法(针对与Region来说),因为涉及对象移动,所以需要暂停用户线程。
总结
到此,如果读者之前阅读过笔者之前的关于垃圾回收器讲解的文章,其实已经对现在大多数垃圾回收器机制和实现原理了解的差不多了,读者有兴趣可以自行去看Shenandoah收集器和ZGC收集器,本文不在叙述,主要确实文章内容优点太长了哈哈。
后面的文章将不在分析垃圾回收器的知识,但是还是会更新关于JVM的文章。