GC和GC算法

目录

判断对象是否为可回收的方法:可达性分析算法

生存还是死亡

垃圾收集

垃圾收集算法

垃圾收集器

按收集对象分类

按是否多线程分类

垃圾收集器详细说明

详解分代回收

内存分配的原则

Minor GC、MajorGC、Full GC特点及触发场景


无特殊说明:以下都是基于HotSpot虚拟机

判断对象是否为可回收的方法:可达性分析算法

从所有GC Roots对象开始作为起始点,向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots对象没有任何一条引用链存在时,证明此对象不可以用,此对象将被判定为可回收对象。

GC Roots对象包括:

  • 虚拟机栈中引用的对象,即栈帧中本地变量表里对象引用指向的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • Native方法引用的对象

生存还是死亡

通过可达性算法判断可回收的对象是否一定会被回收?

答案是否定的。

判断一个对象死亡并被回收要经过两次垃圾收集。

第一次垃圾收集:可达性分析算法中不可达的对象,会被第一次标记并且会判断需不需要执行对象的finalize()方法。有两种情况不会执行对象的finalize()方法,第一种是finalize()方法已经被调用过一次,第二种是对象类没有重写Object定义的finalize()方法。
如果一个对象被判断需要执行finalize()方法,那么该对象会被放置在一个队列中,并且稍后会由虚拟机创建一个低优先即的线程去执行这个方法,但并不代表这个方法一定会被执行结束。可能是这个方法还没有执行结束,对象就被回收了。

第二次垃圾收集:对象仍然是不可达的,那么该对象将被回收。

因此,重写finalize()方法,并在该方法里将自己(this关键字)赋值给某个类变量或者对象的成员变量,那么这个对象将不会在第二次垃圾收集时被回收。因此我们也说调用对象的finalize()方法并不代表对象一定会被回收。

垃圾收集

垃圾收集算法

标记-清除算法:就是通过可达性分析和两次标记判断对象是否可以死亡,最后在统一回收被标记的对象。
标记-清除算法,效率不高,并且会产生大量的不连续的内存碎片。空间碎片太多可能会导致以后程序运行过程中创建大对象时,无法找到连续的内存而不得不提前触发一次垃圾收集动作。

复制算法:将内存容量划分为大小相等的两块,每次只使用其中一块。当这一块用完了,就将还存活的对象复制到另外一块上面,然后再把已用过的内存空间一次清理掉。
这种方式简单高效,代价就是可用的内存缩小一半。

标记-整理算法:标记过程和标记清除算法的标记过程一样,不同的是,在标记后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,保证所有存活的对象占用连续的空间,然后直接清理掉最末尾存活对象之后的内存。

分代回收:对堆内存空间,进行划分,一般分为年轻代和老年代。在年轻代中每次垃圾收集都会发现有大批对象死去,只有少量存活,因此年轻代采用复制算法,只需要复出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高,数量多,占用的空间大,这是采用复制算法,显然从空间和时间上都是不合适的,因此老年代采用标记-清除或者标记-整理算法进行回收

分代回收为什么说一般分为年轻代和老年代,一般是因为垃圾收集器G1虽然也是基于分代回收的思想,保留年轻代和老年代的概念,但他并不是把堆物理隔离为年轻代和老年代,而是把整个堆分为多个大小相等的独立区域(Region)。

垃圾收集器

按收集对象分类

年轻代:Serial、ParNew、Parallel Scavenge 基于复制算法

老年代:Serial Old、Parallel Old 这两种基于标记-整理算法、CMS基于标记清理算法

Region(年轻代+老年代):G1,G1从整体上看基于标记-整理算法,从局部看基于复制算法,因此G1不会产生空间碎片。

按是否多线程分类

串行收集器:指一条垃圾收集线程进行垃圾收集,用户线程处于等待状态。Serial、Serial Old是分别用于年轻代和老年代的串行收集器

并行收集器:指多条垃圾收集线程并行工作,而用户线程仍然处于等待状态。ParNew、Parallel Scavenge适用于年轻代的并行收集器。Parallel Old是用于老年代的并行收集器。Parallel Scavenge是吞吐量优先的收集器,可以通过配置相关参数来影响吞吐量。

并发收集器:指用户线程与垃圾收集线程同时执行(不一定是同时也可能交替),用户线程在继续运行,垃圾收集线程也在运行。CMS是用于老年代的并发收集器。G1适用于整个堆空间的并发收集器。

串行和并行收集器都会在垃圾收集时造成户线程停顿。
并发收集器CMS虽然也会使用户停顿,但因为CMS在收集过程中存在并发过程允许用户线程与垃圾收集线程并发执行,所以相对来说停顿时间要小于串行和并行。

垃圾收集器详细说明

年轻代的三种:

Serial收集器:年轻代单线程收集器,基于复制算法,垃圾收集时会使用户线程等待。

ParaNew收集器:年轻代多线程并行收集器,基于复制算法,采用多线程进行并行垃圾收集,垃圾收集时会使用户线程等待。这种收集器会因为多线程而抢占CPU资源,因此当只有一个CPU的时候,可能性能还比不上Serial收集器。不过现在都是多核多线程CPU了。

Parallel Scavenge收集器:可以看做ParaNew的吞吐量版本,基于复制算法,它也是多线程并行收集器,不同于ParaNew,Parallel Scavenge收集器更关注吞吐量(吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间))。

老年代的三种:

Serial Old收集器:Serial收集器的老年代版本,基于标记-整理算法,采用多线程并行进行垃圾收集。垃圾收集时会使用户线程等待。

Parallel Old收集器:Parallel Scavenge收集器的老年代版本,基于标记-整理算法,采用多线程进行并行垃圾收集。垃圾收集时会使用户线程等待。

CMS并发收集器:用于垃圾收集时,降低用户线程等待时间的并发垃圾收集器。

CMS运作过程分为4步:初始标记==》并发标记==》重新标记==》并发清除

初始标记:单线程标记,会使用户线程等待。会发生STW(stop the world)。
并发标记:标记线程和用户线程同时执行。
重新标记:多条标记线程,回使用户线程等待。会发生STW(stop the world)。
并发清除:清除线程和用户线程同时执行。

CMS的缺点:

  1. 因为是并发的可能会和用户线程争抢CPU资源,造成吞吐量下降。
  2. 无法处理浮动垃圾,可能会造成“Concurrent Mode Failure”而触发Full GC的产生,由于并发清理阶段用户线程也在执行,因此这个时候会伴随程序的运行产生新的垃圾,这部分垃圾称为浮动垃圾,而这部分垃圾只有等待下一次GC再清理。
  3. CMS基于标记-清理算法,因此会产生空间碎片,当空间碎片过多,无法找到连续的空间分配大对象时,就会提前触发Full GC。对于CMS造成的空间碎片的问题,虚拟机提供了一个配置参数 -XX:CMSFullGCsBeforeCompaction,这个参数用于设置执行多少次不压缩的FullGC后,跟着来一次带压缩的(默认值为0,表示每次进入FullGC是都会进行碎片整理)。碎片整理会使整个Full GC的时间变长。

整个堆分为多个Region区(保留年轻代和老年代的思想):G1收集器(较为复杂后续补齐)

综上所有垃圾收集器的特点,在JVM配置中常常采用下图的垃圾收集器组合方式:

组合方式:Serial+CMS+SerialOld、Serial+Serial Old 、ParNew+CMS+Serial Old、ParNew+Serial Old、Parallel Scavenge+Serial Old、Parallel Scavenge+Parallel Old(这种是JVM默认的配置)

上面组合可以发现:使用CMS时会搭配Serial Old使用,是因为CMS会产生内存碎片,使用Serial Old作为老年代的备用垃圾收集器,Serial Old是基于标记-整理算法的,Serial Old作为CMS的备胎,帮助整理CMS产生的内存碎片。

Parallel Scavenge和CMS不能搭配使用,因为他们实现的架构不一样。

详解分代回收

Java 的堆内存被分代管理,为什么要分代管理呢?分代管理主要是为了方便垃圾回收,这样做基于2个事实,第一,大部分对象很快就不再使用;第二,还有一部分不会立即无用,但也不会持续很长时间。

虚拟机堆内存划分为年轻代、老年代、和永久代,如下图所示。

      

  • 年轻代主要用来存放新创建的对象,年轻代分为 Eden 区和两个 Survivor 区。大部分对象在 Eden 区中生成。当 Eden 区满时,还存活的对象会在两个 Survivor 区交替保存,达到一定次数(这个次数可以通过参数配置,默认15)的对象会晋升到老年代。

  • 老年代用来存放从年轻代晋升而来的,存活时间较长的对象。

  • 永久代,主要保存类信息等内容,这里的永久代是指对象划分方式,不是专指 1.7 的 PermGen,或者 1.8 之后的 Metaspace。

内存分配的原则

  1. 对象优先在新生代中Eden区分配,当Eden区没有空间,虚拟机将发起一次MinorGC
  2. 大对象直接进入老年代。大对象需要大量连续的内存空间。比如很长的字符串,或者很大的数组。
  3. 长期存活的对象将进入老年代。当对象在Eden区分配后,经历过第一次Minor GC年龄就增加一岁。当年龄超过15(15是默认值)后就会晋升到老年代中。对象晋升老年代的年龄阈值可以通过参数-XX:MaxTenuringThreshold设置
  4. 动态对象年龄判定。虚拟机并不是永远都要求对象的年龄要达到MaxTenuringThreshold设置的阈值才能晋升老年代,如果在Survivor空间中相同年龄的所有对象大小总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。
  5. 空间分配担保,JVM在发生MinorGC之前会检查老年代最大的可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则正常进行一次MinorGC,尽管有“风险”;如果小于,或者HandlePromotionFailure设置不允许空间分配担保,这时要进行一次FullGC,所以正常情况下为了避免频繁FullGC,参数HandlPromotionFailure都是开启的。
    上面说的“风险”:因为判断的是平均大小,有可能这次的晋升对象比平均值大很多;这个时候会导致FullGC。

Minor GC、MajorGC、Full GC特点及触发场景

Minor GC:清理年轻代,当Eden区空间不足时会触发Minor GC
Major GC:清理老年代,很多时候MajorGC是由于Full GC触发的,如果老年代采用CMS收集器,默认情况下老年代占用比例达到68%时会触发MajorGC
Full GC: 清理整个堆空间,即触发MinorGC清理年轻代,触发MajorGC清理老年代。
Full GC发生的场景,老年代空间不足,而老年代空间不足主要根据垃圾收集器有常见的几种情况:
1. 如果老年代采用Serial Old或者Parallel Old这两种基于标记-整理算法的收集器,那么当整理后的空闲空间不足以分配对象时将触发FullGC
2.如果老年代采用CMS这种基于标记-清除算法的收集器,那么可能发生:由于浮动垃圾造成老年代空间不足触发FullGC,或者由于空间碎片导致没有足够大的连续空闲空间分配对象时触发FullGC
3.空间分配担保失败,老年代最大连续可用的空间不足以分配晋升对象时会触发FullGC

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章