美文网首页
垃圾回收基础

垃圾回收基础

作者: miaoLoveCode | 来源:发表于2017-04-11 19:22 被阅读329次

    垃圾回收(Garbage Collection,GC)是虚拟机内存管理很重要的一个模块,它帮助虚拟机在技术层面上做到了内存动态分配后的回收。垃圾回收主要发生在线程公有的区域。GC主要完成这样三件事情:

    • 确定哪些内存需要回收;

    • 确定待回收的内存在何时回收;

    • 如何来做垃圾回收。

    接下来就以如何做这三件事情为切入点,详细介绍一下GC的相关基础。

    注:为什么垃圾回收发生在线程公有区域?
    私有区域会随着线程的消亡而消亡,但是共有区域不会,公有区域内存的回收需要GC来完成。了解Java内存模型的小伙伴都知道,堆和方法区是公有的,所以GC目标区域在堆和方法区。

    如何确定对象已死?

    堆中几乎存放着Java程序中所有的对象实例,对于回收堆上的内存来说,确定哪些内存需要回收实质就是确定哪些对象已经死了,而后续的垃圾回收也就是将这些已死对象的内存做回收。那么,如何确定对象已死?

    引用计数算法

    确定一个对象是否已经死了就是看这个对象是不是没有引用了。在很多资料上都看到了判断对象是不是有引用的算法是这样子的:给对象添加一个引用计数器,一旦有地方引用它时,计数器值+1,引用失效时,计数器值-1,当引用计数器值为0时,该对象不再被引用。客观的说,引用计数算法实现很简单,判断效率也很高,但是,假如存在对象之间互相引用,这个算法就解决不了了,所以Java的GC并没有选用该算法来管理内存。

    举个简单的例子:对象A和B都有私有属性instance,赋值令A.instance = B,B.instance = A,A和B互相引用。


    demo

    GC日志:


    GC日志

    从GC日志可以看出,虚拟机并没有因为a和b互相引用就不回收它们,这也说明虚拟机并不是通过引用计数算法来判断对象是否存在引用的。

    根搜索算法

    以GC Roots对象作为起始点,从这些节点依次向下搜索,如果当前对象到GC Roots没有任何的路径相连(对象不可达)时,那么,当前对象没有引用。

    引用链:对象到GC Roots的走过的路径

    在Java中,以下对象可作为GC Roots:

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

    2. 本地方法栈中引用的对象;

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

    4. 方法区中的静态属性引用的对象。

    根搜索算法中不可达的对象,是不是就非死不可呢?其实并不是的,要真正宣告一个对象死亡,至少要经过两次标记。在这里必须要多嘴几句说说finalize。

    finalize

    有过面试经历的人,可能都被问到了final,finally和finalize的区别。前两个关键字比较常用,在这里就不再做介绍了。finalize是一个方法名,Java允许使用finalize方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作,所以很多文章中又称它为析构函数的替代者。那么finalize方法是何时何地被调用呢?每次GC都会调用它么?

    • finalize方法调用过程
      待回收的对象被标记出来之后,会对这些对象做一次筛选,看对象是否有必要执行finalize方法。判断对象是否有必要执行该方法主要有以下两个依据:

      • 对象有没有覆盖finalize方法;

      • 对象已覆盖finalize方法,检查finalize方法是否被虚拟机调用过,如果已被调用,就不需要再次执行。

      验证待回收对象有必要执行finalize方法后,这个对象将会被放置到队列F-Queue中,并且会被虚拟机的单独线程Finalizer执行。一定需要注意的是,这里的执行不代表执行成功,只触发finalize方法,不承诺等待该方法执行结束。为什么这么设计呢?

      • 对象finalize方法执行时间较高,会导致F-Queue中的其他对象等待时间较长;

      • 如果程序员不小心,在finalize方法的实现中写了个死循环,这时候F-Queue中的其他对象会一直处于等待状态,甚至导致整个GC崩溃。

    注:看到这里大致已经了解了finalize的执行过程以及执行次数,千万别认为finalize方法会一直都被执行,其实它只会被执行一次,一旦它在上次GC已经被执行,以后就不会再执行,除非你重启系统。

    • finalize使用案例
      finalize使用案例
      运行结果:

      案例通过finalize方法成功的实现了逃脱被GC,但是从运行结果可以看出,第一次确实成功的逃脱了,但是第二次就没那么好运了,被GC掉了。这也能说明finalize方法只能被执行一次。

    如何回收内存?

    在确定需要回收的对象之后,接下来就将这些内存做回收。先来看看垃圾回收算法吧,本文只简要介绍垃圾回收算法思想,至于实现后面文章会给出较详细的分析。

    • 标记 - 清除算法
      标记 - 清除算法是最基础的垃圾收集算法。它分为两个阶段:

      • 标记阶段:标记出所有待回收的对象;

      • 清除阶段:清除掉所有被标记的对象;

      该算法实现过程简单,成本低,但是它有这样几个缺点:

      • 效率较低;

      • 标记清除并没有对内存做过压缩整理,这样会导致清除后出现大量的内存碎片,空间内存碎片较多可能会导致在后续程序需要分配较大对象时无法找到足够的连续内存空间而不得不提前触发一次GC。

      为什么说它是最基础的垃圾收集算法,是因为后续的垃圾收集算法都是基于该算法思路并对其不足之处改进得到的。

    • 复制算法
      为了改善标记 - 清除算法的效率问题,复制算法出现了。它将可用内存划分为大小固定的两块,每次只使用其中的一块,当这一块内存使用完了,就将还活着的对象复制到另外一块内存上,一次性清理掉已使用过的内存空间。这样子做的好处就是每次的垃圾收集只对其中一块内存进行回收,效率提升,而且,复制算法在回收内存不会产生垃圾碎片。但是,该算法有个不好处就是它将原来的内存缩小了。一般young区采用该算法来回收内存。

    注:HotSpot默认Eden区和Survivor区大小比例为8:1,至于为什么设置这个比例是因为young区的对象98%都是朝生夕死的。当然,JVM也提供参数以供使用者来更改Eden区和Survivor区的大小比例。

    • 标记 - 整理算法
      由于每次要执行较多的复制操作,复制收集算法在对象存活率比较高的区域做内存回收就比较费劲了。更重要的一个点就是如果不想浪费一部分内存空间,就需要额外的空来进行分配担保,来应对被使用的内存中所有对象都存活的情况。根据old区的特点,标记 - 整理算法被提出。该算法的标记的过程与标记 - 清除算法一致,但是,后续的过程就有区别了,标记 - 整理算法不会直接对已标记的对象做内存回收,而是会让所有还存活的对象移动到一端,然后清除掉这一端边界以外的内存。

    垃圾回收算法的思想到这里就分析完毕了,要想完整的了解垃圾回收,还需要了解垃圾回收载体 - 垃圾收集器。考虑到垃圾收集器类型较多,分析篇幅较大,就不再本文做相关分析了,会在后续的文章就垃圾收集器做详细的分析。

    到这里,垃圾回收的相关基础内容就介绍完毕了,欢迎大家继续关注后续的垃圾回收更深层次的介绍。

    相关文章

      网友评论

          本文标题:垃圾回收基础

          本文链接:https://www.haomeiwen.com/subject/ipofattx.html