JVM 垃圾回收机制(GC)

JVM 垃圾回收机制(GC)

文章目录

  • [JVM 垃圾回收机制(GC)](#JVM 垃圾回收机制(GC))
  • 观前须知
  • [1. 垃圾回收机制(GC)的功能](#1. 垃圾回收机制(GC)的功能)
  • [2. 垃圾回收的工作过程](#2. 垃圾回收的工作过程)
  • [3. 第一步:找到垃圾](#3. 第一步:找到垃圾)
  • [4. 第二步:释放垃圾](#4. 第二步:释放垃圾)
  • [5. 垃圾收集器](#5. 垃圾收集器)
  • [5. 总结](#5. 总结)

观前须知

如果你是第一次点击这篇博客的,你需要先看我的这篇博客:
JVM初识 & JVM 内存区域划分

,再过来看这篇博客。

垃圾回收机制(Garbage Collection, 简称GC),如今并不是 Java 才有的,很多语言都有这个机制。

但是,GC 的确是 Java 带头引起了潮流。

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

1. 垃圾回收机制(GC)的功能

垃圾回收机制(Garbage Collection, 简称GC)是自动管理内存的一种技术,主要用于识别和回收不再使用的对象,从而释放内存空间,避免内存泄漏

垃圾回收机制(Garbage Collection, 简称GC),是 Java 中,释放内存的手段。

学过 C语言/C++ ,我们知道,申请内存之后,需要手动调用 free方法 ,释放内存,否则,就会出现内存泄露
free方法 ,程序员不一定会时时刻刻记住这件事 ,总会不小心忘记了,或者写的代码,没有执行到 free方法

例如:

所以,手动释放内存太麻烦,太容易出错Java 引入垃圾回收机制,进行自动释放内存

JVM 会自动识别出,哪个内存,是不是后续不再使用了,如果不适用了,就会自动释放不再使用的内存,避免了内存泄露。

Java垃圾回收 ,相当于开车时的 自动挡
C++没有垃圾回收 ,相当于开车时的 手动挡

开过车的都知道,开过自动挡,就不想再开手动挡了,至少大部分人是这样的。

1.1 大杂谈

由于 垃圾回收,很方便,Java 之后的各种编程语言,都引入了 垃圾回收机制。

Python,PHP,Rust,Go......等等,都引入了垃圾回收。

那么,问题来了,为什么 C++ 还是没有引入垃圾回收机制?

为什么 C++ 还是没有引入垃圾回收机制?

C语言,已经躺平了,引入 GC,不太可能了。

但是,C++还是在积极的进化当中。

不引入 GC 的原因,就是:GC,会使 C++ 程序运行的效率降低。C++ 这边,不愿承担这样的代价。

GC ,由于它会每隔一段时间,就会扫描一次程序,判断是否有 需要回收的内存,会导致程序的运行效率降低

C++的设计目标,有两个:

  1. 和 C语言 兼容
  2. 极致的性能

GC,不符合第二个目标,所以 C++,就没有引入 GC 了。

C++ 既然没有为了解决 内存泄露 问题,引入 GC,但是,C++ 为了解决 内存泄露 问题,也是做出了策略的,引入了一个机制,叫做 智能指针,它能够一定程度缓解 内存泄露 的问题。

但是,和 GC 比起来,易用性上,简直就是弟弟中的弟弟。

STW问题

GC中,还有一个臭名昭著的问题,叫做:STW问题
STW(stop the world)问题 :触发了大规模的 GC ,可能就会因为 GC ,使得其他业务代码不得不暂停下来,等待 GC 结束,再继续执行后续代码

这样的场景,在生活中,有一个很典型的例子:

你放假在家,用电脑打游戏,此时,你妈妈拿着扫把进来了,她会让你起来,空出那个位置,让她打扫垃圾(大规模的 GC),你就不得不起身,双手离开键盘,停止玩游戏,腾出地方(暂停业务代码)。

如果你不起来,那么你妈妈的扫把,就不是扫地了,而是打你了。

GC 发展多年,这个 STW问题,也改进了很多。

在 Java17(JDK17)及以上版本,可以做到 STW大部分情况下,占用的时间 <1ms

Rust:既不用操心内存泄露,又不影响性能

既然 GC,会消耗很多的性能,是否有办法,可以让程序员,既不用操心 内存泄露,又不影响程序性能呢?

答:新的语言 -> Rust

Rust 通过严格的编译器检查,来对代码中的内存问题,进行校验

编译过程中进行检查,发现问题,直接编译报错。

对于程序运行,没有任何性能上的影响。

上述,看似完美,但是,有得就有舍,天下没有白吃的午餐。

完美的解决方案,换来的代价就是:Rust 的语法,特别的复杂,比 C++ 都复杂。

各语言,程序运行效率

由于 Java 有 GC机制,论性能,确实不如 C++,但是 Java 这么多年发展下来,性能也没比 C++ 差多少。

比如,运行同一段程序:

用 C++实现,是 1 个单位时间

用 Java实现,是 1.5~2 个单位时间

用 Go实现,是 4~5 个单位时间

用 Python实现,是 100 个单位时间

看到运行效率,你会奇怪,那为什么现在流行一种说法: Go将要取代 Java?

Go:由 Google 设计,语法比较简单 ,旨在应对多核处理器 和网络服务的时代需求,其哲学是 "简单" 和 "高效"。总结来说:轻量化
Java通过 JVM 实现跨平台能力,一次编译,到处运行,拥有庞大且成熟的生态系统,涵盖了各种开发工具、库和框架等。但是,它不够轻量化,比较笨重。

那为什么 Python,网上都在吹?

主要原因就是:Python 开发效率高。

为什么 Python 广泛应用于 AI领域?

AI 领域,真正的扛把子,是 C++
AI 领域 ,它的核心逻辑,密集计算的逻辑,都是 C++ 来完成的 ,甚至是 C++ 调用 GPU(显卡)来完成的。
核心代码,是 C++ 代码实现的

Python 只不过是调用 C++ 写的动态库而已。

但是,不是只有 Python 能调用 C++ 写的动态库,Java也能。

只不过 Python 调用 C++ 写的动态库,特别简单

所以,AI 的兴起,是 C++ 带着Python 起飞

1.2 垃圾回收的是哪个区域?

JVM初识 & JVM 内存区域划分

在这篇博客当中,我们了解到,JVM 划分出了四个核心区域:

  1. 程序计数器
  2. 元数据区

垃圾回收机制 ,回收的是哪一块内存?

答:堆 上的内存

程序计数器:线程销毁,自然就销毁了。

栈:方法执行结束,栈帧的结束了,随之释放了。

元数据区:保存类对象,一般不会释放。

堆:存放着 new出来的对象,这些对象,有新对象的产生,也有旧对象的消亡,需要进行回收。

垃圾回收,回收的是 "对象"。

2. 垃圾回收的工作过程

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

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

接下来,我们对这两个步骤,分别进行介绍。

3. 第一步:找到垃圾

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

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

接下来,我们分别介绍这两种方案。

3.1 引用计数

每个对象,在 new 的时候,都搭配一个小的内存空间,保存一个整数

引用计数 :这个整数 ,就表示当前对象(上图为 Test对象),有多少个引用指向它 (上图,t2,t3 指向 Test对象,所以引用计数为 2

每次进行引用赋值的时候,就会自动触发引用计数的修改。

通过用引用计数,记录有多少个 引用 指向这个对象(如上图的 Test对象)。

Java 当中,要想使用某个对象,一定时通过 "引用" 来完成的 (这部分是 Java基础语法 的知识)。

引用计数,有两种情况:

第一种:引用计数不为 0 ,就说明 还有引用 指向这个对象 ,这个对象,还需要进行使用,不是垃圾

第二种:引用计数为 0 ,说明 没有引用 指向这个对象 了,这个对象,不会进行使用了,这个对象就是垃圾

但是,这种找垃圾的方案,有两种缺陷

  1. 消耗内存多
  2. 循环引用问题

缺陷一:消耗的内存更多

我们需要开辟空间,来记录对象,引用的个数

一个对象,开辟一个空间,两个对象,就需要开辟两个空间。

如果对象本身,是比较小的,引用计数消耗的空间的比例就更大了。

假设,一个对象本身的大小是 8 个字节(8B)引用计数的空间是 4 个字节(4B) ,那么,对象 + 引用计数 消耗的空间,就达到 12 个字节(12B)

这个对象的引用计数,就相当于提高了 50% 的空间占用率

一个对象,就提高了 50% 的空间占用率 ,如果是多个这样的对象,消耗的内存空间就更多了

50%,是一个很高的数字了,想象一下,一个公司,每个员工,都在原工资的基础上,涨薪 50% ,这是非常恐怖的一件事。

缺陷二:可能出现 循环引用 的问题

什么叫做循环引用?

我们通过一段代码来理解:

上述过程,就是一个 循环引用 的构成过程。

此时,这两个 Test 的引用计数,都不为 0

虽然不为 0,但是这两个对象,都是无法使用的

你想要访问第二个对象,就需要访问第一个对象,拿到里面的引用变量,去访问第二个对象。

要想访问第一个对象,就需要访问第二个对象,拿到里面的引用变量,去访问第一个对象。

两个对象,相互依赖,就构成了 循环引用 的现象。

这段话,你画个图,就发现,他们构成一个圆圈,循环了。

就类似于:
家里的钥匙,锁在车里了,需要打开车

打开车的车钥匙,又锁家了,需要家里的钥匙,才能开门

家里的钥匙,锁在车里了,需要打开车。

......

总结:

正是上述两点缺陷,Java 并没有采取这种方案,去找 垃圾

Python,PHP 虽然使用引用计数,但是,需要搭配其他的方案,辅助解决上述的引用计数的 循环引用 问题。

3.2 可达性分析

引用计数方案,由于缺陷一,是有 空间开销 的
可达性分析 ,用 时间开销,换空间开销。

可达性分析的具体过程,是这样的:

  1. 以代码中的一些特定对象,作为遍历的 "起点" (这个起点叫做 GCRoots

这些特定对象,有三种

  1. 栈上的局部变量(需是 引用类型 的局部变量)
  2. 常量池引用指向的对象
  3. 静态成员(引用类型)

这三种特定的对象,程序运行到任何一个时刻,JVM 都是很容易可以获取到的。

  1. GC会尽可能的进行遍历程序中的对象,判定某个对象,是否能访问到
  2. 每次访问到一个对象,这个对象,就会被标记成 "可达" ,当完成所有的对象的遍历 后,未被标记成 "可达" 的对象,就会被标记为 "不可达"
  3. 通过可达性分析
    "可达" 的对象 ,是要被使用 的,不会进行回收
    "不可达" 的对象 ,是不会被使用 的,就是要回收的垃圾

上述过程,一个程序中,有多少个对象,JVM自身是知道的

我们用一段代码,来演示说明一下:

java 复制代码
class Node {
    String val;
    Node left;
    Node right;
}

Node build() {
    Node a = new Node("a");
    Node b = new Node("b");
    Node c = new Node("c");
    Node d = new Node("d");
    Node e = new Node("e");
    Node f = new Node("f");
    Node g = new Node("g");

    a.left = b;
    a.right = c;
    b.left = d;
    b.right = e;
    e.left = g;
    c.right = f;
    return a;
}

这段代码,是用来构造一棵 二叉树 的代码。

调用 build()方法 构造的二叉树,长这样:

可达性分析,用流程图来表示,是这样的:

找到一个节点,就会把那个节点对象,标记为 "可达"

可达性分析的过程,很像 二叉树 的遍历过程。

此时,我运行一条这样的代码:root.right.right = null

也就是把 c 指向 的对象,改变了,指向为 null;

GC 机制,再次进行 可达性分析 的过程中,f 这个节点对象,就会被标记为 "不可达"对象

那么,f 节点对象,就会被当作 垃圾,被 GC 回收

可达性分析,这个过程,是一个周期性的过程。
每隔一定的时间,就会触发一次这样的 可达性分析 的遍历,寻找 垃圾

就像我们在初中高中,每一天都有同学负责打扫教室,这是周期性的。

正是因为 GC,它会周期性的遍历一遍所有的对象,C++ 才会嫌弃 GC机制

如果 对象 特别多,这个过程就得非常消耗时间和资源,降低了 程序运行 的效率

总结

垃圾回收,找垃圾的方案,我们介绍了两个:

  1. 引用计数
  2. 可达性分析

知道这两个方案的工作过程,面试中能够说出来,就可以了。

至于面试中怎么回答,回答哪一个,要看面试官怎么问的。

问题:介绍一下 垃圾回收机制 的基本策略

回答方式:两个方案都可以回答(引用计数,可达性分析)

问题:介绍一下 Java的垃圾回收 的基本策略

回答方式:只介绍 可达性分析 找垃圾的过程

4. 第二步:释放垃圾

这部分的内容,有 3000字+,如果放在这篇博客,内容太多,影响观看体验。

我将这部分内容,放到这篇博客中:JVM 垃圾回收机制(GC)-- 释放垃圾

你需要点击链接,观看这篇博客。

当然,你可以看到这里后,休息,之后再点击博客链接,继续学习。

5. 垃圾收集器

JVM 中,存在 垃圾收集器 模块,用来实现上述 分代回收 的策略。

分代回收,只是最基本的思想,落实到具体的垃圾收集器上,会有一些特定的,更进阶的策略,去实现 分代回收 的思想。

至于垃圾收集器,我这里就不展开讲了,主要面试也不考,大家想要了解的,可以去 B站 搜索相关视频进行学习。

5. 总结

这篇博客,我们主要学习了 JVM的垃圾回收机制(GC)。

垃圾回收机制(GC),是 Java 中,释放内存的手段。

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

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

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

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

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

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

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

相关推荐
ʚB҉L҉A҉C҉K҉.҉基҉德҉^҉大5 小时前
自动化机器学习(AutoML)库TPOT使用指南
jvm·数据库·python
笨手笨脚の5 小时前
深入理解 Java 虚拟机-03 垃圾收集
java·jvm·垃圾回收·标记清除·标记复制·标记整理
dyyx1116 小时前
使用Scikit-learn进行机器学习模型评估
jvm·数据库·python
weixin_499771556 小时前
使用Seaborn绘制统计图形:更美更简单
jvm·数据库·python
尽兴-7 小时前
JVM执行引擎深度解析
jvm·jit·执行引擎
疯狂的喵8 小时前
用Matplotlib绘制专业图表:从基础到高级
jvm·数据库·python
qq_192779878 小时前
用Pygame开发你的第一个小游戏
jvm·数据库·python
weixin_704266058 小时前
Java线程与进程:基础概念解析
java·开发语言·jvm
2401_838472518 小时前
Python单元测试(unittest)实战指南
jvm·数据库·python
黄连升8 小时前
JVM-java 虚拟机
java·jvm·垃圾回收·类加载·垃圾回收算法·双亲委派·jvm 内存结构