亲宝软件园·资讯

展开

JVM垃圾收集算法 JVM知识总结之垃圾收集算法

AlaGeek 人气:0
想了解JVM知识总结之垃圾收集算法的相关内容吗,AlaGeek在本文为您仔细讲解JVM垃圾收集算法的相关知识和一些Code实例,欢迎阅读和指正,我们先划重点:JVM垃圾收集算法,JVM垃圾回收,下面大家一起来学习吧。

一、什么是垃圾

本文要讲的是垃圾收集算法,那么首先要确定的问题就是什么是垃圾,也就是哪些对象是要被回收的,对此有两种判断方式:

1.1 引用计数算法

什么样的对象是要被回收的,很明显,没有被引用的对象才要被回收。因此在对象中加一个引用计数器,当有一个对象引用该对象的时候,计数器就加一,当引用结束后,计数器就减一,当计数器为0的时候,对象就可以被回收了。

1.1.1 优点

1.1.2 缺点

1.2 可达性分析算法

因为引用计数算法的缺陷,各大主流的商用程序语言都采用可达性分析算法来判断对象是否需要被回收。可达性顾名思义,是指对象跟对象之间有引用关系,此处有两种引用关系:

要判断一个对象是否可达,首先要有一个根对象,在Java中有一系列被称为“GC Roots”的根对象作为起始节点集,任何从“GC Roots”不可达的对象都是需要被垃圾收集器回收的垃圾。

1.2.1 优点

可以有效解决引用计数算法的相互循环引用问题

 二、什么是引用

在讨论什么是垃圾的时候,多次提到引用一词,那么什么是引用呢?

2.1 JDK1.2以前

按照书中的说法,在JDK1.2以前,引用的意思是:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称该reference数据是代表某块内存、某个对象的引用。
在这种定义下可以发现,对于一个对象来说,就只有未被引用和被引用两种状态了,但其实可以发现,在实际应用中,并不是一定要把对象回收掉的,书中有个词就很贴切,“食之无味,弃之可惜”,我们想要的是当内存空间足够的时候,把这部分本该回收的对象留着不回收,当内存不够的时候,就将其回收。

2.2 JDK1.2之后

因此在JDK1.2之后,引用的概念就扩张到了以下四种:

三、垃圾判断全流程

按照书中所述,我画了个流程图,如下:

垃圾判断全流程

一个对象在被回收前,需要进行两次标记,第一次进行可达性分析后,对象被垃圾收集器认为是垃圾,则对对象进行第一次标记,然后垃圾收集器会给予对象一次自救的机会,不然就没必要两次标记了,一次标记直接回收就好了。
我们都知道对象有个finalize()方法,自救的机会就在这个方法中,当第一次标记后,垃圾收集器会对对象做一次筛选,筛选条件是要不要执行对象的finalize()方法,如果开发者未对finalize()方法进行覆写或者虚拟机已经执行过该对象的finalize()方法了,那么自然就不用再执行了,反之则需要执行。
将筛选出来的需要执行finalize()方法的对象放入一个特定的队列中,由虚拟机统一执行,如果finalize()方法中使得对象被别的对象引用了,导致可达性分析认为对象是可用的,那么自救就成功了。
根据筛选的条件可以知道,对象的自救机会在整个程序中只有一次,因为finalize()方法只会被执行一次。
需要注意的是,官方明确申明不推荐使用finalize()方法,因为使用它的不确定性太大。对于资源清理等操作,try…catch语法可以做的更好。

四、垃圾收集算法

大多数虚拟机的垃圾收集都采用了分代收集的形式,这是因为三条经验法则:

因为对象的生存周期是不一样的,所以我们不能对所有对象采用同一种垃圾收集算法,采用分代收集,将有共性的对象放在一个集合里,会大大地提高垃圾收集效率。
按照上述经验法则,可以将堆内存分为两代:

下面分别介绍三种垃圾收集算法:

4.1 标记-清除算法

标记-清除算法是最基础的垃圾收集算法,顾名思义,标记就是判断对象是否是垃圾,也就是前面第四节讲到的内容,清除就是统一回收垃圾,该算法有两种执行过程:

标记-清除算法示意图:

标记-清除算法

标记-清除算法有两大缺点:

4.2 标记-复制算法

基于标记-清除算法的缺点,标记-复制算法将内存空间一分为二,两块内存空间等大,每次只使用其中一块内存空间,当这一块内存空间用完了,就把存活的对象复制到另一块内存空间中,然后一次性清理所有已使用的内存空间。

标记-复制算法示意图:

标记-复制算法

标记-复制算法解决了标记-清除算法面对大量可回收对象场景下的不足之处,面对这种情况,标记-复制算法只需要将内存空间中的存活对象复制到另一半内存空间中,可以有效解决内存碎片的问题,在给对象分配内存的时候,只需要移动堆顶指针按顺序分配即可,不过这个算法也有缺点:

4.3 标记-整理算法

标记-复制算法不足以应对有大量存活对象的场景,因此就有了标记-整理算法,该算法的执行流程如下:

标记-整理算法示意图:

标记-整理算法

标记-整理算法同样可以解决内存碎片化问题,并且不会造成空间浪费,不过它也有缺点:

在大量对象存活的情况下,移动对象并更新引用也会花费大量时间

4.4 应用

不同的场景适用不同的垃圾收集算法,像标记-复制算法就适用于存活对象少的情况下,也就是新生代区域,像标记-整理算法就适用于存活对象多的情况下,也就是老年代。
这里有点需要注意的是,标记-整理算法对于老年代来说也不是完美的,在5.3节我们说过,在大量对象存活的情况下,移动对象和更新引用也是要花费大量时间的,不过算法这个东西吧,它比的是谁更适合,对于标记-复制算法来说,我把区域一分为二,如果大量对象存活,我要把对象全部复制到另一块内存区域,这个开销不见得比标记-整理算法少,并且它还有个缺点就是可用内存一下子少了一半,这个问题在标记-整理算法中是没有的。也有的虚拟机采用标记-清除算法标记-整理算法协作的垃圾收集方案,没有最适合,只有更适合

4.5 优化

前面讲标记-复制算法的时候说到要把内存区域等半分,这是在没有规定场景的情况下,在新生代中采用该垃圾收集算法可以做更好的优化。

众所周知,新生代中的对象都是朝生夕死的,因此当标记完成后的存活对象肯定是少量的,根据这个现象,可以将内存区域非等半分,比如说9:1的分法,这里我们将90%的内存区域称为Eden空间,将10%的内存区域称为Survivor空间,一开始使用Eden空间的内存,当垃圾收集时,将Eden空间的存活对象复制到Survivor空间中。
这里肯定有人要问了,那下一次使用Survivor空间不是就只有10%的内存了吗?

对的,所以这里有两种解决方案:

第二种方法就是大名鼎鼎的半区复制分代策略,现在叫Appel式回收,因为提出这个策略的人叫Apple,目前很多虚拟机在新生代的垃圾收集算法中采用这个策略。

4.5.1 缺点

半区复制分代策略也是有缺点的,从上面的叙述中我们可以知道,Eden空间Survivor空间的内存占比为8:1:1,如果当垃圾收集后的存活对象所需要的内存空间大于一个Survivor空间时,那就难办了。

4.5.2 补丁

既然Survivor空间的内存不够放存活对象了,那就去借内存区域,这个借当然不能跟Eden空间Survivor空间借,不然会影响到整个算法,增加算法的复杂度。新生代不能借,那就跟老年代借,这里就有一个所谓的内存分配担保,放不下的存活对象将直接通过分配担保机制进入到老年代中。有了这个“逃生门”一样的设计,这个策略才算是没有漏洞。

五、写在后面

几个垃圾收集算法的图是我直接截了书里面的图,因为我觉得它讲的很详细了,第四节垃圾判断过程在书中实际上是一长串的代码,看懂不难,不过我想画个流程图可能更清楚点,这个流程图是用plantUML画出来的,这个工具可以用代码画出各种图,功能强大,有兴趣的可以百度搜搜,下面是这个流程图的代码:

@startuml
start
:对对象进行可达性分析;
if (对象是否为垃圾?) then (是)
    :进行第一次标记;
    if (对象没有覆盖finalize()方法 或 finalize()方法已经被虚拟机调用) then(是)
        :没必要执行对象的finalize()方法;
    else (否)
        :将对象放入队列F-Queue中;
        :等待虚拟机的Finalizer线程执行对象的finalize()方法;
        :执行对象的finalize()方法;
    endif
    if (对象是否为垃圾?) then (是)
        :进行第二次标记;
        :垃圾回收;
    else (否)
        stop
    endif
else (否)
    stop
endif
stop
@enduml

加载全部内容

相关教程
猜你喜欢
用户评论