详解Java垃圾回收器

详解Java垃圾回收器

上文讲述了垃圾回收算法,本文介绍垃圾回收器,也就是垃圾回收算法的具体实现。

垃圾回收系统一般是基于分代收集策略,所以一个完整的垃圾回收系统一般是新生代垃圾收集器和老年代垃圾收集器搭配使用。唯一特别的是G1垃圾收集器,不仅可以对新生代垃圾进行回收,也可以对老年代垃圾进行回收。下图是各个新生代收集器和老年代收集器搭配使用的情况,下文将详细讲述各个新生代垃圾收集器和老年代垃圾收集器。

img

新生代垃圾回收器

Serial

Serial收集器是最古老,最稳定以及效率高的收集器,只使用一个线程对垃圾进行收集,收集过程中,需要进行Stop The World,即需要暂停用户线程,有可能造成长时间的停顿。详细过程如下如图。

ParNew

ParNew收集器本质是Serial收集器的多线程版本,除了使用多线程进行垃圾收集之外,其余行为和Serial收集器完全一致。在Server模式下,ParNew收集器是一个非常重要的新生代收集器,因为除Serial外,目前只有它能与CMS收集器配合工作;但在单个CPU环境中,不会比Serail收集器有更好的效果,因为存在线程交互开销。可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。

Parallel Scavenge

Parallel Scavenge垃圾收集器因为与吞吐量关系密切,也称为吞吐量收集器(Throughput Collector)。有一些特点与ParNew收集器相似:新生代收集器;采用复制算法;多线程收器集;CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间;Parallel Scavenge收集器的目标则是达一个可控制的吞吐量(Throughput),即减少垃圾收集时间,让用户代码获得更长的运行时间;

吞吐量=运行用户代码时间 /(运行用户代码时间+垃圾收集时间)

老年代垃圾回收器

Serial Old

Serial的老年版本,使用标记整理算法,主要有两个用处:1.早期与Parallel Scavenge搭配使用。2.作为CMS的后备预案,在并发收集发生Concurrent Model Failure时使用。后面讲述CMS的时候会提及。

Parallel Old

Parallel Scavenge的老年版本,同样也是使用标记整理算法。主要用来与Parallel Scavenge搭配使用

img

CMS收集器

该收集器是一种以获取最短回收停顿时间为目标的收集器,使用“标记-清除”算法实现,整个过程分为4个步骤

  • 初始标记(CMS initial mark):仅仅是标记一下GC Roots能直接关联到的对象,速度很快。
  • 并发标记(CMS concurrent mark):进行GC Roots的Tracing的过程.
  • 重新标记(CMS remark):为了修正在并发标记期间因用户程序继续运行而导致的标记产生变动的那一部分对象的标记记录。
  • 并发清除(CMS concurrent): 清除垃圾的过程。

初始标记阶段和重新标记阶段需要暂停所有的用户线程。

在CMS垃圾收集器工作时,需要借助年轻代来判断当前老年代中的对象是否是存活着的。如下图所示,无法在老年代中直接使用GC ROOT TRACING来判断老年代的对象的存活状态。为了找出并标记老年代存活的对象,需要扫描年轻代中的对象。由于年轻代中的对象较多,一般会采取先进行一次Minor GC使得年轻代的对象大幅度减少,也即会进行一次并发预清理阶段

老年代的机制与一个叫CARD TABLE的东西密不可分。CMS将老年代的空间分成大小为512bytes的块,card table中的每个元素对应着一个块。并发标记阶段会把引用发生变化的老年对象所在的Card标识为Dirty,后续重新标记阶段就只需要扫描这些Dirty Card的对象,从而避免扫描整个老年代。

举个例子:

  • 并发标记时对象的状态:

  • 但随后current obj的引用发生了变化:

    current obj所在的块被标记为dirty card.随后到了重新标记阶段,

    通过currrent obj变得可达的对象也被重新标记了,变成下面这样

CMS收集器有以下3个明显的缺点:

  • CMS收集器对CPU资源非常敏感:即在并发阶段,它虽然不会导致用户线程的停顿但是会因为占用了一部分线程(或者说CPU资源)而导致用户应用程序变慢,总吞吐量降低。CMS默认启动的回收线程数是(CPU数量+3)/4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。

  • CMS收集器无法处理浮动垃圾(Floating Garbage),由于CMS在并发清理阶段用户线程还在运行着,伴随着程序的运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留到下一次GC时再清理掉,这一部分垃圾就称为“浮动垃圾”。

    因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留有足够的内存空间给用户线程使用。若CMS运行期间预留的内存无法满足用户程序的需要,就会出现一次“Concurrent Mode Failure”失败。这时虚拟机会临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间会更长。

  • 由于CMS采用的是标记清除算法,因此意味着收集结束时会有大量空间碎片产生,碎片产生,需要进行内存碎片整理,而碎片整理过程无法并发,因此会增加用户线程的停顿时间。

G1收集器

将G1与CMS进行比较,G1是更好的解决方案。第一个区别是G1是压缩型收集器。G1的压缩功能,足以完全避免使用细粒度的空闲内存进行分配。这大大简化了收集器,并且消除了大部分的潜在碎片问题。此外,G1比CMS收集器提供可预测的垃圾回收暂停时间

  • Remembered Set

    G1被分为多个Region。G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序对Reference类型数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之间,如果是便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set中。当内存回收时,在GC根节点的枚举范围加入Remembered Set即可保证不对全局堆扫描也不会有遗漏。

  • G1工作原理概述

    G1收集器采取不同的方法。堆被分成一组大小相等的区域,每个是连续范围的虚拟内存。某些Regions被分配给和常规收集器一样的角色(eden区,survivor区,老年代),但他们没有固定的大小。这提供了更大的内存使用灵活性。

    G1 Heap Structure

    G1收集器与CMS收集器相比有两个显著的改进:一是G1收集器是基于标记整理算法实现的收集器,也就是说它不会产生空间碎片。二是它可以非常精确地控制停顿,既能让使用者明确指定一个长度为M毫秒的时间片段里,消耗在垃圾收集上的时间不得超过N毫秒。G1将整个Java堆(包括新生代、老年代)划分为多个大小固定的独立区域(Region),并且跟踪这些区域里面的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域(这就是GarbageFirst名称的来由)。区域划分及有优先级的区域回收,保证了G1收集器在有限的时间内可以获得最高的收集效率。

    回收过程与CMS类似也是分为四个阶段

    • 初始标记(Initial Marking):仅仅标记GC Roots能直接关联到的对象。
    • 并发标记(Concurrent Marking):从GC Roots开始对堆中对象进行可达性分析,找出存活的对象。
    • 最终标记(Final Marking):为了修正在并发标记期间因用户线程继续运行而导致标记产生变动的把一部分记录,虚拟机将这段时间对象变化记录在线程Remember Set Logs里面,最终标记阶段需要把Remember Set Logs的数据合并到Remember Set中,这阶段可以需要暂停用户线程,也可以进行并发。
    • 筛选回收(Live Data Counting and Evacuation):该阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。

参考文章

深入理解Java虚拟机

原创文章 218 获赞 1220 访问量 22万+

猜你喜欢

转载自blog.csdn.net/zycxnanwang/article/details/106033847