垃圾回收——》垃圾对象判断,回收算法,垃圾收集器

1.如何判断对象为垃圾对象

1.引用计数法

	定义:在对象中添加一个引用计数器,当有地方引用这个对象的时候,此对象的引用计数器值就+1,当引用失效的时候
	,此对象的引用计数器值就-1。

	打印垃圾回收信息添加在idea中添加以下参数即可打印:
	
	-XX:+PrintGC (与 -verbose:gc 是相同作用)
	-XX:+PrintGCDetails (日志输出具体详情)
	-XX:+PrintGCDateStamps(日志输出的时间戳)
	-Xloggc:C:\gc.log (日志输出的位置)
	
	主动进行垃圾回收:System.gc(); 调用这个方法

代码实例如下:
public class TestGC {

    private Object instance;

    public TestGC(){
        byte[] bytes = new byte[20 * 1024 * 1024];//20M空间占用
    }
    @Test
    public void test01(){
        TestGC testGC1 = new TestGC();
        TestGC testGC2 = new TestGC();
        testGC1.instance = testGC2;
        testGC2.instance = testGC1;
        testGC1 = null;
        testGC2 = null;
        System.gc();//主动调用垃圾回收方法
    }
}

日志输出如下:

Java HotSpot(TM) 64-Bit Server VM (25.181-b13) for windows-amd64 JRE (1.8.0_181-b13), built on Jul  7 2018 04:01:33
by "java_re" with MS VC++ 10.0 (VS2010)

Memory: 4k page, physical 16643720k(5203544k free), swap 33285580k(21612560k free)

CommandLine flags: -XX:InitialHeapSize=266299520 -XX:MaxHeapSize=4260792320 -XX:+PrintGC -XX:+PrintGCDateStamps 
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseCompressedClassPointers 
-XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC 

2019-05-08T14:01:32.831+0800: 3.081: [GC (Allocation Failure) [PSYoungGen: 49195K->3105K(75776K)] 
49195K->3105K(249344K), 0.0464946 secs] [Times: user=0.03 sys=0.00, real=0.05 secs] 

2019-05-08T14:01:32.885+0800: 3.136: [GC (System.gc()) [PSYoungGen: 47264K->1848K(140800K)] 
47264K->1848K(314368K), 0.0379813 secs] [Times: user=0.00 sys=0.00, real=0.04 secs] 

2019-05-08T14:01:32.924+0800: 3.174: [Full GC (System.gc()) [PSYoungGen: 1848K->0K(140800K)] 
[ParOldGen: 0K->1785K(173568K)] 1848K->1785K(314368K), [Metaspace: 8440K->8440K(1056768K)], 0.3702027 secs]
 [Times: user=0.30 sys=0.00, real=0.37 secs] 

Heap
 PSYoungGen      total 140800K, used 4670K [0x000000076b580000, 0x0000000774980000, 0x00000007c0000000)
  eden space 130048K, 3% used [0x000000076b580000,0x000000076ba0faa8,0x0000000773480000)
  from space 10752K, 0% used [0x0000000773f00000,0x0000000773f00000,0x0000000774980000)
  to   space 10752K, 0% used [0x0000000773480000,0x0000000773480000,0x0000000773f00000)
 ParOldGen       total 173568K, used 1785K [0x00000006c2000000, 0x00000006cc980000, 0x000000076b580000)
  object space 173568K, 1% used [0x00000006c2000000,0x00000006c21be7f8,0x00000006cc980000)
 Metaspace       used 8673K, capacity 9554K, committed 9856K, reserved 1058816K
  class space    used 1104K, capacity 1289K, committed 1408K, reserved 1048576K

配置打印垃圾回收参数如下:参数之间空格隔开
在这里插入图片描述

2.可达性分析

    定义:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(
    Reference Chain),
当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是
不可用的.

作为GCRoots 的对象:虚拟机栈,方法区的类属性所引用的对象,方法区中常量所引用的对象,本地方法栈中引用的对象


2.如何回收垃圾对象

1.回收策略(算法)
1.1 标记清除算法

定义:算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象

1.1标记清除算法图示如下:
在这里插入图片描述

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


1.2 复制算法(新生代收集器)

定义:它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象
复制到另外一块上面  然后再把已使用过的内存空间一次清理掉

1.2 复制算法图示如下:
在这里插入图片描述

问题:利用空间换取效率空间大大浪费了。

改良后的的复制算法的收集图示:

将内存划分为三部分 各占 10% 10% 80%的区域,创建对象时,在Eden区域进行内存划分,如果Eden也占用完成,
继续向其中一个Survivor区 域占用,然后调用垃圾回收算法进行垃圾回收,因为我们知道在程序运行完成之后,
只用极少数的对象能够存活下来,这时将存活对象复制到未 占用的另外一个survivor区域中,如果存活的对象空间
超过survivor区域,则需要额外的申请内存担保来存储对象,然后将其他两个区域全部 清除。这样就可以最大限度
使用内存!再次分配对象时,再使用Eden区以及刚刚被 拷贝存活对象的那个Survivor区进行分配,再清除时,
再将存活拷贝到另外一个survivor区,如果Survivor区域有存活对象,可以将此对象 拷贝到Tenured Gen (老年区)

在这里插入图片描述


1.3 标记-整理算法

定义:标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向
一端移动,然后直接清 理掉端边界以外的内存

1.3 标记-整理算法图示:
在这里插入图片描述


1.4 分代收集算法

定义:根据对象存活周期的不同将内存划分为几块。 一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的
特点采用最适当的收集 算法。 在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,
只需要付出少量存活对象的复制成本就可以完成收集。 而老年代中因为对象存活率高、 没有额外空间对它进行分配担保,
就必须使用“标记—清理”或者“标记—整理”算法来进行回收。


2.常见垃圾收集器
2.1 Serial收集器

1.客户端垃圾收集器
2.最基本,发展最悠久
3.单线程垃圾收集器

垃圾收集器回收图示:
在这里插入图片描述
Serial垃圾收集器工作流程:

这个收集器是一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾
收集工作,更重要的是  在  它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。 “Stop The World”
这个名字也许听起来很酷,但这项工作实际上是由虚拟机在后台自动发起和自动完成的,在用户不可见的情况下把用户
正常工作的线程全部停掉,这对很多应用来说都是难以接受的。

 读者不妨试想一下,要是你的计算机每运行一个小时就会暂停响应5分钟,你会有什么样的心情。


实际上到现在为止,它依然是虚拟机运行在Client模式下的默认新生代收集器。
它也有着优于其他收集器的地方:简单而高效(与其他收集器的单线程比),对于限定单个
CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最
高的单线程收集效率。 在用户的桌面应用场景中,分配给虚拟机管理的内存一般来说不会很
大,收集几十兆甚至一两百兆的新生代(仅仅是新生代使用的内存,桌面应用基本上不会再
大了),停顿时间完全可以控制在几十毫秒最多一百多毫秒以内,只要不是频繁发生,这点
停顿是可以接受的。 所以,Serial收集器对于运行在Client模式下的虚拟机来说是一个很好的
选择。
2.2 Parnew收集器

定义:ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括
Serial收集器 可用的所有控制 参数(例如:-XX:SurvivorRatio、 -XX: PretenureSizeThreshold、 -XX:
HandlePromotionFailure等)、 收集算法、 Stop The World、 对 象分配规则、 回收策略等都与Serial收集器
完全一样,在实现上,这两种收集器也共用了相 当多的代码

ParNew收集器的工作过程如图:
在这里插入图片描述

    ParNew收集器除了多线程收集之外,其他与Serial收集器相比并没有太多创新之处,但它却是许多运行在Server模式下的虚拟机中首选的
新生代收集器,其中有一个与性能无关但很重要的原因是,除了Serial收集器外,目前只有它能与CMS收集器配合工作。 在JDK 1.5时
期,HotSpot推出了一款在强交互应用中几乎可认为有划时代意义的垃圾收集器——CMS收集器(Concurrent Mark Sweep,本节稍后将详细介绍
这款收集器),这款收集器是HotSpot虚拟机中第一款真正意义上的并发(Concurrent)收集器,它第一次实现了让垃圾收集线程与
用户线程(基本上)同时工作,用前面那个例子的话来说,就是做到了在你的妈妈打扫房间的时候你还能一边往地上扔纸屑。

	不幸的是,CMS作为老年代的收集器,却无法与JDK 1.4.0中已经存在的新生代收集器Parallel Scavenge配合工作,所以在JDK 1.5中
使用CMS来收集老年代的时候,新生代只能选择ParNew或者Serial收集器中的一个。 ParNew收集器也是使用-XX:+UseConcMarkSweepGC
选项后的默认新生代收集器,也可以使用-XX:+UseParNewGC选项来强制指定它。

		ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术
实现的两个CPU的环境中都不能百分之百地保证可以超越Serial收集器。 当然,随着可以使用的CPU的数量的增加,它对于GC时系统资源
的有效利用还是很有好处的。 它默认开启的收集线程数与CPU的数量相同,在CPU非常多(譬如32个,现在CPU动辄就4核加超线程,服务器超过
32个逻辑CPU的情况越来越多了)的环境下,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。

		注意 从ParNew收集器开始,后面还会接触到几款并发和并行的收集器。 在大家可能产生疑惑之前,有必要先解释两个名词:
并发和并行。 这两个名词都是并发编程中的概念,在谈论垃圾收集器的上下文语境中,它们可以解释如下。

●并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
●并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序
运行于另一个CPU上。

[1]Parallel Scavenge收集器及后面提到的G1收集器都没有使用传统的GC收集器代码框架,而另外独立实现,其余几种收集器则共用了部分
的框架代码,


2.3 Parallel Scavenge收集器

定义:Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器……看上去和ParNew都一样
,那它有什么特别之处呢?

Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,
而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。 所谓吞吐量就是CPU用于运行用户代码的时间与CPU
总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟
,那吞吐量就是99%。

停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高、吞吐量则可以高效率地利用CPU时间,尽快完成程序的
运算任务,主要适合在后台运算而不需要太多交互的任务。

Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接
设置吞吐量大小的-XX:GCTimeRatio参数。

MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,收集器将尽可能地保证内存回收花费的时间不超过设定值。 不过大家不要认为如果
把这个参数的值设置得稍小一点就能使得系统的垃圾收集速度变得更快,GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的:系统把新生代
调小一些,收集300MB新生代肯定比收集500MB快吧,这也直接导致垃圾收集发生得更频繁一些,原来10秒收集一次、 每次停顿100毫秒,
现在变成5秒收集一次、 每次停顿70毫秒。 停顿时间的确在下降,但吞吐量也降下来了。

GCTimeRatio参数的值应当是一个大于0且小于100的整数,也就是垃圾收集时间占总时间的比率,相当于是吞吐量的倒数。 如果把此参数
设置为19,那允许的最大GC时间就占总时间的5%(即1/(1+19)),默认值为99,就是允许最大1%(即1/(1+99))的垃圾收集时间。



2.4 Serial Old收集器

定义:Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法。 这个收集器的主要意义也是在于给
Client模式下的虚拟机使用.如果在Server模式下,那么它主要还有两大用途:一种用途是在JDK 1.5以及之前的版本中与Parallel Scavenge
收集器搭配使用[1],另一种用途就是作为CMS收集器的后备预案,在并发收集发生ConcurrentMode Failure时使用。 这两点都将在后面的内
容中详细讲解。 


Serial Old收集器的工作过程如图:
在这里插入图片描述

需要说明一下,Parallel Scavenge收集器架构中本身有PS MarkSweep收集器来进行老年代 收集,并非直接使用了Serial Old收集器,
但是这个PS MarkSweep收集器与Serial Old的实现非常接近,所以在官方的许多资料中都是直接以Serial Old代替PS MarkSweep进行讲解,
这里笔者也采用这种方式。


2.5 Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器是在JDK 1.6中才开始提供的,
在此之前,新生代的Parallel Scavenge收集器一直处于比较尴尬的状态。 原因是,如果新生代选择了Parallel Scavenge收集器,
老年代除了Serial Old(PS MarkSweep)收集器外别无选择(还记得上面说过Parallel Scavenge收集器无法与CMS收集器配合工作吗?)。
由于老年代Serial Old收集器在服务端应用性能上的“拖累”,使用了Parallel Scavenge收集器也未必能在整体应用上获得吞吐量最大化的效
果,由于单线程的老年代收集中无法充分利用服务器多CPU的处理能力,在老年代很大而且硬件比较高级的环境中,这种组合的吞吐量
甚至还不一定有ParNew加CMS的组合“给力”。


直到Parallel Old收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的应用组合,在注重吞吐量以及CPU资源敏感的场合,
都可以优先考虑Parallel Scavenge加Parallel Old收集器


Parallel Old收集器的工作过程如图

在这里插入图片描述

2.6 CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S
系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。 CMS收集器就非常符合这类应用
的需求

从名字(包含“Mark Sweep”)上就可以看出,CMS收集器是基于“标记—清除”算法实现
的,它的运作过程相对于前面几种收集器来说更复杂一些,整个过程分为4个步骤,包括:

初始标记(CMS initial mark)
并发标记(CMS concurrent mark)
重新标记(CMS remark)
并发清除(CMS concurrent sweep)

其中,初始标记、 重新标记这两个步骤仍然需要“Stop The World”。 初始标记仅仅只是
标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC RootsTracing
的过程,而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变
动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远
比并发标记的时间短。

由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起
工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。 通
过图3-10可以比较清楚地看到CMS收集器的运作步骤中并发和需要停顿的时间


Concurrent Mark Sweep收集器运行示意图
在这里插入图片描述

CMS是一款优秀的收集器,它的主要优点在名字上已经体现出来了:并发收集、 低停
顿,Sun公司的一些官方文档中也称之为并发低停顿收集器(Concurrent Low Pause
Collector)。 但是CMS还远达不到完美的程度,它有以下3个明显的缺点:

CMS收集器对CPU资源非常敏感。 其实,面向并发设计的程序都对CPU资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,
但是会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。 CMS默认启动的回收线程数是(CPU数量
+3)/4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。
 但是当CPU不足4个(譬如2个)时,CMS对用户程序的影响就可能变得很大,如果本来CPU负载就比较大,还分出一半的运算能力
 去执行收集器线程,就可能导致用户程序的执行速度忽然降低了50%,其实也让人无法接受。 为了应付这种情况,
虚拟机提供了一种称为“增量式并发收集器”(Incremental Concurrent Mark Sweep/i-CMS)的CMS收集器变种,
所做的事情和单CPU年代PC机操作系统使用抢占式来模拟多任务机制的思想一样,就是在并发标记、 清理的时候让GC线程、 
用户线程交替运行,尽量减少GC线程的独占资源的时间,这样整个垃圾收集的过程会更长,但对用户程序的影响就会
显得少一些,也就是速度下降没有那么明显。 实践证明,增量时的CMS收集器效果很一般,在目前版本中,i-CMS已经被声明为“deprecated”
即不再提倡用户使用。


CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent ModeFailure”失败而导致另一次Full GC的产生。 由于CMS
并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法
在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。 也是由于在垃圾收集阶段用户线程还需要运行,
那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进
行收集,需要预留一部分空间提供并发收集时的程序运作使用。 在JDK 1.5的默认设置下,CMS收集器当老年代使用了68%的空间后就会被
激活,这是一个偏保守的设置,如果在应用中老年代增长不是太快,可以适当调高参数-XX:CMSInitiatingOccupancyFraction的值来
提高触发百分比,以便降低内存回收次数从而获取更好的性能,在JDK 1.6中,CMS收集器的启动阈值已经提升至92%。 要是CMS运行期间
预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来
重新进行老年代的垃圾收集,这样停顿时间就很长了。 所以说参数-XX:CMSInitiatingOccupancyFraction设置得太高很容易导致
大量“Concurrent Mode Failure”失败,性能反而降低。


还有最后一个缺点,在开头说过,CMS是一款基于“标记—清除”算法实现的收集
器,如果读者对前面这种算法介绍还有印象的话,就可能想到这意味着收集结束时会有大量
空间碎片产生。 空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有
很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full
GC。 为了解决这个问题,CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection开
关参数(默认就是开启的),用于在CMS收集器顶不住要进行FullGC时开启内存碎片的合并
整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间不得不变长。
虚拟机设计者还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction,这个参数是用于
设置执行多少次不压缩的Full GC后,跟着来一次带压缩的(默认值为0,表示每次进入Full
GC时都进行碎片整理)


2.7 G1收集器

G1(Garbage-First)收集器是当今收集器技术发展的最前沿成果之一,早在JDK 1.7刚刚确立项目目标,Sun公司给出的JDK 1.7 
RoadMap里面,它就被视为JDK 1.7中HotSpot虚拟机的一个重要进化特征。 从JDK 6u14中开始就有Early Access版本的G1收集器供
开发人员实验、 试用,由此开始G1收集器的“Experimental”状态持续了数年时间,直至JDK 7u4,Sun公司才认为它达到足够成熟的
商用程度,移除了“Experimental”的标识。

G1是一款面向服务端应用的垃圾收集器。 HotSpot开发团队赋予它的使命是(在比较长期的)未来可以替换掉JDK 1.5中发布的CMS收集器。 
与其他GC收集器相比,G1具备如下特点:

1.并行与并发:G1能充分利用多CPU、 多核环境下的硬件优势,使用多个CPU(CPU或者
CPU核心)来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿Java线程执行的
GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。

2.分代收集:与其他收集器一样,分代概念在G1中依然得以保留。 虽然G1可以不需要其
他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已
经存活了一段时间、 熬过多次GC的旧对象以获取更好的收集效果。

3.空间整合:与CMS的“标记—清理”算法不同,G1从整体来看是基于“标记—整理”算法实
现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,但无论如何,这
两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。 这种
特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一
次GC。

4.可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关
注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一
个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实
时Java(RTSJ)的垃圾收集器的特征了。



在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这
样。 使用G1收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java堆划分
为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和
老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。
G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java
堆中进行全区域的垃圾收集。 G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的
空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时
间,优先回收价值最大的Region(这也就是Garbage-First名称的来由)。 这种使用Region划分
内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高
的收集效率。


G1把内存“化整为零”的思路,理解起来似乎很容易,但其中的实现细节却远远没有想象
中那样简单,否则也不会从2004年Sun实验室发表第一篇G1的论文开始直到今天(将近10年
时间)才开发出G1的商用版。 笔者以一个细节为例:把Java堆分为多个Region后,垃圾收集是否就真的能以Region为单位进行了?听起来顺理成章,再仔细想想就很容易发现问题所
在:Region不可能是孤立的。 一个对象分配在某个Region中,它并非只能被本Region中的其
他对象引用,而是可以与整个Java堆任意的对象发生引用关系。 那在做可达性判定确定对象
是否存活的时候,岂不是还得扫描整个Java堆才能保证准确性?这个问题其实并非在G1中才
有,只是在G1中更加突出而已。 在以前的分代收集中,新生代的规模一般都比老年代要小许
多,新生代的收集也比老年代要频繁许多,那回收新生代中的对象时也面临相同的问题,如
果回收新生代时也不得不同时扫描老年代的话,那么Minor GC的效率可能下降不少。


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


如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为以下几个步骤:

初始标记(Initial Marking)
并发标记(Concurrent Marking)
最终标记(Final Marking)
筛选回收(Live Data Counting and Evacuation)

对CMS收集器运作过程熟悉的读者,一定已经发现G1的前几个步骤的运作过程和CMS
有很多相似之处。 初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改
TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的
Region中创建新对象,这阶段需要停顿线程,但耗时很短。 并发标记阶段是从GC Root开始
对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执
行。 而最终标记阶段则是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动
的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最
终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线
程,但是可并行执行。 最后在筛选回收阶段首先对各个Region的回收价值和成本进行排序,
根据用户所期望的GC停顿时间来制定回收计划,从Sun公司透露出来的信息来看,这个阶段
其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控
制的,而且停顿用户线程将大幅提高收集效率。


G1收集器运行示意图
在这里插入图片描述

*** 垃圾收集器总结如下图所示:***
在这里插入图片描述

3.何时回收

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值