Java虚拟机 垃圾回收机制(GC区域、垃圾回收算法、垃圾收集器)

本文主要总结自《深入理解Java虚拟机》一书。主要是对java虚拟机GC哪些内存区域以及如何回收等内容作出整理。

概述

C/C++中,内存由开发者主动申请,同时对象不用了还需要手动去释放内存,否则很容易造成内存泄漏。而Java开发者则无需这方面的关注,只有少数部分例如文件流、数据库操作等需要做相应的关闭操作即可,内存的申请和释放都由Java虚拟机来为我们完成。
垃圾收集主要需要完成三件事:
(1)哪些内存需要回收
(2)什么时候进行回收
(3)如何回收
本篇文章将基于此,主要从对象是否可回收、回收算法、垃圾收集器三个方面来分析JVM的垃圾回收机制。

GC回收的区域

在前面博客中介绍了java虚拟机的运行时数据区域,其中程序计数器、虚拟机栈、本地方法栈都是线程私有的,生命周期和线程是一样的,栈中的栈帧随着方法进入和退出进行入栈和出栈的操作。其栈帧分配多少内存在类结构确定下来就是已知的,因此次这几个区域的内存分配和回收都具备确定性,当方法结束或者线程结束,内存自然就随之回收。
而java堆和方法区则是线程共享的,且有着明显的不确定性。接口的不同实现类需要的内存,方法中的不同分支需要的内存都是动态分配的,需要在程序运行期间才能够知道哪些对象要创建,因此,垃圾收集关注的是这部分内存。

如何判断对象已死

在java世界里离不开类与对象实例,而堆中存放的就是运行时几乎所有的对象实例,而加载的类型信息、常量、静态变量则存放在方法区。这部分内容首先介绍在java堆中如何判断哪些对象存活,哪些对象死亡,即确定哪些对象需要被回收。而在后面会介绍方法区中如何确定废弃的类型。

java堆

java堆是虚拟机所管理的内存中最大的一块。是被所有线程共享的一块内存区域,在虚拟机启动时创建。所有的对象实例和数组都在堆上分配,它是垃圾收集器的管理的主要区域,需要考虑哪些对象内存需要被回收。判断方法主要是引用计数算法和可达性分析算法。

引用计数算法

引用计数算法就是给对象添加一个引用计数器,每当有一个地方引用该对象的时候就会+1,相反当失去一个引用的时候就-1,当引用数为0的时候也就说明这个对象不在被使用就可以被回收。
优点:实现简单,效率高。
缺点:循环引用问题(objA.instance=objB;objB.instance=objA)很难解决。

循环引用例子:


public class ReferenceCountingGC {
 
    public Object instance = null;
 
    public static void main(String []args){
        // 第一部分 
        ReferenceCountingGC objA = new ReferenceCountingGC();
        ReferenceCountingGC objB = new ReferenceCountingGC();
        // 第二部分
        objA.instance = objB;
        objB.instance = objA;
        // 第三部分
        objA = null;
        objB = null;
 
    }

如果使用引用计数算法来管理内存,判断内存是否可以回收,那么上面的例子即便为objA和objB制空,也是无法回收的,这是因为:
第一部分代码,开辟两块内存,对应的实例我们暂且命名为A和B,分别被objA和objB引用,因此A、B两个对象的引用计数器分别为1;
第二部分代码,由于又有各自的instance指向内存A和B,因此A、B两个对象的引用计数器分别为2;
第三部分代码,由于给objA和objB制空,因此A、B两个对象的引用计数器分别减1,最后A、B两个对象的引用计数器还是为1,不为0,因此不能回收。

可达性分析算法

这个算法的基本思想是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始根据引用关系向下搜索,搜索中所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则表明该对象不可达,即可以进行内存回收。
如下图,Object8、9、10是可以被回收的,因为他们到GC Roots是没有引用链的,是不可达的。
利用可达性分析算法判定对象是否可回收


在JAVA体系中,固定可作为GC Roots的对象包括以下几种:

(1)虚拟机栈(栈帧中的本地变量表)中引用的对象;

(2)方法区中类静态属性引用的对象;

(3)方法区中常量引用的对象;

(4)本地方法栈中JNI(即一般说的Native方法)引用的对象;

(5)Java虚拟机内部的引用,基本数据类型对应的Class对象,一些常驻的异常对象。

(6)所有被同步锁(synchronized关键字)持有的对象。

总结就是,方法运行时,方法中引用的对象;类的静态变量引用的对象;类中常量引用的对象;Native方法中引用的对象;jvm内部的基本数据类型的Class对象以及常驻异常对象;同步锁持有的对象。


但是,即使在可达性分析中不可达的对象,也并非“非死不可”,要确定一个对象死亡,至少要经历两次标记过程:

(1)可达性分析时发现对象没有与任何GC Roots相连的引用链连接,进行第一次标记,并进行筛选,看其是否有必要执行finalize()方法(标准是:对象没有覆盖finalize()方法或者finalize()方法已经被虚拟机调用过,则都是没有必要执行的)。
(2)如果有必要执行finalize()方法,对象加入F-Queue队列,并在稍后由一个虚拟机自动创建的、低优先级的Finalizer线程去执行它(执行是指触发finalize()方法,但是并不会等它执行完,以防止一个对象在finalize()方法中耗时太久或者死循环导致F-Queue队列中其他对象永久处于等待,甚至是导致整个内存回收系统崩溃).
(3)finalize()方法是对象自救的最后一次机会(且这个方法在运行过程中只会调用一次),稍后GC将对F-Queue队列中对象进行第二次小规模标记。如果此时,对象已经与其他对象进行关联(把自己赋值给别的对象),则会被移除出“即将回收”的集合,如果此时对象还没有自救成功,则基本上就被回收了。


java官方不推荐使用重写finalize()方法,因为它的运行代价高昂,不确定性大,无法保证对象的调用顺序。


java中的引用类型

前面提到对象的引用,关于引用的基本定义为:如果reference类型的数据中存储的数值代表的是另一块内存的起始地址,则称这块内存代表着某块内存、某个对象的引用。
在jdk1.2之后,又细分为:

  • 强引用:Object obj = new Object()这种就是强引用,只要强引用存在,GC就不会回收掉被引用的对象;
  • 软引用:SofReference类实现的引用,用来描述一些有用但并非必须的对象。在系统要发生内存溢出异常前,将会把 软引用对象列进回收范围进行第二次回收。如果第二次回收完还是没有足够内存,才抛出内存溢出异常。
  • 弱引用:用WeakReference实现的引用,用于描述非必需对象。弱引用对象只能生存到下一次GC前,当GC时,无论当前内存是否足够,都会回收掉弱引用关联的对象。
  • 虚引用:用PhantomReference实现的引用,也叫幽灵引用或幻影引用,无法使用虚引用来取得对象实例,只是为了能在该对象被GC时收到一个系统通知,虚引用关联的对象不会影响对象的生存时间,即不印象GC判定。

方法区(类回收条件)

Java虚拟机规范中不要求虚拟机在方法区实现GC,而且方法区中GC性价比较低:堆中,GC一般可回收70%-95%的空间,而永久代中效率远低于此。
方法区中GC主要包括两部分:

  • 废弃常量
    与堆中对象类似,当没有String对象引用常量池中的如“abc”这样的字符串时,且没有其他地方引用了这个字面量,如果有必要则“abc”常量会被系统清理出常量池。常量池中其他类(接口)、方法、字段的符号引用也是类似。
  • 无用的类
    同时满足以下三个条件才能算是“无用的类”:
    该类所有实例都已经被回收(堆中不存在该类实例);
    加载该类的ClassLoader已经被回收;
    该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

在大量使用反射、动态代理 、CGLib等ByteCode框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证方法区不会溢出。

垃圾收集算法

前面内容分析了如何确定要回收的对象内存,当我们通过可达性分析确定了要回收的内容后,下一步就是如何回收。
以下主要介绍四种垃圾收集算法:标记-清除算法,标记0复制算法,标记-整理算法,分代收集算法。

标记-清除算法

标记-清除(Mark-Sweep)算法算是最基础的垃圾收集算法,该算法主要分为“标记”和“清除”两个阶段。先标记可以被回收的对象,然后统一回所有被标记的对象,其中标记过程采用的就是可达性分析算法。


算法缺点:
效率问题,标记和清除两个过程效率都不高;
空间问题,标记清除后产生大量不连续的内存碎片,碎片过多可能会导致程序运行过程中需要分配较大对象时,无法找到足够的连续内存,而不得不提前触发另一次垃圾收集动作。

标记-清除算法

标记-复制算法

为了解决效率问题及标记清除算法的缺点,一种称为“复制”(Copying)的收集算法出现了。

它将可用内存按容量划分为大小相等的两块,每次只使用其中一块,当这一块用完了,触发GC操作将存活的对象复制到另一个区域当中,然后再把使用过的内存空间一次清理掉。这样使得每次都对整个半区进行内存回收,内存分配也不用考虑内存碎片的问题,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。


算法缺点:可用内存缩小了原来的一半,空间浪费太多了。

标记-复制算法
现在的商业虚拟机都采用这种收集算法来回收新生代,IBM公司的专门研究表明,新生代中的对象98%是“朝生夕灭”的,所以并不需要按照 1:1 的比例来划分内存空间,而是将内存分为一块较大Eden空间和两块较小Survivor空间,每次使用Eden和其中一块Survivor。

  • 当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。
  • HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存会被“浪费”。
  • 当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。

标记-整理算法

复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。

根据老年代的特点,提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。
标记-整理算法
注:标记-整理算法需要移动存活对象,每一次移动存活对象并更新所有引用都需要全程暂停用户程序的运行。

分代收集算法

前商业虚拟机的垃圾收集都采用“分代收集”(Generational Collection)算法,这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。
一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或者“标记-整理”算法来进行回收。
在Java虚拟机中,对象的生命周期有长有短,大部分对象的生命周期很短,只有少部分的对象才会在内存中存留较长时间,因此可以依据对象生命周期的长短将它们放在不同的区域。在采用分代收集算法的Java虚拟机堆中,一般分为三个区域,用来分别储存这三类对象:

  • 新生代 - 刚创建的对象,在代码运行时一般都会持续不断地创建新的对象,这些新创建的对象有很多是局部变量,很快就会变成垃圾对象。这些对象被放在一块称为新生代的内存区域。新生代的特点是垃圾对象多,存活对象少。

  • 老年代 - 一些对象很早被创建了,经历了多次GC也没有被回收,而是一直存活下来。这些对象被放在一块称为老年代的区域。老年代的特点是存活对象多,垃圾对象少。

  • 永久代 - 一些伴随虚拟机生命周期永久存在的对象,比如一些静态对象,常量等。这些对象被放在一块称为永久代的区域。永久代的特点是这些对象一般不需要垃圾回收,会在虚拟机运行过程中一直存活。(在Java1.7之前,方法区中存储的是永久代对象,Java1.7方法区的永久代对象移到了堆中,而在Java1.8永久代已经从堆中移除了,这块内存给了元空间。)

在这里插入图片描述


分代收集算法也就根据新生代和老年代来进行垃圾回收的。

对于新生代区域,每次GC都会有很多垃圾对象被回收,只有少量存活。因此采用复制回收算法,GC时把剩余很少的存活对象复制过去即可。

在新生代区域中,并不是按照1:1的比例来进行复制回收,而是按照8:1:1的比例分为了Eden、SurvivorA(From)、Survivor(To)三个区域。其中Eden意为伊甸园,形容有很多新生对象在里面创建;Survivor区则为幸存者,即经历GC后仍然存活下来的对象。

Eden区对外提供堆内存。当Eden区快要满了,则进行Minor GC(新生代GC),把存活对象放入SurvivorA区,清空Eden区;

Eden区被清空后,继续对外提供堆内存;

当Eden区再次被填满,此时对Eden区和SurvivorA区同时进行Minor GC(新生代GC),把存活对象放入SurvivorB区,此时同时清空Eden区和SurvivorA区;

Eden区继续对外提供堆内存,并重复上述过程,即在 Eden 区填满后,把Eden区和某个Survivor区的存活对象放到另一个Survivor区;

当某个Survivor区被填满,且仍有对象未被复制完毕时(空间担保),或者某些对象在反复Survive 15次左右时,则把这部分剩余对象放到老年代区域(大对象直接进入老年区);当老年区也被填满时,进行Major GC(老年代GC),对老年代区域进行垃圾回收。

老年代区域对象一般存活周期较长,每次GC时,存活的对象比较多,没有额外空间对它进行分配担保,就必须使用“标记-清理”或者“标记-整理”算法来进行回收。

什么时候进行回收

在介绍了垃圾回收的目标,以及如何进行回收的各种算法后,那么虚拟机一般是什么时候进行回收的呢?
总结来说,垃圾回收一般发生在:

1、会在cpu空闲的时候自动进行回收
2、在堆内存存储满了之后
3、主动调用System.gc()后尝试进行回收

具体哪块内存空间什么时候进行回收,在学习了之前博客中内存分代划分,以及上面的分代算法以后,可以知道垃圾回收将内存分为三个区域年轻代、老年代、永久代(jdk1.8之后为元空间,实际上就是方法区):
java堆从内存回收角度划分


(1)一个对象实例化时 先去看Eden(伊甸园/生成区)有没有足够的空间:

  • 如果Eden(伊甸园/生成区)有足够空间不进行垃圾回收 ,对象直接在Eden(伊甸园/生成区)存储。
  • 如果Eden(伊甸园/生成区)内存已满,会进行一次Minor Gc。

(2)然后再进行判断Eden(伊甸园/生成区)中的内存是否足够,如果足够就是直接在Eden(伊甸园/生成区)存储; 如果不足则去看Survior(存活区)的内存是否足够:

  • 如果Survior(存活区)内存足够,把Eden(伊甸园/生成区)部分活跃对象保存在Survior(存活区),然后把对象保存在Eden(伊甸园/生成区)。
  • 如果Survior(存活区)内存不足,向OldGen(老年代)发送请求。

(3)查询老年代的内存是否足够:

  • 如果OldGen(老年代)内存足够,将部分Survior(存活区)的活跃对象存入老年代.然后把Eden(伊甸园/生成区)的活跃对象放入Survior(存活区),对象依旧保存在Eden(伊甸园/生成区)。
  • 如果OldGen(老年代)内存不足,会进行一次Full Gc,之后OldGen(老年代)会再进行判断内存是否足够,如果足够同上;
    如果不足 会抛出OutOfMemoryError.

JAVA垃圾回收发生的情况

经典的垃圾收集器

如果说算法是内存回收的方法论,那么垃圾收集器就是内存回收的实践者。事实上Java虚拟机规范对垃圾收集器应该如何实现,并没有任何的规定,所以不同的厂商、不同版本的虚拟机所提供的垃圾收集器都会有所不同,并且一般都会提供参数供用户根据自己的应用特点和要求组合出各个年代所使用的收集器。
各款经典的垃圾收集器关系如图:
各个垃圾收集器的关系
由上图我们可以总结出几个结论:

(1)新生代垃圾收集器:Serial、ParNew、Parallel Scavenge;

  • 老年代垃圾收集器:Serial Old(MSC)、Parallel Old、CMS;

  • 整堆垃圾收集器:G1


(2)垃圾收集器之间的连线表示可以搭配使用,有如下几种组合:

Serial/Serial Old、Serial/CMSParNew/Serial Old、ParNew/CMS、Parallel Scavenge/Serial Old、Parallel Scavenge/Parallel Old、G1;
 JDK 9 以后以上的黄色部分组合,这些组合的支持已经被取消了
 
 注:其中Serial Old作为CMS出现“Concurrent Mode Failure”失败后的后备预案


(3)串行收集器Serial:Serial、Serial Old

  • 并行收集器 Parallel:Parallel Scavenge、Parallel Old

  • 并发收集器:CMS、G1

(并行描述的是多条垃圾收集线程之间的关系,同一时间有多条这样的线程协同工作,通常默认此时用户线程处于等待阶段;并发描述的是垃圾收集线程和用户线程之间的关系,说明同一时间垃圾收集器线程和用户线程都在运行。)


(4)使用的垃圾收集算法

  • 复制算法:Serial、ParNew、Parallel Scavenge、G1

  • 标记-清除:CMS

  • 标记-整理:Serial Old、Parallel Old、G1


Serial收集器

这是一个最基本,历史最悠久的垃圾收集器,是JDK1.3之前新生代唯一的垃圾收集器。
该收集器有如下特点:
①作用于新生代
  由上图也可看出,这是一个新生代垃圾收集器,采用的垃圾回收算法是复制算法。
  
②单线程
  工作时只会使用一个CPU或者一条收集线程去完成工作。
  
③进行垃圾收集时,必须暂停所有工作线程
  也就是说使用Serial收集器进行垃圾回收时,别的工作线程都暂停,系统这时候会有卡顿现象产生。
  
④适用场景
  Serial 收集器由于没有线程交互的开销,对于限定单个CPU的环境,可以获得最高的单线程收集效率。一般在用户的桌面场景中,分配给虚拟机管理的内存一般来说不会很大,收集几十兆或一两百兆的新生代,定顿时间可以控制在几十毫秒,只要不是频繁发生的,这点停顿是可以接受的。


所以 Serial 收集器对于运行在 Client 模式下的虚拟机是一种很好的选择。

ParNew收集器

这个收集器其实就是Serial收集器的多线程版本。
也就是说其特点除了多线程,其余和Serial收集器一样,事实上,这两个收集器实现上也共用了很多代码。
①作用于新生代
  一个新生代垃圾收集器,采用的垃圾回收算法是复制算法。
  
②多线程
 弥补了Serial收集器单线程的缺陷。
 
③适用场景
由于其多线程的特性,是大多数运行在 Server 模式下的虚拟机首选新生代垃圾收集器。


另外需要说明的是,能够与下面将要介绍的划时代垃圾收集器CMS(Concurrent Mark Sweep)配合使用,也是一个重要原因。

Parallel Scavenge收集器

前面介绍的垃圾收集器关注点是尽可能缩小垃圾收集时的用户线程停顿时间。而 Parallel Scanvenge 收集器是为了达到一个可控制的吞吐量。
 吞吐量 = 运行用户代码的时间 / (运行用户代码的时间+垃圾收集时间)
 可以用下面两个参数进行精确控制:
 -XX:MaxGCPauseMills 设置最大垃圾收集停顿时间
 -XX:GCTimeRatio 设置吞吐量大小
 
①作用于新生代
 一个新生代垃圾收集器,采用的垃圾回收算法是复制算法。
 
②多线程
  并行的多线程垃圾收集器。
  
③吞吐量
  这个收集器可以精确控制吞吐量。
  
④适用场景
  设置垃圾收集停顿时间短适合需要与用户快速交互的程序;
  而设置高吞吐量可以最高效的利用CPU效率,尽快的完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

Serial Old收集器

Serial Old 收集器是 Serial 收集器的老年代版本,特点如下:

①作用于老年代
 
②单线程
 
③使用标记-整理算法
 
④进行垃圾收集时,必须暂停所有工作线程

Parallel Old收集器

Parallel Old 是 Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。
 
①作用于老年代
 
②多线程
 
③使用标记-整理算法
 
除了具有以上几个特点,比较关键的是能和新生代收集器 Parallel Scavenge 配置使用,获得吞吐量最大化的效果。

CMS(Concurrent Mark Sweep)收集器

CMS,全称为 Concurrent Mark Sweep ,顾名思义并发的,采用标记-清除算法。另外也将这个收集器称为并发低延迟收集器(Concurrent Low Pause Collector)


这是一款跨时代的垃圾收集器,真正做到了垃圾收集线程与用户线程(基本上)同时工作。和 Serial 收集器的 Stop The World(妈妈打扫房间的时候,你不能再将垃圾丢到地上) 相比,真正做到了妈妈一边打扫房间,你一边丢垃圾。
 
①作用于老年代
 
②多线程
 
③使用标记-清除算法


整个算法过程分为如下 4 步:
 
  一、初始标记(CMS initial mark):只是仅仅标记GC Root 能够直接关联的对象,速度很快,但是需要“Stop The World”  
 
  二、并发标记(CMS concurrent mark):从GC Roots的直接关联对象开始遍历整个对象图的过程,简单来说就是遍历初始标记阶段标记出来的存活对象,然后继续递归标记这些对象可达的对象,耗时长,但是可以并发执行。
 
  三、重新标记(CMS Remark):修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录(采用增量更新),需要“Stop The World”。这个时间一般比初始标记长,但是远比并发标记时间短。
 
  四、并发清除(CMS concurrent sweep):对上一步标记的对象进行清除操作,由于不需要移动存活对象,所以可以并发执行。
 
  由于整个过程最耗时的操作是第二(并发标记)、四步(并发清除),而这两步垃圾收集器线程是可以和用户线程一起工作的。所以整体来说,CMS垃圾收集和用户线程是一起并发的执行的。
 
  缺点:
 
①对CPU资源敏感
  因为在并发阶段,会占用一部分CPU资源,从而导致应用程序变慢,总吞吐量会降低。
 
②产生浮动垃圾
  由于CMS并发清理阶段用户线程还在工作,这个时候产生的垃圾,CMS无法在本次收集中处理掉它们,只能留在下一次GC时再将其处理掉,这部分垃圾称为“浮动垃圾”。
 
③产生内存垃圾碎片
  因为采用的算法是标记-清除,很明显,会有空间碎片产生。

Garbage First(G1)收集器

这是当前收集器技术发展的最前沿的成果。可以实现在基本不牺牲吞吐量的前提下完成低停顿的内存回收,首发于JDK8中,是JDK9默认的垃圾回收器。

这是因为它并不像前面介绍的所有垃圾收集器是区分新生代,老年代的,它作用于全区域。将整个Java堆划分为多个大小固定的独立区域(Regin),G1跟踪各个Region里的垃圾堆积价值大小(所获得空间大小以及回收所需时间),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region,从而保证了再有限时间内获得更高的收集效率。


G1算法将堆划分为若干个区域(Region),它仍然属于分代收集器。

  • 不过,这些区域的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。
  • 老年代也分成很多区域,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有CMS内存碎片问题的存在了。
      G1收集器Region分区图
      注:其中Humongous专门用来存储大对象。G1认为只要超过一个Region容量的一半的对象即为大对象。

整个算法过程分为如下 4 步:
 
  一、初始标记(Initial Mark):只是仅仅标记GC Root 能够直接关联的对象,这个过程需要停顿线程,但是耗时很短,而且是借用Minor GC的时候同步完成。(所以G1收集器实际在这个阶段没有额外停顿)  
 
  二、并发标记(Concurrent Mark):从GC Roots的直接关联对象开始遍历整个对象图的过程,简单来说就是遍历初始标记阶段标记出来的存活对象,然后继续递归标记这些对象可达的对象,耗时长,但是可以并发执行。
 
  三、最终标记(Final Mark):修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录(采用原始快照),需要“Stop The World”(停顿用户线程)。且停顿时间比初始标记稍长,但远比并发标记短;采用多线程并行执行来提升效率;
 
  四、筛选回收(Live Data Counting and Evacuation):对各个Region回收价值和成本进行排序,根据用户所期望的GC暂停时间来执行回收。(会将决定回收的Region中的存活对象复制到空的Region中,再清理掉整个旧的Region全部空间,因为涉及存活对象的移动,所以需要暂停用户线程。)


G1收集器的优点:
 
1、并行与并发:G1能充分的利用多CPU、多核的环境使用多个CPU来缩短停顿的时间,也就是说同样拥有和用户线程同时执行的功能。

2、分代收集:与CMS不同,G1可以不需要其他收集器的配合就能独立管理整个Java堆,它是一个整堆垃圾回收期。虽然还保留了新生代和老年代的概念,但是新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。采用了不同的方式去处理新建对象和存活了一短时间的对象,这样效果更佳。

3、空间整理:与CMS的标记清理算法不同,G1从整体来看是基于标记整理算法实现的,从局部两个Region上来看是基于复制算法,但是不管哪种算法都不会产生内存碎片的问题。

4、可预测的停顿时间:这是G1比CMS的另一优势,降低停顿时间是CMS和G1的共同关注点,但是G1出了追求低停顿外,还可以预测停顿的时间,让使用者明确指定一个长度为毫秒的时间,消耗在垃圾收集的时间不超过这个时间。


补充知识:

G1收集器不再是完全的将堆划分新生代和老年代,取而代之的是将堆划分为多个大小的相等的独立区域(Region),虽然还保留了新生代和老年代的概念,但是新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。

G1可以预测停顿时间是因为它可以有计划的避免对整个Java堆进行全区域的垃圾收集,G1跟踪各个Region里面的垃圾价值情况,也就是回收后会获得的空间大小和回收所需要多少时间的经验,在后台维护一个优先列表,每次根据允许的时间去判断回收哪个区域后获得的价值更大,这样使用Region和优先级的方式回收,可以保证G1在有限的时间内获得最高的收集价值。

因为一个对象被分配到一个Region中,但是并非只能本Region中的其他对象才能引用,而是可以被整个Java堆中的任意对象所产生引用关系,那么为了避免进行全局的扫描,G1收集器在每个Region中都维护了一个Remembered Set(用来记录跨Region引用的数据结构,在分代中就是记录夸新生代和老年代)。如果虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,然后检查Reference引用的对象是否处于不同的Region之中(在分代中就是检查老年代和新生代的夸代引用),如果是就会通过CardTable(可以理解为是Remembered Set的一种实现)把相关引用的信息记录到被引用对象所属的Region的Rememered Set之中。当进行内存回收时,在GC跟节点的范围加入对Remembered Set中的对象分析,这样就不用为了查找引用而进行全堆的搜索了。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值