JVM-垃圾回收算法复习总结(超详细版)

一、垃圾回收概述

1、什么是垃圾?

垃圾:指的是在运行中没有任何引用的对象(没有任何指针指向的对象),这一类对象是需要被回收的,我们称之为垃圾。如果垃圾不被回收的话,最终有可能会导致内存溢出。

2、垃圾回收的主要区域?

这里是引用
● 垃圾回收器可以对年轻代回收,可以对老年代回收,甚至是全堆和方法区的回收。其中大部分的垃圾回收都是发生在年轻代。老年代比较少收集,元空间基本不收集。

二、垃圾回收算法–标记阶段

1、对象存活判断之–引用计数算法

思路
可以理解为就是一个计数器。每个对象保存着一个整型的计数器,该对象A只要被任何一个对象引用了,计数器就+1,引用失效的时候,计数器就-1。只要对像A的计数器为0,即表示A不再被引用,可以进行回收。
优点
实现简单,辨识率高。
判定效率高,回收没有延迟性。
缺点
单独的计数器,增加了内存的开销。
无法处理循环引用,导致Java垃圾回收器没有使用这种算法
图示:循环引用在这里插入图片描述

2、对象存活判断之–可达性分析(根搜索)算法

思路
我们通过下面这张图来进行描述:
所谓的可达性分析算法,又叫根搜索算法,顾名思义,就是从根节点开始往下进行搜索的算法。可达性分析算法就是以根对象集合(GC Roots)为起始点,从上往下的方式搜索GC Roots所连接的对象是否可达,如果可达的,则视为是存活的对象不进行回收,否则可以进行回收。如下图所示。在这里插入图片描述
其中被根节点直接或间接连着的称之为引用链。不在这条链上的即是垃圾(需要被回收的对象)。
优点:回收效率高,不存在循环依赖而导致内存泄漏的问题,因此此算法是Java所选择的。
GC Roots包含的对象类型:
虚拟机栈中引用的对象
字符串常量池的引用
Java类的引用类型的静态变量
总结为一句话:
一个指针保存(指向)了堆内的对象,但是自己却不在堆内,那么它就是一个GC Root。

3、对象的finalization机制(重点)

定义
Java语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁(回收)之前的自定义处理逻辑。
使用
通过重写finalize()方法;垃圾被回收之前总会先执行这个方法
注意
应该交给垃圾回收机制调用,永远不要主动调用某个对象的finalize ()方法。原因是因为
1、finalize时对象可能复活
2、执行时间没有保障,完全由GC线程决定
3、糟糕的finalize会严重影响GC性能

如果从GC Roots无法到达某个对象,本质上该对象已经是垃圾了。一般来说,需要被回收了。但事实上,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段。一个无法触及(可达)的对象有可能在某一个条件下“复活”自己,如果这样,那么对它的回收就是不合理的由于finalize()方法的存在,JVM中对象一般处于三种可能的状态:
可触及的(可达的)
字如其名,根节点可以到达的对象
可复活的(不可达的但可复活的)
根节点无法到达的对象,但是可以在回收之前,GC线程执行finalize方法复活的。
不可触及的(不可达不可复活的)
根节点无法到达的对象,finalize也无法复活的。该状态下对象只能被回收,因为finalize方法只会执行一次,相当于如果finalize方法中可以复活,这个复活只能复活一次,不能无限复活

示例代码:

public class finalizationTest {

    public static finalizationTest obj;//静态类变量,属于GC Roots根节点


    //此方法只能被调用一次
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("调用当前类重写的finalize()方法");
        obj = this;//被回收的对象重新获得引用,“复活”
    }


    public static void main(String[] args) {
        try {
            obj = new finalizationTest();
            // 设置为null,如果没有重写finalize方法,则直接死亡,这里我们重写了
            // 对象第一次成功拯救自己
            obj = null;
            System.gc();//调用垃圾回收器
            System.out.println("第1次 gc");
            //Finalizer线程优先级很低,暂停2秒,确保主线程在其GC之后执行
            Thread.sleep(2000);
            if (obj == null) {
                System.out.println("obj is dead");
            } else {
                System.out.println("obj is still alive");
            }
            System.out.println("第2次 gc");
            // 下面这段代码与上面的完全相同,复活失败,因为finalize只能被执行一次
            obj = null;
            System.gc();
            Thread.sleep(2000);
            if (obj == null) {
                System.out.println("obj is dead");
            } else {
                System.out.println("obj is still alive");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

输出结果:

调用当前类重写的finalize()方法
第1次 gc
obj is still alive
第2次 gc
obj is dead

三、垃圾回收算法–清除阶段

●在前面标记阶段我们通过可达性分析算法找出需要待回收的垃圾后,现在开始进入我们的垃圾回收阶段,目前JVM常见的三种收集算法有如下三种:

1、对象回收之-标记清除算法

实现思想:
正如其名字所言,标记清除算法分为两个阶段,标记阶段和清除阶段。当堆空间内存满了的时候,就会触发STW(Stop The World)停止线程去进行垃圾回收,这个过程中会执行两项工作:标记和清除
标记
从根节点(GC Roots)标记所有的被引用的对象(注意是引用,关于博客和很多地方说的都是未被引用的对象,这个说法是错的)。一般是在对象头中标记为可达对象(可触及的)。
清除
对堆内存从头到尾进行线性遍历,如果发现某个对象在其对象头中没有被标记为可达对象,则将其回收。
如图所示:
在这里插入图片描述
优点
1、基础算法,实现简单
缺点
1、效率低下,递归的方式进行遍历;
2、GC的时候,停止用户线程,用户体验差;
3、清理出的空间不是连续的,产生内存碎片,需要维护一个空闲列表
补充说明:
需要注意的一点是,这里的清除,并不是真正意义上的清空,而是把需要清除对象的地址保存在空闲的地址列表里。通俗的讲就好比如我们电脑的硬盘,我们格式化了之后,其实是可以恢复的,但前提是不能往里面放东西,因为这样的话就会覆盖了原来的地址,这里也是一个意思。

2、对象回收之-复制算法

实现思想:
将可用的内存空间按容量分为大小相同的两块,每次只使用其中一块, 当其中一块内存用完了,就将活着的对象复制到另一块空间上,然后再把使用过的内存空间清理掉。
如图所示:
在这里插入图片描述
我们前面说的堆空间的内存结构中,新生代的两个幸存者区S0,S1使用的就是复制算法。使用的是指针碰撞的内存分配方式。
优点:
1、通过上图我们能很明显的看到,通过复制算法可以解决内存碎片化的问题。
2、没有经过标记阶段和清除阶段,实现简单,运行高效。
缺点:
1、内存缩小为原来的一半。存活对象如果很多的话,复制算法复制对象数量并不会很大,效率低下。
应用场景:
复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生。

3、对象回收之-标记压缩算法

实现思想:
通过名字其实可以看出来,该垃圾回收算法分为两个阶段,标记和压缩。在第一阶段的标记和和标记清除算法中的标记阶段是一样的。第二个阶段压缩,指的是将所有存活的对象压缩到内存的一端,按顺序排放。干说有点枯燥,我们上图演示:
在这里插入图片描述
标记压缩算法最终的本质上其实就是标记清除算法的基础上,执行了碎片化的整理。两者的本质差别其实就在于整理碎片上,标记清除算法没有对对象地址进行移动,而标记压缩算法因为整理了内存空间,实际上是对对象地址进行了以动的。
优点:
解决了上述两种算法的缺点。
缺点:
效率比较低。
移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址。移动过程中,需要全程暂停用户应用程序。即: STW


四、三种垃圾回收算法的小结

这里是引用


五、分代收集算法

分代收集算法
与其说是一种算法,不如说是一种思想。因为我们上述的三种算法各有各的优点,没有一个很明确的最优的算法,只能说有最合适的算法,分代收集算法的思想就是根据不同场合选择最适合当下场合垃圾回收算法的算法。
举个例子:
比如我们前面说到,复制算法它的适用场合是对象生命周期短,垃圾对象多的场景下效率比较高,而新生代最大的一个特征就是对象朝生夕死,而且数据表明80%以上的垃圾都在新生代产生,因此在新生代分代收集算法采用的就是复制算法。
而在老年代,我们都知道老年代的特点是区域较大,对象生命周期长、存活率高,回收不频繁。因此在这种阶段,分代收集算法采用的一般是由标记清除算法或者是标记压缩算法的实现。


六、增量收集算法和分区算法

1、增量收集算法

引言:
我们都知道在使用标记清除算法或者标记压缩算法的时候,由于需要对垃圾对象进行标记,会进行STW操作,暂停用户线程。这直接造成的结果就是非常影响用户的体验,那有没有一种方法能在对垃圾回收的同时,可以不影响用户体验呢?诶!增量收集算法应运而生!
增量收集算法的思想:
如果要满足回收垃圾的同时,又不影响用户的体验,我们是不是很眼熟呢?对!没错,并发的思想在这里就得到充分的应用!也就是说,对于垃圾回收线程和用户线程,我们只要在一个极短的时间间隔内,每次垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成
这就是增量收集算法的思想。总的来说,增量收集算法的基础仍是传统的标记一清除和复制算法
优点
提高了用户的体验
缺点
线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。

2、分区算法

引言:
我们都知道,关于堆的垃圾回收,对空间越大,GC时间越大,因而引发的STW时间也越长,那么除了上面说的增量收集算法能减少STW时间,还有什么方法呢?因此引出我们的分区算法:
思想:
为了减少STW的时间,该算法的实现思想是将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。
分代收集算法的思想是将区划分成不同的代,根据不同的代进行不同的垃圾回收策略,而分区收集算法则是把整个堆分成一小个一小个大小相同的region,每一个小区间都独立使用,独立回收,如下图所示:
在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值