网络知识 娱乐 图文详解JVM中的垃圾回收机制(GC)

图文详解JVM中的垃圾回收机制(GC)

文章目录

  • 1. 什么是垃圾回收机制(GC)
    • 1.1 垃圾回收机制的优缺点
  • 2. 哪些内存需要回收
  • 3. 垃圾回收具体是如何回收的
    • 3.1 找垃圾/判定垃圾
      • 3.11 基于引用计数
      • 3.12 引用计数的优缺
      • 3.13 基于可达性分析
      • 3.14 可达性分析的优缺点
    • 3.2 回收垃圾(释放内存)
      • 3.21 回收垃圾(释放内存)三种基本策略
        • 标记 - 清除
        • 复制算法
        • 标记 - 整理
      • 3.22 分代回收
  • 4. 垃圾回收器
  • 🎉✨总结


1. 什么是垃圾回收机制(GC)

在早期的计算机语言,比如 C 和 C++,需要开发者手动的来跟踪内存,这种机制的优点内存分配和释放的效率很高。但是它也有它的缺点如果程序员不小心忘记释放内存,从而造成内存的泄露

内存泄露:申请内存之后,忘记释放了 导致 可用的内容越来越少,最终无内存可用

新的编程语言,比如 JAVA,Go,Python,PHP… 现在市面上的大部分主流编程语言,都采取了一个方案,那就是 “垃圾回收机制”,运行时自身会运行相应的垃圾回收机制。程序员只需要申请内存,而不需要关注内存的释放垃圾回收器(GC)会在适当的时候将已经终止生命周期的变量的内存给释放掉。

1.1 垃圾回收机制的优缺点

GC的优点:

  • 它大大简化了应用层开发的复杂度(不需要开发者再去手动跟踪内存)
  • 降低了内存泄露的风险

GC的缺点:

  • 消耗额外的开销(消耗的资源更多了)
  • 会影响程序的流畅运行

2. 哪些内存需要回收

JVM的内存结构包括四大区域:1.程序计数器 2.栈 (虚拟机栈,本地方法栈)3.堆 4.方法区

在这里插入图片描述

举个例子,任何组织里,人都有三个派别,1.积极派 2.消极派 3.中间摇摆派,如图,对于上述三个派别,哪些是要进行回收释放内存的?

正在使用的内存中的对象 代表 积极派

不再使用,但是尚未回收的内存中的对象 代表消极派

中单部分为中间摇摆派
在这里插入图片描述
需要进行回收释放资源的:消极派

为什么中间摇摆派不回收释放内存呢?对于这种部分仍在使用,一部分不在使用的对象,整体来说是不释放的!等到这个对象彻底完全不使用,才真正的释放!!

注意

垃圾回收的基本单位是“对象”,而不是“字节”

3. 垃圾回收具体是如何回收的

分为两个阶段:

  1. 找垃圾/判定垃圾
  2. 回收垃圾(释放内存)

3.1 找垃圾/判定垃圾

如何找 垃圾/判定垃圾呢?当下主流的思路,有两种方案:

  1. 基于引用计数(不是Java中采取的方案,这是别的语言,像Python采取的方案)
  2. 基于可达性分析(这个是Java采取的方案)

3.11 基于引用计数

什么是基于引用计数:简单来说,针对每个对象,都会额外引入一小块内存,保存这个对象有多少个引用指向他
举个例子
1.Test t = new Test();,此时 new 了一个对象,那么我们就会额外引入一小块内存,此时 t 指向这个对象的引用,因此 引用计数 加 1

在这里插入图片描述
2.Test t2 = t; 此时 t 和 t2 都是指向这个对象的引用,此时引用计数 从1 变为 2
在这里插入图片描述
3.

void func() {
   Test t = new Test();
   Test t2 = t;
}

func()//调用方法过程中,创建了对象(分配内存),在方法执行过程中,引入计数量是2,当方法执行结束,由于 t 和 t2 都是局部变量,跟着栈帧一起释放了,这一释放就导致引用计数为0了(没有引用指向这个对象了,也就没有代码能够访问到这个对象了),此时就认为这个对象是个垃圾!

注意:引用计数为0的时候,就不再使用了,这个内存不再使用,就释放了(为后面理解做铺垫)

3.12 引用计数的优缺

引用技术,简单可靠高效,但是有个两个致命缺陷!!

  1. 空间利用率比较低,每个 new 的对象都得搭配个 计数器,计数器假设 4个字节,如果对象本身很大(几百个字节),多出来4个字节,就不算什么,但是如果本身对象很小(自己才4个字节),多出4个字节,相当于空间被浪费了一半
  2. 会有循环引用的问题

循环引用问题:写个代码举例子便于理解

// 先创建一个类
class Test {
// 成员变量
    Test t = null;
}
// 创建实例
Test t1 = new Test();
Test t2 = new Test();

画出内存布局:

在这里插入图片描述

t1.t = t2;//把 t2 赋值给了 t1里面的t属性,此时对象2有两个引用
引用计数加1,变为2

在这里插入图片描述

t2.t = t1// 把 t1 赋值给 t2 里面的 t 属性,此时对象1 有两个引用
引用计数加1,变为2

在这里插入图片描述
接下来,烧脑的环节:

t1 = null
t2 = null

在这里插入图片描述

此时此刻,两个对象的引用计数,不为0,所以无法释放,但是由于引用长在彼此的身上,外界的代码也无法访问到这两个对象,此时此刻,这俩对象,就不能使用,又不能释放,就出现了“内存泄露”的问题。

所以,像 Python,PHP里进行GC也不只靠引用计数,还依赖其他的机制配合,但是Java可以直接采用可达性分析,来判断垃圾

3.13 基于可达性分析

基于可达性分析:简单的来说,通过额外的线程,定期的针对整个内存空间的对象进行扫描,有一些起始位置(称为 GCRoots),会类似于 深度优先遍历一样,把可以访问到的对象都标记一遍(带有标记的对象就是可达对象),没有被标记的对象,就是不可达,也就是垃圾!

什么才算 GCRoots

  1. 栈上的局部变量
  2. 常量池中的引用指向的对象
  3. 方法区中的静态成员指向的对象

举个栗子吧,比如:写个代码,构造一个二叉树
在这里插入图片描述
如果我们在外面的代码中

Node root = a

代码中只要拿到 树 根节点,就可以掌握所有的节点,树上的任意节点,都可以通过 a 直接/间接的获取到

换句话说,GC在进行可达性分析的时候,当 GC 扫描到 a 的时候,就会把 a 能访问到的所有元素都去访问一遍,并且进行标记,所标记的节点 表示 都不是 垃圾

如果代码中,写了如下代码

c.right = null

在这里插入图片描述
则此时意味着,从 a 出发,访问不到 f,f 就是 不可达,f 就是垃圾,f 就应该被回收

如果代码中

a.right = null

在这里插入图片描述
此时从 a 出发,c 和 f 都是不可达了,也就都被标记成垃圾了!

从上面的这几点我们可以看出,可达性分析是去遍历每一个对象,如果内存中的对象特别多,这个遍历就会很慢,因此 GC 还是比较消耗时间和系统资源的!

3.14 可达性分析的优缺点

优点:

  • 克服了引用计数的两个缺点:
    1. 空间利用率低
    2. 循环引用

缺点:

  • 系统开销大,遍历一次可能比较慢

tips:

找垃圾,核心就是确认这个对象未来是否还会使用,什么算不使用了?没有引用,就不使用了

明确了谁是垃圾之后,接下来就要回收垃圾了!

3.2 回收垃圾(释放内存)

3.21 回收垃圾(释放内存)三种基本策略

标记 - 清除

如图:这是一块内存,上面被分成了很多小块,其中有些部分是垃圾(打钩的)

在这里插入图片描述

这里的 标记 ,就是可达性分析的过程

清除,就是直接释放内存 ,灰色区域代表释放内存

在这里插入图片描述

此时如果直接释放,虽然内存还是还给了系统,但是被释放的内存是离散的(不是连续的)

分散开带来的问题就是:“内存碎片”,这个问题其实非常影响程序的执行!

内存碎片:比如,空闲的内存,有很多,假设一共是 1G,如果要申请 500M 内存,也是可能申请失败的,因为要申请 500M 的内存 必须是连续的,每次申请,都是申请的连续的内存空间,而这里的 1G 可能是多个 碎片加在一起 才 1G,可用的并不多

为了解决内存碎片因此我们引入了复制算法!

复制算法

如图:一块内存,分成两半,左边一半有很多对象,打钩的标记为垃圾,右边为 左边不是垃圾的,拷贝过来

在这里插入图片描述

然后再将左边全部标记为垃圾(灰色),全部释放掉,我们就能保证,左右两侧空间都是整体连续的

在这里插入图片描述

此时内存碎片问题就迎刃而解了!

注意:复制算法的问题有如下几点

  • 内存空间利用率低(只能用一般的空间)
  • 如果要保留的的对象多,要释放的对象少,此时复制开销就很大

针对复制算法我们进行改进!–》 标记 - 整理

标记 - 整理

如图:还是一块内存,上面有一些对象,其中一些被标记为垃圾(打钩的)

在这里插入图片描述

如何进行标记 - 整理呢?

类似于顺序表删除中间元素,有一个搬运操作,我们将 3 搬运到 2 ,再将 5 搬运到 3 ,再把 7 搬运到 4 然后再把后面的部分整体的释放掉

在这里插入图片描述

这个方案空间利用率是高了,但是仍然没有解决复制/搬运元素开销大的问题~

3.22 分代回收

上述的三个方案,虽然能解决回收垃圾的问题,但是都有缺陷,实际 JVM 中的实现,会把多种方案结合起来一起使用,这个思路我们称为 “分代回收”

在这里插入图片描述

我们这个对象,他是怎样在这个区域里来回 轮转 的呢?

  1. 刚创建出来的对象,就放在伊甸区
  2. 如果伊甸区的对象熬过一轮 GC 扫描,就会被拷贝到 幸存区(伊甸区 到 幸存区 应用了复制算法)
  3. 在后续的几轮 GC 中,幸存区的对象就在两个幸存区之间来回拷贝(复制算法),每一轮都会淘汰一波幸存者
  4. 在持续若干轮之后,对象终于,进入老年代,老年代有个特点,里面的对象都是比较老的(年级大的),因此老年代的 GC 扫描频率大大低于新生代。老年代中使用标记整理的方式进行回收!

上述过程是面试中的经典问题!!!一定要重点掌握啊!

注意:
在这里插入图片描述

注意!!!
分代回收中,还有一个特殊情况,有一类对象可以直接进入老年代(大对象,占有内存多的对象),大对象拷贝开销比较大,不适合使用复制算法!

4. 垃圾回收器

上面说的找垃圾,和释放垃圾,说的都是算法思想,不是具体落地实现,在JVM里,真正实现上述算法的模块称为“垃圾回收器”

在这里插入图片描述

在这里插入图片描述


🎉✨总结

“种一颗树最好的是十年前,其次就是现在”

所以,

“让我们一起努力吧,去奔赴更高更远的山海”

如果有错误❌,欢迎指正哟😋

🎉如果觉得收获满满,可以动动小手,点点赞👍,支持一下哟🎉

以梦为马,不负韶华

在这里插入图片描述