JVM 垃圾回收机制(GC)

观前须知

如果你是第一次点击这篇博客的,你需要先看我的这篇博客:
JVM 垃圾回收机制(GC),再过来看这篇博客。

垃圾回收机制(GC),如今并不是 Java 才有的,很多语言都有这个机制。
但是,GC 的确是 Java 带头引起了潮流。

垃圾回收机制(GC),至于它的叫法,我有时会使用 GC 来称呼,有时用 垃圾回收 来称呼,有时候就是全称了,你知道它们都表示 垃圾回收机制 就可以了。

下面的博客,我们会介绍 垃圾回收机制 – 释放垃圾 的原理。

1. 第二步:释放垃圾

垃圾回收的工作过程,分为两个大步骤

  1. 找到 垃圾(不再使用的对象)
  2. 释放 垃圾(对应的内存释放掉)

我们有两种方案,来找到垃圾

  1. 引入计数(Python,PHP 采用的方案)
  2. 可达性分析(Java 采用了这个方案,重点了解)

目前,垃圾找到了,那么我们该怎么释放这些垃圾呢?

释放垃圾,我们有四种方案

  1. 标记-清除
  2. 复制算法
  3. 标记-整理
  4. 分代回收Java 采用的方案,重点了解

接下来,我们分别进行介绍。

1.1 标记-清除

标记-清除:把垃圾对象的内存直接进行释放

这样的做法,会产生一个问题:内存碎片

缺点:内存碎片

假如,我们有这么一块内存:
在这里插入图片描述
当前,2,4,6,8 存储的对象,是垃圾,需要回收。

采用 标记-清除 的做法,释放垃圾之后,这块内存就长这样了:
在这里插入图片描述
虽然,垃圾被回收了,内存空间空闲出来了,但是,这些空间是不连续的!!!
这样的内存空间,就是一个典型的内存碎片问题

我们申请内存,都是申请连续的内存空间(全是空闲的)
比如申请 1M 的内存空间,必须是连续的,不能是多个部分拼接到一起。

如果内存碎片非常多,虽然总的空闲空间很大,但是但凡想申请一个稍微大一点的内存,都会失败。
总的空闲空间是 4G,此时,内存碎片非常多,就会使得申请 1G 空间都可能失败

就比如:
你想买一台 IPhone17 pro max,你总的钱是够的,但是,你的钱是 微信 + 支付宝 + 多张银行卡 凑够的钱。
此时,你想一次性支付,购买 IPhone17 pro max,是不行的。
这种情况,仅和上述 内存碎片 的场景,进行关联举例。

当然,现实生活中,你可以通过很多种方式解决,借钱啊,分期付款啊,都行。

总结:

标记-清除,直接释放垃圾,简单粗暴,但是会产生 内存碎片 的问题

1.2 复制算法

复制算法的思路:
申请的内存空间,存储对象的时候,只会使用一半
需要进行垃圾释放时,把不是垃圾的对象,拷贝(复制)到另一半内存空间,然后把这一侧的空间,全部释放掉

用图片来演示一下:
申请的内存空间,存储对象
在这里插入图片描述
经过一轮 GC,发现2,4,6 存储的对象是垃圾,需要释放
在这里插入图片描述
右边的内存空间,存放新的对象
在这里插入图片描述
又经过一轮 GC,发现 100,300,500 存储的对象是垃圾,需要释放
在这里插入图片描述
之后,就是按照这样的方式,释放垃圾

此时,就可以确保,空闲的内存,不会出现 内存碎片 的问题了。

缺点 & 总结:

复制算法,虽然解决了 内存碎片 的问题,但是,这种释放垃圾的方式,它又有新的缺点。

缺点:

  1. 内存的空间利用率,是很低的浪费一半的内存空间,2G 变 1G)
  2. 一旦不是垃圾的对象(还需要使用的对象)很多,复制的成本,就会很高尤其是一个内存空间占用很大的对象

1.3 标记-整理

标记-整理的思路:
发现垃圾后,把不是垃圾的对象(还需要使用的对象),统统往前移动
移动完成,把剩余的对象,全部删除

我们仍然以图片的方式,进行演示:
在这里插入图片描述

这个搬运过程,类似于数据结构中的 顺序表 的数据搬运,后面的数据,覆盖掉前面的数据。

缺点 & 总结

标记-整理,它的优点是:解决内存碎片问题 & 保证了内存的利用率

但是,它仍然是有缺点的:
内存搬运数据的操作,开销是挺大的
一旦不是垃圾的对象(还需要使用的对象)很多,搬运的成本,就很高,尤其是这样的对象,是一个很大的对象。

所以,标记-整理,只是解决了 内存空间利用率 的问题没有解决复制成本的问题

1.4 分代回收(重点)

上述三种方案,是释放垃圾的基本思想,那么,分代回收,是结合了 2,3 方案,扬长避短

分代 中的 “代”指的是 “对象的年龄”,这里的年龄,指的是 GC的轮次

某一个对象,经历一轮 GC 的可达性分析之后,是 “可达” 的,不是垃圾,此时,这个对象的年龄就会 + 1
一个对象的年龄,初始就是 0

新生代 与 老年代

分代回收,根据对象年龄的大小,把 堆 上的内存空间,分为两个部分

  1. 新生代区年龄小的对象放这里)
  2. 老年代区年龄大的对象放这里)
    在这里插入图片描述

因为不同年龄的对象,特点是不同的
如果某一个对象的年龄,比较大,说明,这个对象经过多轮 GC 的可达性分析之后,仍然在使用,之后,大概率还是会继续存在的。
如果某一个对象的年龄,比较小,说明,这个对象有可能快速挂掉

上述特点分析,我们可以举一个生活中的例子:
老虎,一般是独居动物,狮子,是群居动物。
老虎幼仔刚生出来的时候,是需要老虎妈妈亲自带大的,幼仔一般是很脆弱的,无法独立觅食,容易饿死。(对象年龄小)
一个成年老虎,能够一直存活说明它是身强体壮,猎食能力优秀的,之后,它还是继续存活。(对象年龄大)

再举一个例子:
C语言,已经存在 50年,但是,目前仍是计算机入门语言的首选,说明,C语言是有它的特点之处的。(对象年龄大)
并且,C语言,未来 50年,还会继续持续存在,仍是计算机入门语言的首选
和 C语言 同一个时期诞生出来的语言,都死的差不多了。(对象年龄小)

总结一句话就是一个东西,要死早死了,之所以一直没死,肯定是这个东西,有特殊之后,有用处,之后,大概率还是会继续存在的

经验规律总结:
新生代区:存放的是 新生代的对象(对象年龄小),可能很快速就挂掉了
老年代区:存放的是 老年代的对象(对象年龄大),可能会持续存在

GC 根据对象年龄,做出调整

针对不同年龄的对象,采取不同的策略

根据上述经验规律,GC 就会做出调整:
老年代区的对象,GC 的频次会降低
新生代区的对象,GC 的频次会升高

老年代区的对象,之后会持续存在,就没必要对它进行过多的 GC可达性分析了,可以省一点资源
新生代区的对象可能会快速挂掉,就需要对他们,采取更高频次的 GC可达性分析

新生代:伊甸区 & 幸存区

新生代区域,根据年龄,又做出了不同的划分,分为:

  1. 伊甸区(1块大区域)
  2. 幸存区(2块相同的小区域)

具体的分块是这样的:
在这里插入图片描述
伊甸区和幸存区的内存占比为:8 : 1 : 1

至于,为什么名字是这样命名的,主要是来自于 《圣经》。
你们不用了解,记住这两块区域叫什么名字就行。

分代回收的思路(⭐️重点)

伊甸区:存放新创建的对象
绝大部分的 新对象,都活不过第一轮的 GC。
可能 10个新对象,第一轮 GC 之后,只剩下一个了(具体代码,具体分析,这里随口一说)。

分代回收的具体思路:

  1. 新对象,撑过第一轮 GC 之后,就会使用 复制算法,复制到 幸存区。(伊甸区 => 幸存区:复制算法)

    所以,幸存区的空间 比 伊甸区的空间 小很多。

    伊甸区 => 幸存区 使用 复制算法,这里的复制的对象的规模,是很少的,开销是比较小的。

  2. 幸存区中的对象,也要经历 GC 的扫描(即可达性分析),每一轮,也会消灭一大部分的对象,剩余的对象再次通过 复制算法,复制到 另一个幸存区

    由于 幸存区 占用的空间,是比较少的,虽然这里存放的对象,也比较少,但是,这样的空间利用率,还是可以接受的

    幸存区中的对象会经过,多次 GC扫描,释放垃圾时,使用 复制算法,在两个幸存区中不断移动

  3. 幸存区中的某一个对象,经历了多次 GC扫描 之后,多次复制,都存活下来了,此时,这个对象的年龄就大了,就会使用 复制算法 复制到 老年代区

  4. 处于 老年代区 的对象,GC 扫描 后,发现垃圾,会使用 标记-整理 的方式,释放垃圾

老年代区的对象,数量本身就是很少的,采用 标记-整理 的方式,释放垃圾,搬运对象的成本,就小了很多

疑问:为什么新生代区用 复制算法,老年代区用 标记-整理?

我们重新看一下,堆 上的内存空间,是被分为这样的:
在这里插入图片描述
复制算法,我们是将内存空间划为两份:
在这里插入图片描述
一半用来存对象一半用来存储 复制 之后的对象
符合新生代区中,伊甸区和幸存区的划分方式

标记-整理,我们仅使用一块内存空间
在这里插入图片描述
符合老年代区中,仅划分 一块的内存区域 的方式。

举例理解 分代回收 机制

分代回收 是将 复制算法和标记-整理 两种基本的垃圾释放思想,结合到一起,大家可能不好直接就理解了,但是,我们举一个生活中的例子:
面试进公司,公司年度考核

在这里插入图片描述
伊甸区:公司会收到大量的简历(新对象)
绝大部分会被筛选掉,只有少数能够进入到笔试面试环节

幸存区:进入笔试面试
笔试:基本是 一轮
面试:基本都是 多轮 面试,少则 2-3 轮,多则 6-7 轮。

如果经过多轮面试,你都通过了,拿到 offer,就正式进入到老年代区,成为正式员工

老年代区:进入公司,半年一度的绩效考核,一年一度的年度考核(周期很长)
根据考核结果,进行末尾淘汰。

特殊情况

分代回收也有一种很特殊的情况:如果某个对象特别大,会直接进入老年代区,不会进入伊甸区和幸存区
原因是:复制成本高

这就相当于,有人可以不同面试,直接进公司。
比如:这个公司,是他爸爸开的。

所以,这个特殊情况,出现的概率,很小很小。

分代回收总结(★)

我们画一张草图,总结一下,分代回收的工作方式:
在这里插入图片描述
对象会在伊甸区,幸存区中,经历多次 GC 扫描,垃圾的释放,使用 复制算法(复制成本小)
老年代区的对象GC扫描的频次,会少很多垃圾的释放,使用 标记-整理 (搬运对象成本小)

新生代区 中的对象,大多数的对象会快速消亡,使得每次 复制的对象少,复制的开销少,可控
老年代区 中的对象数量很少 且 大部分的生命周期较长,使得每次需要搬运的对象少,搬运的开销少,可控

1.5 总结

释放垃圾,我们有四种方案:

  1. 标记-清除:简单粗暴,直接释放内存空间,但是会产生 内存碎片 的问题
  2. 复制算法:内存的空间利用率很低,复制的成本不可控
  3. 标记-整理:内存搬运数据的操作,开销不可控
  4. 分代回收:把 复制算法和标记-整理 两种基本的垃圾释放思想,结合到一起,扬长避短,使得 复制的成本和搬运对象的成本 都是可控

分代回收,是 JVM 释放对象,采用的综合性策略

看到这里,你可以点击这篇博客:JVM 垃圾回收机制(GC),回去看完关于 JVM垃圾回收机制 的介绍。

最后,如果这篇博客能帮到你的,请你点点赞,有写错了,写的不好的,欢迎评论指出,谢谢!

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐