Java垃圾回收机制

记得大四快毕业的时候为了应付面试就记了一遍垃圾回收,但是毕竟当时对java还是刚刚入门(虽然现在也是个菜鸟),当时理解了回收的流程,但是对于为什么这样做之类的深层意思还是不明白,于是很快又忘记了,最近在啃jvm虚拟机的东西,决定重新捡起来并记录一下。其实垃圾回收并不是java中特有,相反,GC的历史比java久远,但是因为我只熟悉java,就以java为例了。

1、回收对象

java虚拟机中回收的区域主要有两个:堆区和方法区。之前只是一门心思的去记堆区,其实方法区也是有回收的,但是方法区进行垃圾回收的“性价比”比较低,在堆中,尤其是新生代,常规应用中进行一次垃圾收集一般可以回收70%~95%的空间,而方法区回收效率远低于这个数,这里重点记录堆的回收,方法区放在最后写。

2、堆的回收

其实不管是堆还是方法区,回收的过程无异于两个,标记和清除,对于标记,有“引用计数”和“可达性分析”两个算法,比较常用的是“可达性分析”算法。

2.1.1 标记—引用计数算法

给对象中添加一个引用计数器,当有一个地方引用它时,计数器就加1,当引用失效时,引用减1,当计数器为0那么这个对象就是可以被回收的。这个方法思路很简单,确实也很高效,但是有一个问题:当出现两个对象循环引用的时候(A引用B,B引用A,但是其他对象对这两个对象都没有引用),这时两个对象理论上是可以被回收的废柴了,但是因为计数器都不是0,导致无法回收。所以这个算法并不是主流语言的标记算法,当然java也不会采用

2.1.2 标记—可达性分析算法

这里写图片描述
图片来源(https://www.cnblogs.com/xiaoxi/p/6486852.html) 侵权联系我删除

这个算法基本思路就是通过一列的称为“GC Root”(有关可以作为GC Root的对象,见文末)的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连,则证明此对象是不可用的。这个算法是目前使用较广泛的标记算法

2.2 对象缓刑—finalize()方法

      这个方法现在已经不推荐使用,在jdk9开始就被标记位deprecated,我之前觉得既然这个方法不推荐使用,也没有深入理解它,但是总觉得心里有个疙瘩,这次就了解一下它的使用时机和作用再忘掉它也不迟。
      在对象被标记为可回收时,虚拟机会对这个对象进行回收,但是此时对象是不一定就“死”了的,它还有最后一个自救的机会,那就是finalize()方法。如果对象覆盖了finalize()方法并且在此之前没有调用过这个方法,那么垃圾回收的时候finalize()方法就会被执行,在这个方法里面,对象重新和引用链的任何一个对象建立连接,这时它就自救成功了—会被移除“可被回收”集合。当然,如果对象没有覆盖finalize()方法,或者就算覆盖了,但是之前已经执行过一次这个方法了,那对象就无法自救了,具体看例子:

public class FinalizeEscapeGC {

    public static FinalizeEscapeGC SAVE_HOOK = null;

    public void isAlive(){
        System.out.println("yes, i am still alive");
    }

    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed");

        SAVE_HOOK = this;
    }

    public static void main(String[] args) throws InterruptedException {
        SAVE_HOOK = new FinalizeEscapeGC();

        SAVE_HOOK = null;
        // 对象第一次拯救自己
        System.gc();
        // 因为finalize方法优先级很低,所以暂停0.5秒以等待它
        Thread.sleep(500);
        if(SAVE_HOOK == null){
            System.out.println(" no, i am dead");
        }else{
            SAVE_HOOK.isAlive();
        }

        SAVE_HOOK = null;
        // 下面这段代码与上面的完全相同,但是这一次自救却失败了
        // 一个对象的finalize方法只会被调用一次
        System.gc();
        Thread.sleep(500);
        if(SAVE_HOOK == null){
            System.out.println(" no, i am dead");
        }else{
            SAVE_HOOK.isAlive();
        }
    }
}

执行结果是:
这里写图片描述

代码中有两段完全一样的代码片段,执行结果是一次逃脱成功,一次失败,这是因为任何 一个对象的finalize()方法都只会被系统调用一次,如果对象面临下一次回收,它的finalize()方法不会被执行,因此第二段代码自救失败。

所以说,finalize()相当于对象的一次缓刑机会。

2.3 清除算法

有四种,标记清除算法、复制算法、标记整理算法、分代回收算法。分代回收算法是前面几个算法的综合,也是现今最常用的回收算法

2.3.1 清除—标记清除算法

先标记出需要回收的对象,然后统一清除标记的对象。这个算法存在的问题是:(1)效率低。标记和清除两个过程效率都不高(其实这里我的理解是因为初始时,需要被收集的对象大约占了70%以上,也就是占了多数,标记和清除需要在这么多对象上操作,效率肯定不高,不知道理解的对不对,望高人解答)
(2)空间不连续。标记清除之后产生的内存是不连续的,遇到大内存分配可能会找不到连续内存导致提前触发垃圾回收

具体过程图解如下:
这里写图片描述
图片来源(https://www.cnblogs.com/xiaoxi/p/6486852.html) 侵权联系我删除

2.3.2 清除—复制算法

为了解决上面算法的问题,复制算法出现了。它的思路是把内存等分成两块,分配对象的时候在其中一半,当要进行垃圾回收时,把存活的对象复制到另外一半内存,然后把之前的一般内存统一回收。因为存活的对象是占少数的,所以效率不会低,另外因为一半内存回收之后就是空闲的,所以不会有太多碎片。

具体过程如下:
这里写图片描述
图片来源(https://www.cnblogs.com/xiaoxi/p/6486852.html) 侵权联系我删除

但是这个算法有一个问题就是,把内存缩小了一半,代价有点高。况且,一般第一次回收的时候,存活对象是占少数的,不需要50%的内存去存存活对象。所以不需要内存1:1划分,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,见分代回收算法。

2.3.3 清除—标记整理算法

之所以 我们说复制算法相对标记清除算法存活率高,是因为一般情况下存活对象处于少数,但是我们不能排除存活对象较高的情况。这个时候就要考虑用标记整理算法,标记整理和标记清除类似,不同的是标记整理不是直接对可回收对象清理,而是先让存活对象向一端移动,然后直接清理掉边界可回收端的对象
具体算法如下:
这里写图片描述
图片来源(https://www.cnblogs.com/xiaoxi/p/6486852.html) 侵权联系我删除

2.3.4 清除—分代回收算法

上面那么多算法有利也有弊,并且不同算法适应不同场景(复制 算法适用于存活对象少的,标记整理适用于存活对象多的),所以现在人们考虑让他们综合。

将我们的内存分为年轻代和老年代,年轻代分为Eden、Survivor1、Survivor2,他们的比例一般是8:1:1,对象新建一般是发生在Eden区,第一次垃圾回收时,Eden存在少部分存活对象,会把这些存活对象复制到其中一个Survivor区,然后回收Eden区内存。然后第二次垃圾回收时,会把Eden区存活对象和上次被复制的Survivor区的存活对象复制到另一个Survivor,然后回收Eden区和其中一个Survivor区内存,以此类推,所以每次我们最小空闲去就是年轻代内存的10%(一个Survivor大小),当我们年轻代存活对象所占内存超过了10%的时候,这时就要考虑把超出的部分分一点到老年代了。另外当两个存活区切换了几次(HotSpot虚拟机默认15次,用-XX:MaxTenuringThreshold控制,大于该值进入老年代)之后,仍然存活的对象(其实只有一小部分,比如,我们自己定义的对象),也将被复制到老年代。年轻带的回收我们一般叫Young GC

在年轻代把对象复制到老年代之前,会首先检查对象大小是否超出了老年代剩余空间,如果超出了,就会触发Full GC(全量回收),否则就可以复制到老年代了,老年代的回收算法会考虑使用标记整理算法(其实这个可以通过设置-XX:+HandlePromotionFailure参数改变,
https://blog.csdn.net/mccand1234/article/details/52078645)。

全量回收呢,就好比我们刚才比作的大扫除,毕竟动做比较大,成本高,不能跟平时的小型值日(Young GC)相比,所以如果Full GC使用太频繁的话,无疑会对系统性能产生很大的影响。

所以要合理设置年轻代与老年代的大小,尽量减少Full GC的操作

参考:
https://www.cnblogs.com/xiaoxi/p/6486852.html
https://blog.csdn.net/mccand1234/article/details/52078645
《深入理解java虚拟机》向志明

正文只记录我认为关键记忆的,否则记多了容易记不清,文末是附录型的补充:
(1)java中,可作为GC Root的对象:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象


(2)方法区回收
最主要回收两部分内容:废弃常量和无用的类,回收废弃常量和回收堆中对象类似,以常量池中字面量的回收为例,假如一个字符串“abc”已经进入了常量池中,但是当前系统没有任何一个String对象是叫做”abc”的,换句话说,就是没有任何String对象引用常量池中的”abc”常量,也没有其他地方引用了这个字面量,如果这是发生内存回收,而且必要的话,这个”abc”常量就会被系统清理出常量池,常量池中的其他类(接口)、方法、字段的符号引用也与此类似

判定一个常量是否是“废弃常量”比较简单,而要判断一个类是否是“无用的类”条件就苛刻了:
如何判断无用的类呢?需要满足以下三个条件

  1. 该类的所有实例都已经被回收,即Java堆中不存在该类的任何实例。

  2. 加载该类的ClassLoader已经被回收。

  3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

满足以上三个条件的类可以进行垃圾回收,但是并不是无用就被回收,虚拟机提供了一些参数供我们配置

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值