• 垃圾回收机制——GC详讲


    垃圾回收

    众所周知,程序在运行过程中总是需要申请内存空间,内存空间又不是无限的,为了保持内存空间的持续使用,我们就需要知道如何识别出哪些是垃圾,识别出来后又需要怎么处理这些垃圾?这里就需要垃圾回收机制,也被称为 GC ( 下文通过 GC 代指垃圾回收)

    垃圾回收的主要场所

    Java 进程在启动的时候会向操作系统申请内存空间,并划分为堆,栈,程序计数器,方法区


    1. 这片区域主要存储局部变量,而局部变量都拥有自己的作用域,局部变量出了作用域就会自己被回收,所以这里并不需要“过多关心”
    2. 方法区
      方法区主要用于类加载,存储类对象,伴随了整个 Java 进程的进行
    3. 程序计数器
      由于每一个线程都有自己的一片空间固定的程序计数器,如果一个线程销毁,那么程序计数器也会被销毁

    4. 这里是对象创建的内存空间,存在着大量对象的产生和销毁,因此,此处就是垃圾回收的主要发生区域

    知道了 GC 主要发生在堆上了, 应该怎么识别一个对象是垃圾? 在我们看来, 如果一个变量全部空间都不需要用到了, 那自然是垃圾, 而这个对象空间还有一部分需要用, 那肯定不能识别为垃圾, 所以, 垃圾回收机制的基本单位是对象, 而接下来完全用不到的对象就是 GC 的回收目标

    如何判断这个对象完全用不到了

    这篇主要介绍两个方法:可达性分析引用计数

    引用计数法

    首先是引用计数法,这也是 Python 中使用的垃圾回收机制
    我们来定义一下何为 垃圾对象,如果说我们没有某个对象的引用,我们就用不到这个对象了,既然用不到这个对象,那视为垃圾,没有问题吧

    所以我们可以记录引用是某个对象(我们记为 A)的次数, 如果这个引用是局部变量, 那么出了作用域, 这个对象A的引用次数就减一, 而如果这个引用是成员变量, 那么当这个引用对应的对象被销毁了, 对象A的引用次数才减一, 当对象A的引用次数为 0 的时候, 也就达成了上述情况, 这个对象就可以视为垃圾

    举个例子🌰, 我们创建了一个 Student 对象, 用 a 接收这个对象引用, 再创建一个引用 b 再指向这个 Student 对象, 那么此时引用次数就为 2, 但是如果让 a = b = null; 那么这个 Student 也就没有引用指向它了, 就可以被视为 GC 的回收对象

    public class Main {
        public static void main(String[] args) {
            Student a = new Student();
            Student b = a;
        }
    }
    
    class Student {
        int v;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    如下图
    在这里插入图片描述

    缺点

    1. 在多线程的使用环境下,会涉及到多线程写的操作,所以需要考虑线程安全问题
    2. 可能涉及循环引用问题 👇

    接下来讲一下循环引用问题, 如下代码,Test 类中有个 Test 类型的引用变量,然后创建两个实例,让a,b分别指向它们(假设 a 引用指向的地址为 0x100, b 引用指向的地址为 0x200)

    class Test {
    	Test ptr;
    }
    public class Main {
    	public static void main(String[] args) {
    		Test a = new Test(); // 假设引用为 0x100
    		Test b = new Test(); // 假设引用为 0x200
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    好!执行完这个代码后,引用数的情况是这样的, 两个对象各有一个引用,所以对象的引用次数都为 1,好理解
    在这里插入图片描述
    然后再执行下述代码

    	a.ptr = b;
        b.ptr = a;
    
    • 1
    • 2

    这时候的情况是这样的,两个对象各增加了一个引用,所以引用计数也各自加一,于是两个对象的引用计数总共就为 2

    在这里插入图片描述
    接着我们直接销毁引用a, b,让 a = b = null,此时由于引用数量减少,两个对象引用次数都减一
    于是如下图,最终这两个对象都存着彼此的引用,但是实际上这两个对象的地址已经无法获取了,但是由于引用次数不为 0,所以无法视为垃圾,只能占着空位置却用不了,这就是引用计数法中的重大缺陷
    在这里插入图片描述

    可达性分析

    这也是 JVM 在使用的算法
    以一系列「起点」出发,能够直接或间接访问到的对象标记为「可达」, 否则标记为“不可达”,而被标记为“不可达”的对象即会被认为是垃圾。而这些起点就被定义为 GCRoot 。换个说法,只有能够和 GCRoot 直接或间接访问的对象才会被认为是存活对象。

    在这里插入图片描述

    GCRoot

    那么 GCRoot 是怎么被定义的 ?
    GCRoot 包括但不局限于以下几种

    1. 局部变量表中的局部变量
      简单来说就是,栈中都所有局部变量都可以是 GCRoot
    2. 常量池中的对象
    3. 方法区静态引用类型的成员

    回收垃圾

    那么现在能够识别出垃圾了还不够,清理垃圾也很讲究,一起康康垃圾是怎么清理的

    首先介绍一种最容易想到的算法

    标记-清楚

    这个算法简单暴力好理解,就是哪里是垃圾,就释放哪里的内存
    在这里插入图片描述

    缺点就是会出现垃圾碎片的问题,如上图,由于我们在申请内存空间的时候,往往需要连续的内存空间,而这种一小片又不连续的空间,实际上很难利用起来,所以我们需要进化一下算法,起码能解决垃圾碎片的问题

    复制算法

    主要思想:先将内存空间一分为二,一半用来使用,一半是空着的,当我们需要清理垃圾的时候,就将不是垃圾的对象全复制到空着的那一半空间,刚刚那一半全部释放

    在这里插入图片描述
    这个算法较为高效,并且解决了上面的问题,但是缺点显而易见,空间利用率太低,一半空间放着不用

    于是我们就又想创造出一种能够解决上述弊端的算法

    标记-整理

    主要思想:直接释放垃圾内存,随后让存活的对象都往一个方向移动(搬运)
    这其实和 ArrayList 中的删除元素很像,一个删除,后面的元素都往前挪
    如下图

    在这里插入图片描述这个算法既解决了垃圾碎片问题,又解决了空间利用率低的问题,但是时间消耗也比较大。

    虽然 复制算法 和 标记-整理 各有各的缺点,但是如若将这两者应用在不同的场景中够扬长避短——于是分代回收算法出现

    分代回收

    首先,我们将内存区域分成两片,一边叫新生代,另一边叫老年代

    并且引入一个属性:年龄
    我们将年龄定义为:经历过多少次 GC,且还存活的轮次,例如,经历 3 次 GC,还健在的对象年龄为 3

    新生代再分为伊甸区和两个幸存区,然后每次新建立的对象都会放在伊甸区,并且根据经验规律,大部分对象连一轮 GC 都撑不过,所以每次 GC 后,伊甸区只会剩下小部分对象

    然后将这部分对象通过复制算法移动到其中一个幸存区中,再释放伊甸区的对象(注意:幸存区有两个,可以方便复制算法在这两个区域运行)

    在这里插入图片描述
    移动到幸存区后,还会经历多次 GC,每次 GC 运行后,幸存的对象又会通过复制算法移动到另一个幸存区,再将之前的幸存区释放

    随后,将幸存区中满足年龄要求的对象会通过复制算法移动到老年代

    能够到达老年区也就足以说明这些对象在短时间内还死不了,所以老年代中会进行频率更低的 GC,如果老年代中发现了垃圾,就会通过标记-整理算法清除。(如果对象空间很大,则会被直接移动到老年代中)

    至此,我们来总结一下
    虽然复制算法需要一定的空闲空间,但是由于经验规律,刚创建完的对象大部分在第一轮就会死去,所以幸存区的空间小点就能够满足需求,于是我们将这些对象通过复制算法移动到幸存区,然后在这两片幸存区中,每次 GC 都会将幸存对象通过复制算法移动到另一个幸存区,当对象年龄够大且存活时,再移动到老年代。在老年代中,GC 仍以较低频率运转,如果老年代中出现垃圾,就使用标记-整理算法进行处理

  • 相关阅读:
    Excel VSTO开发4 -其他事件
    Vue项目中,el-image实现按钮触发大图预览模式
    TCP ZeroWindow 问题
    基于萤火虫算法优化支持向量机实现中文语音情感识别附matlab代码
    Qt QSS QSlider样式
    Spring Boot常见面试题
    好奇宝宝看 Docker 底层原理(上)
    解答:EasyDSS视频点播时音频是否可以设置为默认开启?
    Django学习日志07
    Nacos在Ubuntu下启动失败 |Debug日志 startup.sh: 130: startup.sh: [[: not found
  • 原文地址:https://blog.csdn.net/weixin_63519461/article/details/127795857