图解JVM垃圾回收算法

1 简单介绍下----->垃圾回收概念

GC中的垃圾,指的是存在于内存中的、不会再被使用的对象。而垃圾回收就是把那些不再被使用的对象进行清除,收回占用的内存空间。如果不及时对内存中的垃圾进行清理,那么这些垃圾对象所占的内存空间会一直保留到应用程序结束,被保留的空间无法被其他对象使用。如果大量不会被使用的对象一致占着空间不放,如果应用程序需要内存空间,没有多余的内存空间供其使用的话,就会导致内存溢出。因此,对内存空间的管理来说,识别和清理垃圾对象是至关重要的。

但是怎么识别一个对象是否存活??也就是可达的?依据什么策略来判断一个对象的可达性??

在java中使用根搜索算法(GC Roots Tracing)判断一个对象是否是可达的。算法的基本思路就是通过一系列的根节点"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有引用链相连时,则说明这个对象是不可达的。就会被判断为可被回收的对象。

一般什么样的对象可以作为 GCRoots呢?

在java中以下几种对象可以作为GCRoots:

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

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

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

4)本地方法栈中JNI(通常说的Native方法)引用的对象

 

2 重点图解介绍----->垃圾回收算法

 下面首先会先介绍算法的主要思想,然后会使用图解的方式直观算法的工作流程。。。。。。。。。

 重点掌握其算法思想,以及其算法的优缺点和适用场景。。。。

(1) 引用计数法

引用计数法是最经典的一种垃圾回收算法。其实现很简单,对于一个A对象,只要有任何一个对象引用了A,则A的引用计算器就加1,当引用失效时,引用计数器减1.只要A的引用计数器值为0,则对象A就不可能再被使用。

虽然其思想实现都很简单(为每一个对象配备一个整型的计数器),但是该算法却存在两个严重的问题

1)  无法处理循环引用的问题,因此在Java的垃圾回收器中,没有使用该算法

2)  引用计数器要求在每次因引用产生和消除的时候,需要伴随一个加法操作和减法操作,对系统性能会有一定的影响。

 

一个简单的循环引用问题描述:

对象A和对象B,对象A中含有对象B的引用,对象B中含有对象A的引用。此时对象A和B的引用计数器都不为0,但是系统中却不存在任何第三个对象引用A和B。也就是说A和B是应该被回收的垃圾对象,但由于垃圾对象间的互相引用使得垃圾回收器无法识别,从而引起内存泄漏(由于某种原因不能回收垃圾对象占用的内存空间)。

如下图:不可达对象出现循环引用,它的引用计数器不为0,

 

注意由于引用计数器算法存在循环引用以及性能的问题,java虚拟机并未使用此算法作为垃圾回收算法

【可达对象】:通过根对象的进行引用搜索,最终可以到达的对象。

【不可达对象】:通过根对象进行引用搜索,最终没有被引用到的对象。

 

(2)标记清除法

标记清除法是现代垃圾回收算法的思想基础

标记清除法将垃圾回收分为两个阶段:标记阶段和清除阶段。

在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象,因此未被标记的对象就是未被引用的垃圾对象然后在清除阶段,清除所有未被标记的对象。这种方法可以解决循环引用的问题,只有两个对象不可达,即使它们互相引用也无济于事。也是会被判定位不可达对象。

标记清除算法可能产生的最大的问题就是空间碎片

如下图所示,简单描述了使用标记清除法对一块连续的内存空间进行回收。

从根节点开始(在这里仅显示了两个根节点),所有的有引用关系的对象均被标记为存活对象(箭头表示引用)。从根节点起,不可达对象均为垃圾对象。在标记操作完成后,系统回收所有不可达对象。

 

从上图可以看出,回收后的内存空间不再连续在对象的对空间分配过程中,尤其是大对象的内存分配,不连续内存空间的工作效率要低于连续空间的,这也是该算法的缺点。

注意:标记清除算法先通过根节点标记所有可达对象,然后清除所有不可达对象,完成垃圾回收后面会讲到标记压缩算法,注意两者的区别。。。。。。


(3) 复制算法

算法思想将原有的内存空间分为两块相同的存储空间,每次只使用一块,在垃圾回收时,将正在使用的内存块中存活对象复制到未使用的那一块内存空间中,之后清除正在使用的内存块中的所有对象,完成垃圾回收。

如果系统中的垃圾对象很多,复制算法需要复制的存活对象就会相对较少(适用场景)。因此,在真正需要垃圾回收的时刻,复制算法的效率是很高的。而且,由于存活对象在垃圾回收过程中是一起被赋值到另一块内存空间中的,因此,可确保回收的内存空间是没有碎片的。(优点)

但是复制算法的代价是将系统内存空间折半,只使用一半空间,而且如果内存空间中垃圾对象少的话,复制对象也是很耗时的,因此,单纯的复制算法也是不可取的。(缺点)

 

图解算法回收流程:

A、B两块相同的内存空间(原有内存空间折半得到的两块相同大小内存空间AB),A在进行垃圾回收,将存活的对象复制到B中,B中的空间在复制后保持连续。完成复制后,清空A。并将空间B设置为当前使用内存空间。

 

在java中的新生代串行垃圾回收器中,使用了复制算法的思想,新生代分为eden空间、from空间和to空间3个部分,其中from和to空间可以看做用于复制的两块大小相同、可互换角色的内存空间块(同一时间只能有一个被当做当前内存空间使用,另一个在垃圾回收时才发挥作用),from和to空间也称为survivor空间,用于存放未被回收的对象。


新生代对象】:存放年轻对象的堆空间,年轻对象指刚刚创建,或者经历垃圾回收次数不多的对象。

老年代对象】:存放老年对象的堆空间。即为经历多次垃圾回收依然存活的对象。


     在垃圾回收时,eden空间中存活的对象会被复制到未使用的survivor空间中(图中的to),正在使用的survivor空间(图中的from)中的年轻对象也会被复制到to空间中(大对象或者老年对象会直接进入老年代,如果to空间已满,则对象也会进入老年代)。此时eden和from空间中剩余对象就是垃圾对象,直接清空,to空间则存放此次回收后存活下来的对象。

优点:这种复制算法保证了内存空间的连续性,又避免了大量的空间浪费。

注意:复制算法比较适用于新生代。因为在新生代中,垃圾对象通常会多于存活对象,算法的效果会比较好。

 

(4) 标记压缩算法

复制算法的高效性是建立在存活对象少、垃圾对象多的情况下,这种情况在新生代比较常见,

但是在老年代中,大部分对象都是存活的对象,如果还是有复制算法的话,成本会比较高。因此,基于老年代这种特性,应该使用其他的回收算法。


标记压缩算法是老年代的回收算法,它在标记清除算法的基础上做了优化。(回忆一下,标记清除算法的缺点,垃圾回收后内存空间不再连续,影响了内存空间的使用效率。。。)

和标记清除算法一样,标记压缩算法也首先从根节点开始,对所有可达的对象做一次标记,

但之后,它并不是简单的清理未标记的对象,而是将所有的存活对象压缩到内存空间的一端,之后,清理边界外所有的空间

这样做避免的碎片的产生,又不需要两块相同的内存空间,因此性价比高。

 

图解其算法工作过程:

通过根节点标记出所有的可达对象后,沿着虚线进行对象的移动,将所有的可达对象移到一端,并保持他们之间的引用关系,最后,清理边界外的空间。

 

标记压缩算法的最终效果等同于标记清除算法执行完成后,再进行一次内存碎片的整理,因此也称之为标记清除压缩算法。

 

(5) 分代算法

前面介绍的垃圾回收算法中,并没有一种算法可以完全替代其他算法,各自具有自己的特点和优势,因此需要根据垃圾对象的特性选择合适的垃圾回收算法。


分代算法思想:将内存空间根据对象的特点不同进行划分,选择合适的垃圾回收算法,以提高垃圾回收的效率

 

通常,java虚拟机会将所有的新建对象都放入称为新生代的内存空间。

新生代的特点是:对象朝生夕灭,大约90%的对象会很快回收,因此,新生代比较适合使用复制算法。

当一个对象经过几次垃圾回收后依然存活,对象就会放入老年代的内存空间,在老年代中,几乎所有的对象都是经过几次垃圾回收后依然得以存活的,因此,认为这些对象在一段时间内,甚至在程序的整个生命周期将是常驻内存的。

老年代的存活率是很高的,如果依然使用复制算法回收老年代,将需要复制大量的对象。这种做法是不可取的,根据分代的思想,对老年代的回收使用标记清除或者标记压缩算法可以提高垃圾回收效率。


注意:分代的思想被现有的虚拟机广泛使用,几乎所有的垃圾回收器都区分新生代和老年代

对于新生代和老年代来说,通常新生代回收的频率很高,但是每次回收的时间都很短,而老年代回收的频率比较低,但是被消耗很多的时间。为了支持高频率的新生代回收,虚拟机可能使用一种叫做卡表的数据结构,卡表为一个比特位集合,每一个比特位可以用来表示老年代的某一区域中的所有对象是否持有新生代对象的引用,

这样以来,新生代GC时,可以不用花大量时间扫描所有老年代对象,来确定每一个对象的引用关系,而可以先扫描卡表,只有当卡表的标记为1时,才需要扫描给定区域的老年代对象,而卡表为0的所在区域的老年代对象,一定不含有新生代对象的引用。


如下图表示:

卡表中每一位表示老年代4KB的空间,卡表记录为0的老年代区域没有任何对象指向新生代,只有卡表为1的区域才有对象包含新生代对象的引用,因此在新生代GC时,只需要扫面卡表为1所在的老年代空间,使用这种方式,可以大大加快新生代的回收速度。

 

(6) 分区算法

算法思想:分区算法将整个堆空间划分为连续的不同小区间,

如图所示:

每一个小区间都独立使用,独立回收。

算法优点是:可以控制一次回收多少个小区间

通常,相同的条件下,堆空间越大,一次GC所需的时间就越长,从而产生的停顿时间就越长。为了更好的控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理的回收若干个小区间,而不是整个堆空间,从而减少一个GC的停顿时间。

 


猜你喜欢

转载自blog.csdn.net/u012150590/article/details/80330876