jvmJava基础一些收藏

《深入理解JVM虚拟机》读书笔记-标记阶段的两种算法

2021-10-26  本文已影响0人  乙腾

标记阶段的两种算法

对象存活判断

在堆里存放着几乎所有的Java对象实例,在GC执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为己经死亡的对象,GC才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程我们可以称为垃圾标记阶段。

那么在JVM中究竟是如何标记一个死亡对象呢?简单来说,当一个对象已经不再被任何的存活对象继续引用时,就可以宣判为已经死亡。

判断对象存活一般有两种方式:引用计数算法和可达性分析算法。

<font color=red>垃圾回收这个动作通过推理可知,至少需要两个步骤:1.判断对象是否是垃圾 2.回收垃圾 ;这里标记阶段就是第一步,判断对象是否是垃圾。</font>

注意:

哪些地方有GC?

meta space 和 heap逻辑上是都有GC和OOM的,pc计数器没有GC,OOM,stack没有GC,有SOF。

方法区确实有GC,但是不是虚拟机必须清理的,所以这里说的GC主要说的是heap

引用计数算法

优点:

实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。

缺点:

注意

1.缺点1,2是可以推理到的

其实这种引用计数法的实现思路是很容易让大家想到的,即所有对象都维护一个变量存储当前对象被引用的次数,如果为0就标记为垃圾,但是这种方式必然两方面的确定:

1.开销内存空间

2.每次变量的重新赋值,对象应用的变化,都会导致,对象引用计数器的动态更新。

以上两点是可以直接推理出的缺点。

n2.循环引用

循环引用指的是一个变量P引用一个对象A,对象A的引用计数器属性为1,同时这个对象里面的一些变量,指向对象B,而对象B中有变量指向对象C,对象C中有变量指向对象A,这样就造成了一个循环引用,而此时对象A的引用计数器属性在原有的基础上+1,变成2。

image.png

一旦这个变量P不再指向对象A,那么此时其引用计数器属性变成了1,但是因为对象循环引用其引用计数器属性不会变成0,那么其也就不会被标记为垃圾,造成内存泄露。


例子:证明java没有使用引用计算


/**

 * testGC()方法执行后,objA和objB会不会被GC呢?

 * @author zzm

 */

public class ReferenceCountingGC {

    public Object instance = null;

    private static final int _1MB = 1024 * 1024;

    /**

     * 这个成员属性的唯一意义就是占点内存,以便能在GC日志中看清楚是否有回收过

     */

    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;

        // 假设在这行发生GC,objA和objB是否能被回收?

        System.gc();

    }

}

运行结果


[Full GC (System) [Tenured: 0K->210K(10240K), 0.0149142 secs] 4603K->210K(19456K), [Perm : 2999K->2999K(21248K)], 0.0150007 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]

Heap

    def new generation   total 9216K, used 82K [0x00000000055e0000, 0x0000000005fe0000, 0x0000000005fe0000)

    Eden space 8192K,   1% used [0x00000000055e0000, 0x00000000055f4850, 0x0000000005de0000)

    from space 1024K,   0% used [0x0000000005de0000, 0x0000000005de0000, 0x0000000005ee0000)

    to   space 1024K,   0% used [0x0000000005ee0000, 0x0000000005ee0000, 0x0000000005fe0000)

    tenured generation   total 10240K, used 210K [0x0000000005fe0000, 0x00000000069e0000, 0x00000000069e0000)

    the space 10240K,   2% used [0x0000000005fe0000, 0x0000000006014a18, 0x0000000006014c00, 0x00000000069e0000)

    compacting perm gen  total 21248K, used 3016K [0x00000000069e0000, 0x0000000007ea0000, 0x000000000bde0000)

    the space 21248K,  14% used [0x00000000069e0000, 0x0000000006cd2398, 0x0000000006cd2400, 0x0000000007ea0000)

    No shared spaces configured.

从运行结果中可以清楚看到内存回收日志中包含“4603K->210K”,意味着虚拟机并没有因为这两个对象互相引用就放弃回收它们,这也从侧面说明了Java虚拟机并不是通过引用计数算法来判断对象是否存活的。

可达性分析算法

  当前主流的商用程序语言(Java、C#,上溯至前面提到的古老的Lisp)的内存管理子系统,都是通过可达性分析(Reachability Analysis)算法来判定对象是否存活的。这个算法的基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。

  即将活跃的引用都放到一组集合中,每个存放到集合中的引用都作为根节点即GC Roots,每个被引用的对象都通过一条线和GC Roots连接,那么只有在内存中被GC Roots连接的对象才是存活的对象

image.png

在Java技术体系里面,固定可作为GC Roots的对象包括以下几种:

上面列了那么多,其实就是想说,因为Root采用栈方式存放变量和指针,所以如果一个指针它保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那它就是一个Root。将堆外,所有对于堆的指向都拷贝到GC Roots中。

注意

上一篇 下一篇

猜你喜欢

热点阅读