Java虚拟机(JVM):第三幕:自动内存管理 - 垃圾收集器与内存分配策略

前言:Java与C++之间有一堵高墙,主要是有内存动态分配和垃圾收集技术组成的。墙外的人想要进来,墙内的人想要出去。

一、概述

  每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的。内存的分配和回收都具有确定性。

二、对象已死?

  垃圾收集器在对堆进行回收之前,不能确定哪些“对象”活着,哪些“对象”死去。

  1、引用计数算法

    在对象中,添加一个引用计数器,当有一个地方引用它时,计数器的值加一;引用失效的时候,计数器的值减一;任何时刻,计数器为零的对象不能被再次使用。这样就存在一个问题,如果两个对象之间相互引用,是否永远不能够被回收。代码如下所示:

package com.example.dayevery;
public class ReferenceCountingGC{
    public Object instance = null;
    private static final int _1MB = 1024*1024;
    // 测试 是否被回收
    private byte[] bigSize = new byte[2 * _1MB];
    public static void TestGc (){
        ReferenceCountingGC objA = new ReferenceCountingGC();
        ReferenceCountingGC objB = new ReferenceCountingGC();
        objA.instance = objB;
        objB.instance = objA;
        objA = null;
        objB = null;
        // 测试 objA和objB是否被系统回收
        System.gc();
    }
    public static void main(String[] args) {
        // 提交执行
        TestGc();
        System.out.println("1");
    }
}

  通过结果发现,虚拟机最终并没有因为两个对象之间互相引用就放弃回收它们,也就是说Java虚拟机并不是通过引用技术算法来判断对象是否存活。

  2、可达性分析算法

    当前主流算法(Java.C#)的内存管理子系统,采用可达性分析算法来判断对象是否存活。

    算法思路:通过一系列称之为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程走过的路径称之为“引用链”,如果某一个对象和GC Roots集合之间没有任何的引用链相连,则证明此对象之间不能被再次使用。

    GC Roots:除了固定的集合之外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可能会有其他的“对象”临时的加入,从而构成了完整的GC Roots集合。

三、生存还是死亡?

  当在可达性分析算法中,被标记为“死亡”的时候,该对象并不是真正的“死亡”,除了这一次的标记之外,还有第二次自救,那就是采用finalize()方法,从而让自己跟某一个起始节点集合挂上联系。

  finalize()方法:能且只能执行一次,如果失败,那么就会被回收。这是一个被可以在Java语言中被遗忘的方法。

四、回收方法区

  方法区回收的垃圾主要有两部分:废弃的常量和不再使用的类型。其中废弃的常量,举个例子,一个字符串“java”进入常量池中,没有任何的地方引用这个字符串,这属于废弃的常量。

  不再使用的类型要满足以下几点。1、该类的所有实例被回收。2、该类的类加载器已经被全部回收。3、该类对应的java.lang.Class类对象没有在任何地方引用。

五、垃圾收集算法

  垃圾收集算法可以划分为“引用计数式垃圾收集”和“追踪式垃圾收集”。本文主要介绍追踪式垃圾收集。

  首先要介绍分代收集理论,它建立在两个分代假说之上:1、弱分代假说。2、强分代假说。此后,还延伸出来“跨代引用假说”:存在相互引用的两个对象应该倾向于同时生存或者同时消亡的。

  针对不同的区域安排与里面存储对象存亡特征相匹配的垃圾收集算法:1、标记 - 复制算法。2、标记 - 消除算法。3、标记 - 整理算法。

  1、标记 - 消除算法

    标记 - 消除算法是最基础的算法,主要分为“标记”和“清除”两部分,标记过程就是对象是否属于垃圾的判定过程。

    算法流程:首先标记出所有需要回收的对象,标记完成之后,统一回收掉所有被标记的对象。同时也存在两个缺点:1、执行效率不太稳定。2、内存空间的碎片化问题。

  2、标记 - 复制算法

    为了解决标记 - 清除算法,面对可回收对象时执行效率低的问题,提出采用“半区复制”,将内存划分为大小相等的两块,每次只是使用其中的一块。当其中一块内存用完之后,就将还存活的对象复制到另一块的上面,然后把已经使用过的内存空间一次性的清理到。最明显的缺点:可用内存缩小为原来的一半。

    标记 - 复制算法中的“半区复制”在89年发生了变化,针对“新生代”朝生夕灭的特点,采用了一种更加优化的半区复制分代策略,称为Appel式回收:将新生代的内存分为一块较大(80%)的Eden空间和两块较小(10%)的Survivor空间。

    每次使用Eden空间和一块Survivor空间,在垃圾回收的时候,将仍然存活的对象直接复制未被使用的Survivor空间中,当所需空间不能满足存活对象要求,采用“逃生门”的安全设计,依赖其他内存区域(老年代)进行分配担保。PS:有点像是银行借贷这种。

  3、标记 - 整理算法

    算法过程:标记 - 整理算法和标记 - 清除算法的第一步是相同的,但是标记 - 整理算法不能够直接对可回收对象进行清理,而是让所有的存活对象都往内存空间的一端移动,直接清理掉边界以外的内存。

六、HotSpot虚拟机的算法细节实现

    见下一篇博客。

posted @ 2023-07-07 12:29  我太想努力了  阅读(10)  评论(0编辑  收藏  举报