垃圾回收机制——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. 可能涉及循环引用问题 👇

接下来讲一下循环引用问题, 如下代码,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,好理解
在这里插入图片描述
然后再执行下述代码

	a.ptr = b;
    b.ptr = a;

这时候的情况是这样的,两个对象各增加了一个引用,所以引用计数也各自加一,于是两个对象的引用计数总共就为 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 仍以较低频率运转,如果老年代中出现垃圾,就使用标记-整理算法进行处理

  • 6
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

答辣喇叭

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值