垃圾回收常用的算法与经典垃圾收集器

如何确定垃圾

.Java采用计数法和可达性分析来确定对象是否应该被回收

引用计数器

该方法就是简单的进行以下操作,有新的地方引用他,它的引用计数器就加一,引用他的地方少了一个引用计数器就减一,为零说明没有其他地方对他进行引用了,就可以回收了。
但是这个方法的可靠性不是很好,单纯的计数不能解决对象相互引用的问题。

可达性分析

这个方法的思想就是通过一些称为"GC Roots"的根对象作为开始的集合,然后从这个集合开始按照引用关系向下搜索,搜索时走过的路径称为"引用链",如果一个对象没有任何路径可以到达GC Root就说明此对象是不可达的,说明这个对象就不会再被引用了。

JAVA中常用的垃圾回收算法

. Java中常用的垃圾回收算法有标记清除(Mark-Sweep)、复制(Copying)、标记整理(Mark-Compact)和分代收集(Generational Collecting)4种垃圾回收算法。
垃圾回收常用的算法与经典垃圾收集器

标记清除算法

·是基础的垃圾回收算法,其过程分为标记和清除两个阶段。在标记阶段标记所有需要回收的对象,在清除阶段清除可回收的对象并释放其所占用的内存空间,如下图:
垃圾回收常用的算法与经典垃圾收集器

缺点:由于标记清除算法在清理对象所占用的内存空间后并没有重新整理可用的内存空间,因此如果内存中可被回收的小对象居多,则会引起内存碎片化的问题,继而引起大对象无法获得连续可用空间的问题。
还有就是回收的效率没办法保证,如果JAVA堆中有大量数据要回收,然后有大部分要回收,这时候就要进行大范围的标记清除,效率很难保证。

优点:速度比较快。

复制算法

·复制算法为了解决标记清除算法内存碎片化问题而设计的。
·它首先将内存划分为两块大小相等的内存区域,即区域1和区域2,新生成的对象都被存放在区域1中,在区域1内的对象存储满后会对区域1进行一次标记,并将标记后但仍然存活的对象全部复制到区域2中,这时区域1将不存在任何存活对象,直接清理整个区域1的内存即可。如下图:
垃圾回收常用的算法与经典垃圾收集器

·复制算法的内存清理效率高且易于实现,但由于同一时刻只有一块内存区域可用,即可用的内存空间被压缩到原来的一半,因此存在大量的内存浪费。同时,在系统中有大量长时间存活的对象时,这些对象将在内存区域l和内存区域2之间来回复制而影响系统的运行效率。
·因此,该算法只在对象为“朝生夕死”状态时运行效率最高。

标记整理算法

·该方法结合了标记清除算法和复制算法的优点,其标记阶段和标记清除算法的标记阶段相同,在标记完成后将存活的对象移到内存的另一端,然后清除该端的对象并释放内存,如下图:

垃圾回收常用的算法与经典垃圾收集器

HotSpot的算法细节体现

根节点枚举

迄今为止所有垃圾收集器在根节点枚举这个过程还是要进行暂停用户线程的,而且根节点的枚举还要再保证在一致性快照的前提下才能进行(一致性指的是,在某时刻时间好像静止了,根节点集合中的引用关系在这个时刻上不会改变了),如果一致性无法满足枚举的准确性就是无稽之谈了,这是导致任何垃圾回收过程都要停止用户进程的重要原因,即使那些号停顿时间可控的收集器也要在这步进行停顿。

在用户进程停止的时候并不需要检查全部的执行上下文和引用,虚拟机应当是有办法知道哪些地方放着引用的。在HotSpot中,叫OopMap的数据结构就可以达到上述目的,一旦类加载动作完成,HotSpot就会把对象内什么偏移量上是什么类型的数据算出来,也会在特定的位置记录下栈和寄存器里面哪些位置是引用,这样收集器在扫描的时候就可以直接知道了,并不需要一个不差的进行查找。

安全点

在OopMap的协助下,HotSpot已经可以很快速的完成根节点的枚举,但是问题也就来了,要是引用的变化过于多了,要是为每一个引用变化都匹配一个相应的OopMap的话显然占有的资源太多了。
上面已经说了只需要在“特定的位置”记录这些信息就行,这些位置称为安全点,有了安全点的概念的话,就说明用户程序在执行过程中并非在任何位置都可以停下来进行垃圾收集,只有到了安全点才可以停下来进行垃圾的收集,安全点的选择是以“是否有让程序长时间等待”这个标准来确定的,其中最明显的特征就是指令的复用,如方法调用,指令跳转等,所以说具备这样功能的指令才会产生安全点。

安全区域

安全点可以保证程序在不太长的时间段内就可以进入安全点进行垃圾收集,但是如果程序根本不执行呢,就是说该程序根本就没分到时间片,这样安全点就不能解决了,就要引入安全区域了。

安全区域可以看成是拉长版的安全点,安全区域是指在一段代码的区域中,引用关系不会发生改变,因此,在这个区域的任何地方进行垃圾收集都是没有任何问题的。

当用户线程进入安全区域的代码段的时候,它会标识自己进入了安全区域了,这样在执行垃圾回收的时候,就不用管这些已经声明在安全区域里面的线程了,当用户线程要离开的时候会先确认根节点枚举(或者垃圾回收过程中其他需要暂停用户线程的过程)是否完成,如果完成了就可以出去了,要是没完成就要等到收到可以离开安全区域的信号为止。

记忆集和卡表

为了解决跨代引用所带来的问题,垃圾回收器在新生代中建立了名为记忆集的数据结构来记录老年代指向新生代的引用,

写屏障

并行可达性分析

分代收集算法

·无论是标记清除法、复制算法还是标记整理算法,都无法对所有类型(长生命周期,短生命周期,大对象,小对象)的对象都进行垃圾回收。因此,针对不同的对象类型,JVM采用了不同的垃圾回收算法,该算法被称为分代收集算法。
·它根据对象的不同类型将内存划分为不同的区域,JVM将堆划分为新生代和老年代。
·新生代主要存放新生成的对象,其特点是对象数量众多但是生命周期短,在每次仅限垃圾回收器时都有大量的对象被回收;
·老年代主要存放大对象和生命周期长的对象,因此可回收的对象相对较少。因此,JVM根据不同的区域对象的特燕选择了不同的算法。
分代收集算法-新生代
·大部分的JVM针对新生代都采用了复制算法,因为在新生代中每次进行垃圾回收时都有大量的对象被回收,需要复制的对象(存活的对象)较少,不存在大量的对象在内存中被来回复制的问题,因此,采用复制算法能安全、高效低回收新生代大量的短生命周期的对象并释放内存。
JVM将新生代进一步划分为较大的Eden区和两块较小的Servivor区,而Servivor区又分为From区和To区。
.JVM进行垃圾回收时会将Eden区和ServivorFrom区中存活IDE对象复制到ServivorTo区,然后清理Eden区和
ServivorFrom区的内存空间。如下图:

垃圾回收常用的算法与经典垃圾收集器

分代收集算法-老年代

·老年代主要存放生命周期较长的对象和大对象,因而每次只有少量非存活的对象被回收,因而在老年代采用标记清除算法。
·在JVM中还有一个区域,即方法区的永久代,永久代用来存储Class对象、常量、方法描述等。在永久代主要回收废弃的常量和无用的类。

总结

JVM内存中的对象主要被分配到新生代获得Eden区和From区,在少数情况下会被直接分配到老年代。在新生代的
Eden区和From取得内存空间不足时会触发一次GC,该过程被称为MinorGC。
·在MinorGC后,在Eden区和From区中存活的对象或被复制到To区,然后Eden区和From区被清理。如果此时在To区无法找到连续的内存空间存储某个对象,则将这个对象直接存储到老年代。
·若Servivor区的对象经过一次GC后仍然存活,则其年龄加1
在默认的情况下,对象在年龄达到15时,将被无情地移到老年代。

垃圾收集器

.Java堆内存分为新生代和老年代;
·新生代主要存储短生命周期的对象,适合使用复制算法进行垃圾回收;
·老年代主要存储长生命周期的对象,适合使用标记整理算法进行垃圾回收;
·因此,JVM针对新生代和老年代分别提供了多种不同的垃圾收集器,针对新生代提供的垃圾收集器有Serial、ParNew、Parallet Scavenge;
·针对老年代提供的垃圾收集器有Serial Old、Parallel0ld、CMS,还有针对不同区域的G1分区收集算法。

垃圾回收常用的算法与经典垃圾收集器

Serial垃圾收集器:单线程,复制算法

·它是基于复制算法的实现,它是一个单线程收集器,在它正在进行垃圾收集时,必须暂停其他所有工作线程,直到垃圾收集结束。
.Serial垃圾收集器采用了复制算法,简单,高效,对于单CPU运行环境来说,没有线程交互开销,可以获得最高的单线程垃圾收集效率。
·因此Serial垃圾收集器是Java虚拟机运行在Client模式下的新生代的默认垃圾收集器。

ParNew垃圾收集器:多线程,复制算法

.ParNew垃圾收集器是Serial垃圾收集器的多线程实现,同样采用了复制算法,它采用多线程工作模式,除此之外和Serial收集器几乎一样。
.ParNew垃圾收集器在垃圾集过程中会暂停所有其他工作线程,是Java虚拟机运行在Server模式下的新生代的默认垃圾收集器。
. ParNew垃圾收集器默认开启与CPU同等数量的线程进行垃圾回收,在Java应用启动时可通过-XX:parallelGCThreads参数调节ParNew垃圾收集器的工作线程数。

Parallel Scavenge垃圾收集器:多线程,复制算法

·它是为提高新生代垃圾收集效率而设计的垃圾收集器,基于多线程复制算法实现,在系统吞吐量上有很大的优化,可以更高效地利用CPU尽快完成垃圾回收任务。
. Parallel Scavenge通过自适应调节策略提供系统的吞吐量,提供了三个参数用于调节、控制垃圾回收的停顿时间
及吞吐量,分别是:
·控制最大垃圾收集停顿时间的:—XX:MaxGCPauseMillis
·控制吞吐量大小的:—XX:GCTimeRatio
·控制自适应调节侧率开启与否的:UseAdaptiveSizePolicy参数。

Serial Old垃圾收集器:单线程,标记整理算法

·它是Serial垃圾收集器的老年代实现,同Serial一样采用单线程执行,不同的是,Serial Old针对老年代长生命周期的特点基于标记整理算法实现的。
·它是JVM运行在Client模式下的老年代的默认垃圾收集器。新生代的Serial垃圾收集器和老年代的Serial 0ld垃圾收集器可以搭配使用,分别针对JVM新生代和老年代进行垃圾回收,其垃圾收集讨程如下图:
垃圾回收常用的算法与经典垃圾收集器

Parallel Old垃圾收集器:多线程,标记整理算法

.Parallel Old垃圾收集器采用多线程并发进行垃圾回收,它根据老年代长生命周期的特点,基于多线程的标记整理算法实现。
. Parallel Old垃圾收集器在设计上优先考虑系统的吞吐量,其次考虑停顿时间等因素,如果系统对吞吐量的要求较高,则可以优先考虑新生代的Parallel Scavenge垃圾收集器和老年代的Parallel 0ld垃圾收集器的配合使用
垃圾回收常用的算法与经典垃圾收集器

CMS垃圾收集器

CMS(Current Mark Sweep)垃圾收集器是位老年代设计的垃圾收集器,其主要的目的是达到最短的垃圾回收停顿时间基于线程的标记清除算法实现,以便在多线程并发环境下以最短的垃圾收集停顿时间提高系统的稳定性。
·垃圾回收过程包含如下几个步骤:
.1.初始标记:只标记和GC Roots直接关联的对象,速度很快,需要暂停所有工作线程。
·2.并发标记:和用户线程一起工作,这个是从GC Roots直接相连的对象开始遍历整个对象图的过程,不需要暂停工作线程
3.重新标记:在并发标记过程中用户线程继续运行,导致在垃圾回收过程中部分对象的状态发生变化。为了确保这部分对象的状态正确性,需要对其重新标记并暂停工作线程。
·4.并发清除:和用户线程一起工作,执行清除GC Roots不可达对象的任务,不需要暂停工作线程。

.CMS垃圾收集器在和用户线程一起工作时(并发标记和并发清除)不需要暂停用户线程,有效缩短了垃圾回收时系统的停顿工作,同时由于CMS垃圾收集器和用户线程一起工作,因此其并行度和效率也有很大的提升,流程图如下:
垃圾回收常用的算法与经典垃圾收集器

但它并不是完美的,他有以下三个缺点:

首先,CMS对处理器资源非常敏感,在并发阶段虽然它可以跟用户进程一起并发,但也正因为此,他也会占用了CPU的资源使得,用户程序处理的速度变慢,影响用户的总吞吐量。

然后,由于CMS无法处理浮动垃圾,可能出现"Concurrent Mode Failure"失败导致的另一次完全的"Stop The World"的FULL GC产生。
在CMS的并发标记和并发清除阶段用户线程还是运行的,这个时候也会继续产生垃圾,这部分的垃圾是出现在标记之后才发生的,所以只能等到下一次垃圾回收时再进行处理,这部分垃圾就叫浮动垃圾。
由于并发标记和并发清除阶段用户线程还是运行的,所以要事先预留出足够的空间来给用户程序来用,如果此时的预留的空间不能满足用户程序的分配新对象的请求的话,就会出现"Concurrent Mode Failure",这个时候虚拟机不得不采用后背预案:冻结用户程序的执行,临时使用Serial Old来进行老年代垃圾的清理,这时时间就很长了。所以预留的空间要进行合适的选择。

最后一个缺点,CMS是由标记清除算法来实现的会有大量的空间碎片化出现。

G1垃圾收集器

G1(Garbage First)垃圾收集器为了避免全区域垃圾收集引起的系统停顿,将堆内存划分为大小固定的几个独立区域,独立使用这些区域的内存资源并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,在垃圾回收过程中根据系统允许的最长垃圾收集时间,优先回收垃圾最多的区域。

开创了收集器面向局部区域收集的设计思想和基于Region的内存收集模式。

服务对象:主要面向服务端的应用。

他可以面向堆中的任何一个部分来进行垃圾收集,衡量标准不再是处于哪一个分代,而是哪里有更多的垃圾,收集后的收益最大,这就是G1的Mixed GC

他与之前出现的垃圾收集器最明显的差异就是,他没有固定数量和大小的分代区域的固定的划分,二十把堆分成多个大小相等的独立的区域(Region),每个Region可以根据需要去扮演老年代和新生代等部分,对扮演不同角色的Region采用不同的收集策略,无论是对老对象,还是新对象,都有比较好的效果。

Region中还有一类特殊的Humongous区域专门用于存储大对象,G1认为大于Region一半的对象就叫大对象,如果对象超级大,G1则会将这个对象放在N个连续的Humongous中。

G1能够建立可预测的停顿时间的根本原因就是:

Region是它的回收的最小单元,而且更深层次的来说,G1可以跟踪各个Region里面的“垃圾收集后的价值的大小”,他会根据这个价值的大小来维护一个优先队列,优先处理里面价值较高的,这就是(Garbage First)的由来。

当然他也有一些问题:

譬如将堆分成多个Region后,里面会有很多跨Region的引用这个时候就要用到记忆集了,每个Region都会维护自己的记忆集,这些记忆集记录的是哪些别的区域指向我的,G1中的记忆集的机制会更加复杂,G1的记忆集在本质上就是一种哈希表,Key是别的Region的起始地址,Value是一个集合,存储的是卡表的索引号。这种“双向的”卡表会更加复杂(不仅要记录我指向谁,还要记录谁指向了我),所以内存占用必然少不了,G1会有比其他收集器更高的内存负担。

譬如,在并发阶段收集线程与用户线程怎么能互不干扰?CMS使用的是增量更新来解决的G1则是通过原始快照来实现的。此外在垃圾收集过程中肯定会有新的对象持续的在创建,G1为每个Region设计了两个名为TAMS(Top at Mark Start)的指针,把Region的空间划分一部分出来用于存放并发回收时新生成的对象,所有新分配的对象都要在这两个指针以上,G1默认这两个指针以上的是存活的,不列入回收范围。

譬如,如何建立起可靠的停顿模型呢? 用户可以使用-XX:MAXGCPauseMillis参数来指定停顿时间,G1如何来满足用户的需求呢?
可靠的停顿模型是基于衰减均值来实现的,G1会在垃圾收集时记录每个Region的回收耗时,标准偏差等,这里指的衰减均值指的是他会比普通的均值更容易受到新数据的影响,Region的统计状越新越能说明其回收的价值。通过这些信息就可以指定相应的回收计划从而更好地满足用户的需要。

G1的收集过程可以大致分为四部:

1.初始标记:仅仅是标记一下GC Roots能直接关联到的对象,并且相应的修改TAMS的值,让下一个阶段跟用户线程并发时,能正常的在Region中生成用户对象,需要停顿,但是时间很短,而且可以借用进行Minor GC的时候同步完成的,这个阶段实际上没有额外的停顿。

2.并发标记:从GC Roots开始对堆中的对象进行可达性分析,递归扫描整个堆中的对象图,找出要回收的对象,耗时较长,不过可以跟用户程序并发执行,扫描整个堆中的对象图之后还要重新处理一下SATB(原始快照)记录下并发时改变的对象。

3.最终标记:对用户线程进行另一个短暂的暂停,用于处理 并发阶段仍遗留下来的最后的那点少量的SATB(原始快照)记录。

4.筛选回收:负责更新Region中的统计数据,对各个Region中的回收价值和成本进行排序,根据用户提供的时间制定相应的计划,然后将最后活着的那部分Region对象复制到空的Region中,然后将旧的Region清空,这里涉及到对象的移动所以要暂停用户线程,由多条收集器线程完成的。

从上述阶段来看,G1除了并发标记外,其他过程也是需要暂停用户线程的,换言之他也不是纯粹的追求低延迟,官方给出它的标准时在延迟可控的情况下尽量增大吞吐量,才能担当起“全能收集器”的重任。

G1的运行示意图
垃圾回收常用的算法与经典垃圾收集器
由用户指定期望的停顿时间也是G1很重要的一个功能,停顿时间也不能随便设置,一两百毫秒或者二三百毫秒比较合理。

从G1开始所有的垃圾处理器并不追求一次性将JAVA堆清理干净,而是追求能够应付内存分配速率,这样用户进程跟收集程序一起执行,只要收集的速度能够跟得上对象分配的速度就可以了。

相比于CMS,G1的优点有很多,暂且不说可以指定最大的停顿时间,Region的内存布局,和按利益进行收集,但从传统的算法理论上,G1使用的标记整理,CMS使用的标记清除,G1收集后不会产生碎片化内存,G1也是更有优势的。

G1的弱势也很明显,就是它的内存占用比较大,在内存比较小的情况下G1的效果就明显不如CMS了。

就内存占用来说,CMS跟G1虽然都采用了卡表来处理跨代的引用问题,但是G1的卡表更为复杂,它不仅要存储谁指向了它,还要存储他指向了谁,而CMS的卡表只需要记录从老年代到新生代的引用即可。

从负载角度来讲,它们两个都使用了写屏障,CMS使用写后屏障来维护卡表,而G1不但为了更新卡表使用了写后屏障(由于G1的卡表更复杂这步操作的负载会更高),而且为了实现原始快照,还需要使用写前屏障来跟踪指针的变化。相比增量更新,原始快照能够减少并发标记和重新标记阶段的消耗,避免像CMS那样在最终标记阶段停顿时间过长的确定,但是在客户进程运行时跟踪它的变化状态确识会带来额外的消耗,由于CMS的写屏障比G1更简单,所以CMS写屏障实现的是直接同步操作,G1实现的是类似于消息队列的结构,将写前屏障还有写后屏障要做的事放到队列中异步执行。

强调文本 强调文本

加粗文本 加粗文本

标记文本

删除文本

引用文本

H2O is是液体。

210 运算结果是 1024.

插入链接与图片

导入

如果你想加载一篇你写过的.md文件,在上方工具栏可以选择导入功能进行对应扩展名的文件导入,
继续你的创作。