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. 元数据区
  3. 栈
  4. 堆

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

答:堆 上的内存。

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

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

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

堆:存放着 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 采用的方案,重点了解)

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

相关推荐
青春逝如流水5 小时前
Java 线程在内存模型(JMM)与 JVM 实现:规范与落地的多元路径
java·开发语言·jvm·多线程·ai文章
colinu14 小时前
C++ 异步任务与线程生命周期:`std::async`、`future`、`thread`、`jthread` 与协作式退出
java·jvm·c++
wuminyu14 小时前
ForkJoinPool窃取线程在扫描其他WorkQueue时的随机采样算法与Phase线程挂起唤醒机制解析
java·linux·c语言·jvm·c++
DongQiShanRen15 小时前
玄龙(上):TICK 主循环——意识心跳怎么跳
linux·jvm·数据库·人工智能·数据挖掘·rust
leisoo80971 天前
融资融券数据怎么查两融指标含义与杠杆观察方法 IG50免费开源股票数据API接口
开发语言·jvm·数据库·python·json
leisoo80971 天前
股票筹码分布怎么用获利比例成本区间与集中度实战 IG50免费开源股票数据API接口
开发语言·jvm·数据库·python·开源
晒太羊的猫2 天前
jvm面试题
jvm
夜之眷属2 天前
Core dump 崩溃排查:JVM 宕机后,那份 core 文件怎么用 gdb 还原现场
java·运维·服务器·jvm
用户094248568032 天前
第31章:JVM字节码解释器与模板解释器源码路径
java·jvm
阿俊-全栈开发2 天前
LikeShop 版本升级与备份:从旧版平滑迁移的完整步骤
jvm·数据库·oracle·开源·likeshop·likeshop开源商城