【JVM】JVM系列之垃圾回收(二)

简介: 如果不进行垃圾回收,内存迟早都会被消耗空,因为我们在不断的分配内存空间而不进行回收。除非内存无限大,我们可以任性的分配而不回收,但是事实并非如此。所以,垃圾回收是必须的。

一、为什么需要垃圾回收


  如果不进行垃圾回收,内存迟早都会被消耗空,因为我们在不断的分配内存空间而不进行回收。除非内存无限大,我们可以任性的分配而不回收,但是事实并非如此。所以,垃圾回收是必须的。


二、哪些内存需要进行垃圾回收


  对于虚拟机中线程私有的区域,如程序计数器、虚拟机栈、本地方法栈都不需要进行垃圾回收,因为它们是自动进行的,随着线程的消亡而消亡,不需要我们去回收,比如栈的栈帧结构,当进入一个方法时,就会产生一个栈帧,栈帧大小也可以借助类信息确定,然后栈帧入栈,执行方法体,退出方法时,栈帧出栈,于是其所占据的内存空间也就被自动回收了。而对于虚拟机中线程共享的区域,则需要进行垃圾回收,如堆和方法区,线程都会在这两个区域产生自身的数据,占据一定的内存大小,并且这些数据又可能会存在相互关联的关系,所以,这部分的区域不像线程私有的区域那样可以简单自动的进行垃圾回收,此部分区域的垃圾回收非常复杂,而垃圾回收也主要是针对这部分区域。


三、垃圾收集算法


  任何垃圾收集算法都必须做两件事情。首先,它必须检测出垃圾对象。其次,它必须回收垃圾对象所使用的堆空间并还给程序。那么问题来了,如何检测出一个对象是否为垃圾对象呢?一般有两种算法解决这个问题。1. 引用计数算法 2. 可达性分析算法。


  1.引用计数算法


  堆中的每一个对象有一个引用计数,当一个对象被创建,并把指向该对象的引用赋值给一个变量时,引用计数置为1,当再把这个引用赋值给其他变量时,引用计数加1,当一个对象的引用超过了生命周期或者被设置为新值时,对象的引用计数减1,任何引用计数为0的对象都可以被当成垃圾回收。当一个对象被回收时,它所引用的任何对象计数减1,这样,可能会导致其他对象也被当垃圾回收。


  问题:很难检测出对象之间的额相互引用(引用循环问题)


  如下代码段可以从反面验证虚拟机的垃圾回收不是采用的引用计数。


package com.leesf.chapter3;
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;
        // 进行垃圾回收
        System.gc();    
    }
    public static void main(String[] args) {
        testGC();    
    }
}


 代码的运行参数设置为: -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8


image.png


在代码objA = null 和 objB = null 之前,内存结构示意图如下


image.png


注意:局部变量区的第一项并没有this引用,因为testGC方法是类方法。

  在代码objA = null 和 objB = null 之后,内存结构示意图如下


image.png


objA和objB到堆对象的引用已经没有了,但是ReferenceCountingGC对象内部还存在着循环引用,我们在图中也可以看到。即便如此,JVM还是把这两个对象当成垃圾进行了回收。具体的GC日志如下:


image.png

由GC日志可知发生了两次GC,由11390K -> 514K,即对两个对象都进行了回收,也从侧面说明JVM的垃圾收集器不是采用的引用计数的算法来进行垃圾回收的。


  2.可达性分析算法


  此算法的基本思想就是选取一系列GCRoots对象作为起点,开始向下遍历搜索其他相关的对象,搜索所走过的路径成为引用链,遍历完成后,如果一个对象到GCRoots对象没有任何引用链,则证明此对象是不可用的,可以被当做垃圾进行回收。


  那么问题又来了,如何选取GCRoots对象呢?在Java语言中,可以作为GCRoots的对象包括下面几种:


    1. 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。

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

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

    4. 本地方法栈中JNI(Native方法)引用的对象。


  下面给出一个GCRoots的例子,如下图,为GCRoots的引用链。


image.png


由图可知,obj8、obj9、obj10都没有到GCRoots对象的引用链,即便obj9和obj10之间有引用链,他们还是会被当成垃圾处理,可以进行回收。


四、对象的内存布局


  Java中我们提到最多的应该就是对象,但是我们真的了解对象吗,对象在内存中的存储布局如何?对象的内存布局如下图所示


image.png


几点说明:1.Mark Word部分数据的长度在32位和64位虚拟机(未开启压缩指针)中分别为32bit和64bit。然后对象需要存储的运行时数据其实已经超过了32位、64位Bitmap结构所能记录的限度,但是对象头信息是与对象自身定义的数据无关的外存储成本,Mark Word一般被设计为非固定的数据结构,以便存储更多的数据信息和复用自己的存储空间。2.类型指针,即指向它的类元数据的指针,用于判断对象属于哪个类的实例。3.实例数据存储的是真正有效数据,如各种字段内容,各字段的分配策略为


longs/doubles、ints、shorts/chars、bytes/boolean、oops(ordinary object

pointers),相同宽度的字段总是被分配到一起,便于之后取数据。父类定义的变量会出现在子类前面。3.对齐填充部分仅仅起到占位符的作用,并非必须。


  说完对象的内存布局,现在来说说对象的引用,当我们在堆上创建一个对象实例后,如何对该对象进行操作呢?好比一个电视机,我如何操作电视机来收看不同的电视节目,显然我们需要使用到遥控,而虚拟机中就是使用到引用,即虚拟机栈中的reference类型数据来操作堆上的对象。现在主流的访问方式有两种:


  1. 使用句柄访问对象。即reference中存储的是对象句柄的地址,而句柄中包含了对象示例数据与类型数据的具体地址信息,相当于二级指针。


  2. 直接指针访问对象。即reference中存储的就是对象地址,相当于一级指针。


  两种方式有各自的优缺点。当垃圾回收移动对象时,对于方式一而言,reference中存储的地址是稳定的地址,不需要修改,仅需要修改对象句柄的地址;而对于方式二,则需要修改reference中存储的地址。从访问效率上看,方式二优于方式一,因为方式二只进行了一次指针定位,节省了时间开销,而这也是HotSpot采用的实现方式。下图是句柄访问与指针访问的示意图。


image.png


image.png


五、对象的引用


  前面所谈到的检测垃圾对象的两种算法都是基于对象引用。在Java语言中,将引用分为强引用、软引用、弱引用、虚引用四种类型。引用强度依次减弱。具体如下图所示


image.png


对于可达性分析算法而言,未到达的对象并非是“非死不可”的,若要宣判一个对象死亡,至少需要经历两次标记阶段。1. 如果对象在进行可达性分析后发现没有与GCRoots相连的引用链,则该对象被第一次标记并进行一次筛选,筛选条件为是否有必要执行该对象的finalize方法,若对象没有覆盖finalize方法或者该finalize方法是否已经被虚拟机执行过了,则均视作不必要执行该对象的finalize方法,即该对象将会被回收。反之,若对象覆盖了finalize方法并且该finalize方法并没有被执行过,那么,这个对象会被放置在一个叫F-Queue的队列中,之后会由虚拟机自动建立的、优先级低的Finalizer线程去执行,而虚拟机不必要等待该线程执行结束,即虚拟机只负责建立线程,其他的事情交给此线程去处理。2.对F-Queue中对象进行第二次标记,如果对象在finalize方法中拯救了自己,即关联上了GCRoots引用链,如把this关键字赋值给其他变量,那么在第二次标记的时候该对象将从“即将回收”的集合中移除,如果对象还是没有拯救自己,那就会被回收。如下代码演示了一个对象如何在finalize方法中拯救了自己,然而,它只能拯救自己一次,第二次就被回收了。具体代码如下


/*
 * 此代码演示了两点:
 * 1.对象可以再被GC时自我拯救
 * 2.这种自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次
 * */
public class FinalizeEscapeGC {
    public String name;
    public static FinalizeEscapeGC SAVE_HOOK = null;
    public FinalizeEscapeGC(String name) {
        this.name = name;
    }
    public void isAlive() {
        System.out.println("yes, i am still alive :)");
    }
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed!");
        System.out.println(this);
        FinalizeEscapeGC.SAVE_HOOK = this;
    }
    @Override
    public String toString() {
        return name;
    }
    public static void main(String[] args) throws InterruptedException {
        SAVE_HOOK = new FinalizeEscapeGC("leesf");
        System.out.println(SAVE_HOOK);
        // 对象第一次拯救自己
        SAVE_HOOK = null;
        System.out.println(SAVE_HOOK);
        System.gc();
        // 因为finalize方法优先级很低,所以暂停0.5秒以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no, i am dead : (");
        }
        // 下面这段代码与上面的完全相同,但是这一次自救却失败了
        // 一个对象的finalize方法只会被调用一次
        SAVE_HOOK = null;
        System.gc();
        // 因为finalize方法优先级很低,所以暂停0.5秒以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no, i am dead : (");
        }
    }
}


运行结果如下:


  leesf

  null

  finalize method executed!

  leesf

  yes, i am still alive :)

  no, i am dead : (

  由结果可知,该对象拯救了自己一次,第二次没有拯救成功,因为对象的finalize方法最多被虚拟机调用一次。此外,从结果我们可以得知,一个堆对象的this(放在局部变量表中的第一项)引用会永远存在,在方法体内可以将this引用赋值给其他变量,这样堆中对象就可以被其他变量所引用,即不会被回收。


六、方法区的垃圾回收


  方法区的垃圾回收主要回收两部分内容:1. 废弃常量。2. 无用的类。既然进行垃圾回收,就需要判断哪些是废弃常量,哪些是无用的类。


  如何判断废弃常量呢?以字面量回收为例,如果一个字符串“abc”已经进入常量池,但是当前系统没有任何一个String对象引用了叫做“abc”的字面量,那么,如果发生垃圾回收并且有必要时,“abc”就会被系统移出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。


  如何判断无用的类呢?需要满足以下三个条件


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

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

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


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


七、垃圾收集算法


  垃圾收集的主要算法有如下几种:

    1. 标记 - 清除算法

    2. 复制算法

    3. 标记 - 整理算法

    4. 分代收集算法

  7.1 标记 - 清除算法


  首先标记出所有需要回收的对象,使用可达性分析算法判断一个对象是否为可回收,在标记完成后统一回收所有被标记的对象。下图是算法具体的一次执行过程后的结果对比。


image.png


说明:1.效率问题,标记和清除两个阶段的效率都不高。2.空间问题,标记清除后会产生大量不连续的内存碎片,以后需要给大对象分配内存时,会提前触发一次垃圾回收动作。


  7.2 复制算法


  将内存分为两等块,每次使用其中一块。当这一块内存用完后,就将还存活的对象复制到另外一个块上面,然后再把已经使用过的内存空间一次清理掉。图是算法具体的一次执行过程后的结果对比。


image.png


说明:1.无内存碎片问题。2.可用内存缩小为原来的一半。 3.当存活的对象数量很多时,复制的效率很慢。


  7.3 标记 - 整理算法


  标记过程还是和标记 - 清除算法一样,之后让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存,标记 - 整理算法示意图如下


image.png


说明:1.无需考虑内存碎片问题。

  7.4 分代收集算法

  把堆分为新生代和老年代,然后根据各年代的特点选择最合适的回收算法。在新生代基本上都是朝生暮死的,生存时间很短暂,因此可以采拥标记 - 复制算法,只需要复制少量的对象就可以完成收集。而老年代中的对象存活率高,也没有额外的空间进行分配担保,因此必须使用标记 - 整理或者标记 - 清除算法进行回收。

目录
相关文章
|
1月前
|
算法 Java
JVM垃圾回收机制
JVM垃圾回收机制
15 0
|
2天前
|
安全 算法 Java
深入浅出JVM(十三)之垃圾回收算法细节
深入浅出JVM(十三)之垃圾回收算法细节
|
2天前
|
存储 算法 Java
深入浅出JVM(十二)之垃圾回收算法
深入浅出JVM(十二)之垃圾回收算法
|
2天前
|
算法 Java PHP
JVM 的垃圾回收机制以及垃圾回收算法的详解
JVM 的垃圾回收机制以及垃圾回收算法的详解
9 0
|
3天前
|
监控 算法 安全
JVM工作原理与实战(三十九):G1垃圾回收器原理
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了G1垃圾回收器执行流程、年轻代回收原理、卡表(Card Table)、记忆集的生成流程、年轻代回收的详细步骤、混合回收的步骤、初始标记、并发标记、SATB、转移等内容。
13 0
|
4天前
|
存储 监控 算法
JVM工作原理与实战(二十七):堆的垃圾回收-G1垃圾回收器
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了G1垃圾回收器、G1垃圾回收器的回收方式、G1垃圾回收器执行流程、垃圾回收器的选择等内容。
|
4天前
|
机器学习/深度学习 监控 算法
JVM工作原理与实战(二十六):堆的垃圾回收-垃圾回收器
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了垃圾回收器、Serial垃圾回收器、SerialOld垃圾回收器、ParNew垃圾回收器、CMS垃圾回收器、Parallel Scavenge垃圾回收器、Parallel Old垃圾回收器等内容。
11 0
|
4天前
|
Arthas 监控 算法
JVM工作原理与实战(二十五):堆的垃圾回收-垃圾回收算法
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了垃圾回收算法评价标准、标记清除算法、复制算法、标记整理算法、分代垃圾回收算法等内容。
18 0
JVM工作原理与实战(二十五):堆的垃圾回收-垃圾回收算法
|
4天前
|
存储 缓存 监控
JVM工作原理与实战(二十四):堆的垃圾回收-对象引用
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了强引用、软引用、弱引用、虚引用、终结器引用等内容。
11 0
|
4天前
|
监控 算法 安全
JVM工作原理与实战(二十三):堆的垃圾回收-引用计数法和可达性分析法
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了判断堆上的对象是否可以回收的方法(引用计数法、可达性分析法)、查看垃圾回收日志等内容。
12 0