JVM 垃圾回收机制(GC)-- 释放垃圾

JVM 垃圾回收机制(GC)

文章目录

  • [JVM 垃圾回收机制(GC)](#JVM 垃圾回收机制(GC))
  • 观前须知
  • [1. 第二步:释放垃圾](#1. 第二步:释放垃圾)
    • [1.1 标记-清除](#1.1 标记-清除)
    • [1.2 复制算法](#1.2 复制算法)
      • [缺点 & 总结:](#缺点 & 总结:)
    • [1.3 标记-整理](#1.3 标记-整理)
      • [缺点 & 总结](#缺点 & 总结)
    • [1.4 分代回收(重点)](#1.4 分代回收(重点))
      • [新生代 与 老年代](#新生代 与 老年代)
      • [GC 根据对象年龄,做出调整](#GC 根据对象年龄,做出调整)
      • [新生代:伊甸区 & 幸存区](#新生代:伊甸区 & 幸存区)
      • 分代回收的思路(:star:重点)
      • [疑问:为什么新生代区用 复制算法,老年代区用 标记-整理?](#疑问:为什么新生代区用 复制算法,老年代区用 标记-整理?)
      • [举例理解 分代回收 机制](#举例理解 分代回收 机制)
      • 特殊情况
      • 分代回收总结(★)
    • [1.5 总结](#1.5 总结)

观前须知

如果你是第一次点击这篇博客的,你需要先看我的这篇博客:
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垃圾回收机制 的介绍。

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

相关推荐
shengli7221 小时前
Python Lambda(匿名函数):简洁之道
jvm·数据库·python
weixin_445402302 小时前
如何为开源Python项目做贡献?
jvm·数据库·python
2301_821369612 小时前
数据分析与科学计算
jvm·数据库·python
m0_549416662 小时前
自动化与脚本
jvm·数据库·python
hgz07102 小时前
JVM定义
jvm
Hgfdsaqwr3 小时前
使用PyTorch构建你的第一个神经网络
jvm·数据库·python
2301_788662403 小时前
用Python批量处理Excel和CSV文件
jvm·数据库·python
程序员敲代码吗3 小时前
使用Plotly创建交互式图表
jvm·数据库·python
Hgfdsaqwr5 小时前
实战:用Python开发一个简单的区块链
jvm·数据库·python