Java基础18——垃圾收集器

76 阅读11分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

简述Serial收集器:串行回收

单线程收集器,垃圾回收时会暂停STW)其他所有线程,简单高效(与其他收集器的单线程相比,无线程切换开销)

针对新生代使用标记复制算法。针对老年代提供Serial Old使用标记整理算法

简述ParNew收集器:并行回收

是Serial垃圾收集器的多线程版本

针对新生代使用标记复制算法。针对老年代提供ParNewOld使用标记整理算法

简述Parallel收集器:吞吐量优先

Parallel Scavenge垃圾收集器是以高吞吐量为目标多线程垃圾收集器,尽量减少垃圾收集时间,让用户代码获得更长的运行时间,有自适应调节策略,可以动态调整内存分配情况。

新生代使用标记复制算法。针对老年代提供Parallel Old,支持多线程并发收集,使用标记整理算法

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

简述CMS垃圾收集器

(Concurrent Mark Sweep,CMS)

最早提出的并发收集器垃圾收集线程与用户线程同时工作,关注用户线程的停顿时间采用标记清除算法.

整个过程分为4个大步骤:初始标记并发标记重新标记并发清除

(1)初始标记暂停虚拟机(第一次stop-the-world),标记GC roots直接关联的对象,还有被年轻代存活对象所引用的老年代对象

(2)并发标记进行可达性分析,从初始标记阶段找到的根对象出发,遍历所有的对象,标记存活对象

此阶段由于与用户线程并发执行,对象的状态可能会发生变化

  • 年轻代的对象晋升到老年代
  • 大对象被直接分配到老年代
  • Survivor区相同年龄的所有对象大小的总和大于 Survivor 的一半,年龄不小于该年龄的对象就可以直接进入老年代
  • 老年代和年轻代的对象引用关系变化,发生变化的老年代区域会标记为 “脏”区

(3)重新标记暂停虚拟机(第二次stop-the-world),是为了修正并发标记期间用户线程运行使对象引用状态发生变化的标记记录,重新扫新生代对象+GC Roots+被标记为“脏”区的对象

(4)并发清除并发清除未标记的垃圾对象。

缺点:对处理器资源非常敏感内存碎片问题无法处理浮动垃圾

remark过程标记活着的对象,从GCRoot的可达性判断对象活着,无法标记“死亡”的对象。如果在初始标记阶段被标记为活着,并发运行过程中“死亡”,remark过程无法纠正,因此变为浮动垃圾,需等待下次gc的到来。

简述G1垃圾收集器(Garbage First)

G1垃圾收集器是关注延迟吞吐量并发分代垃圾收集器,把堆划分成多个独立的Region区域,每个小空间可以单独进行垃圾回收

在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region

区分年轻代和老年代,年轻代包含EdenSurvivor,但不要求年轻代或者老年代是连续的,也不坚持固定的大小和固定的数量。

新添了一种Humongous区域,用于存储大对象,由于一个短期存在的大对象分配到老年代会对垃圾收集器造成负面影响,因此使用H区专门存放大对象。(一个H存不下则找连续的多个H存储)

G1中提供了三种垃圾回收模式,Young GCMixed GCFull GC,在不同的条件下被触发。

  • 应用程序分配对象Eden区内存不足,触发YGC,(survivor区满了不触发YGC);
  • YGC执行时,若整体使用的内存大于某个阈值(45%),则会启动并发标记;下一次垃圾回收执行Mixed GC
  • 老年代不足,YGC或者Mixed GC后内存还不足,此时将触发FGC

Remembered Set

  • 需要回收指定区域时,为了避免全局扫描,需要使用RSet。
  • 每个Region区设置一个RSet记录来自不同Region的引用
  • 垃圾回收时,将RSet记录的引用加入到GC Roots中作为根结点

YGC(标记复制算法) 对年轻代进行GC(并行的独占式收集器)

应用程序分配内存,当年轻代的Eden区用尽时开始年轻代回收过程;G1的年轻代收集阶段是一个并行的独占式收集器。在年轻代回收期,G1GC会暂停所有用户线程,启动多线程执行年轻代回收。然后从年轻代区间移动存活对象到survivor区间或者老年区间,也有可能是两个区间都会涉及。

(1)发生STW并创建**回收集,将Eden区Survivor区Region**都加入回收集。

(2)扫描根,将GC Roots的根引用RSet记录的引用作为扫描的起点

(3)更新RSet识别老年代中指向年轻代中的对象被指向的年轻代对象被认为是存活的对象

(4)遍历对象树复制对象,将Eden区存活的对象复制到Survivor区,Survivor区存活的对象年龄+1并且年龄达到阈值就被复制到Old区中。若Survivor区内存不够,则部分Eden区对象直接晋升老年代

(5)处理引用,处理软引用、弱引用、虚引用,清空Eden空间。

并发标记

当堆内存使用达到一定值(默认45%)时,开始老年代并发标记过程

初始标记、根区域扫描、并发标记、再次标记、独占清理、并发清理

Mixed GC(标记复制算法) 回收区域比YCG多一点

当越来越多的对象晋升到老年代Old Region时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器Mixed GC,会回收整个Young Region部分的Old Region

简述:标记完成马上开始混合回收过程。和年轻代不同,老年代的G1回收器和其他cc不同,G1的老年代回收器不需要整个老年代被回收,一次只需要扫描/回收-小部分老年代的Region就可以了。同时,这个老年代Region是和年轻代一起被回收的。(垃圾占内存分段比例越高的,越会被先回收

(1)并发标记结束以后,老年代中百分百为垃圾的内存分段被回收了部分为垃圾的内存分段被标记了出来。默认情况下,这些老年代的内存分段会分8次被回收。【也就是一个Region会被分为8个内存段】

(2)混合回收的回收集(Collection Set)包括八分之一的老年代内存分段,和年轻代都会回收。混合回收的算法和年轻代回收的算法完全一样,只是回收集多了老年代的内存分段。

(3)由于老年代中的内存分段默认分8次回收,G1会优先回收垃圾多的内存分段垃圾占内存分段比例越高的,越会被先回收,比例可设置一个阈值控制。如果垃圾占比太低,意味着存活的对象占比高,在复制的时候会花费更多的时间。

(4)混合回收并不一定要进行8次。有一个阈值-XX:G1HeapWastePercent,默认值为10%,意思是允许整个堆内存中有10%的空间被浪费,意味着如果发现可以回收的垃圾占堆内存的比例低于10%,则不再进行混合回收。因为GC会花费很多的时间但是回收到的内存却很少。

Full GC

新生代、老年代、方法区

(1)如果靠YGC和Mixed GC不能正常工作,则会启动独占式、单线程、高强度的Full GC

(2)G1的初衷就是要避免Full GC的出现,一旦发生Full GC需要对JVM参数进行调整

导致G1 Full GC的原因可能有两个:

(1)复制存活对象的时候没有足够的空内存来存放晋升的对象

(2)并发处理过程完成之前空间耗尽

G1回收过程二:并发标记过程

  1. 初始标记阶段:(STW)标记从根节点直接可达的对象。这个阶段是STW的,并且会触发一次年轻代GC。正是由于该阶段时STW的,所以我们只扫描根节点可达的对象,以节省时间。

  2. 根区域扫描:G1 GC扫描Survivor区直接可达的老年代区域对象,并标记被引用的对象。这一过程必须在Young GC之前完成,因为Young GC会使用复制算法对Survivor区进行GC。

  3. 并发标记(Concurrent Marking):

    1. 在整个堆中进行并发标记(和应用程序并发执行),此过程可能被Young GC中断。
    2. 在并发标记阶段,若发现区域对象中的所有对象都是垃圾,那这个区域会被立即回收。
    3. 同时,并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)。
  4. 再次标记(Remark):由于应用程序持续进行,需要修正上一次的标记结果。是STW的。G1中采用了比CMS更快的原始快照算法:Snapshot-At-The-Beginning(SATB)。

  5. 独占清理(cleanup,STW):计算各个区域的存活对象和GC回收比例,并进行排序,识别可以混合回收的区域。为下阶段做铺垫。是STW的。这个阶段并不会实际上去做垃圾的收集

  6. 并发清理阶段:识别并清理完全空闲的区域。

G1与CMS比较

G1垃圾回收器的改进是什么?相比于CMS突出的地方是什么?

回答:G1垃圾回收器抛弃了分代的概念,将堆内存划分为大小固定的几个独立区域,并维护一个优先级列表,在垃圾回收过程中根据系统允许的最长垃圾回收时间,优先回收垃圾最多的区域。(G1算法是可控STW的一种算法,GC收集器和我们GC调优的目标就是尽可能的减少STW的时间和次数。)

G1突出的地方

基于标记整理算法不产生垃圾碎片

可以精确的控制停顿时间,在不牺牲吞吐量的前提下实现短停顿垃圾回收

默认垃圾收集器

现在jdk默认使用的是哪种垃圾回收器?

回答:(被问到过好几次)

jdk1.7 默认垃圾收集器是新生代Parallel Scavenge+老年代Parallel Old

jdk1.8 默认垃圾收集器是新生代Parallel Scavenge+老年代Parallel Old

jdk1.9 默认垃圾收集器G1

垃圾收集器的选择

安全点与安全区

安全点

用户线程暂停,GC 线程要开始工作,但是要确保用户线程暂停的这行字节码指令是不会导致引用关系的变化。所以 JVM 会在字节码指令中,选一些指令,作为“安全点”,比如方法调用、循环跳转、异常跳转等,一般是这些指令才会产生安全点。

为什么它叫安全点,是这样的,GC 时要暂停业务线程,并不是抢占式中断(立马把业务线程中断)而是主动是中断。

主动式中断是设置一个标志,这个标志是中断标志,各业务线程在运行过程中会不停的主动去轮询这个标志,一旦发现中断标志为 True,就会在自己最近的“安全点”上主动中断挂起。

安全区域

为什么需要安全区域?

要是业务线程都不执行(业务线程处于 Sleep 或者是 Blocked 状态),那么程序就没办法进入安全点,对于这种情况,就必须引入安全区域。

安全区域是指能够确保在某一段代码片段之中, 引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的。我们也可以把安全区城看作被扩展拉伸了的安全点。

当用户线程执行到安全区域里面的代码时,首先会标识自己已经进入了安全区域,这段时间里 JVM 要发起 GC 就不必去管这个线程了。

当线程要离开安全区域时,它要 JVM 是否已经完成了(根节点枚举,或者其他 GC 中需要暂停用户线程的阶段)

1、如果完成了,那线程就当作没事发生过,继续执行。

2、否则它就必须一直等待, 直到收到可以离开安全区域的信号为止。