JVM 的垃圾回收机制以及垃圾回收算法的详解

目录

1、JVM 的垃圾回收机制

2、识别垃圾

2.1、引用计数

2.2、可达性分析

3、垃圾回收算法

3.1、标记-清除

3.2、复制算法

3.3、标记-整理

4、分代回收



1、JVM 的垃圾回收机制

对于程序计数器、虚拟机栈、本地方法栈这三部分区域而言,其生命周期与相关线程有关,随线程而生,随线程而灭。并且这三个区域的内存分配与回收具有确定性,因为当方法结束或者线程结束时,内存就自然跟着线程回收了。

因此有关内存分配和回收关注的为堆区和方法区这两个区域。

垃圾回收,就是回收内存。而 JVM 中的内存又分为好几块:
1、程序计数器,由于程序计数器占用的内存空间小,不需要“垃圾回收”。
2、栈区,局部变量都是在代码块执行结束后自动销毁,这和“垃圾回收”没有关系。
3、元数据区 / 方法区,一般都是涉及到“类加载”,很少涉及到“类卸载”,少量使用到“垃圾回收”。
4、堆区,“垃圾回收”的主要战场。

2、识别垃圾

想要回收垃圾,首先要识别出垃圾。在 Java 中,使用对象一定需要通过引用的方法来使用(除了匿名对象,执行完匿名对象那行代码后,对应的对象就会被当作垃圾),如果一个对象没有任何引用指向它,就视为是无法被代码使用的,即可以当作垃圾

但是,如果代码复杂一些,这些引用的生命周期各不相同,此时识别垃圾的判定过程就会复杂一些。为了解决这个问题,引入了两种解决方法:

2.1、引用计数

给每个对象安排一个额外的空间,空间里要保存当前这个对象有几个引用。这种思想虽然并没有在 JVM 中使用,但是广泛应用于其他主流语言的垃圾回收机制中(Python、php)。

 引用计数机制,是一个简单有效的机制,但是存在两个关键的问题:

  • 问题一:消耗额外的内存空间。
  • 问题二:引用计数可能会产生“循环引用”的问题,此时引用计数就无法正常工作。所谓“循环引用”是指类似于:一个类中的一个成员变量指向另一个类的引用,另一个类中的成员变量同样又指向这个类的引用,当将原先引用类的变量置空,此时引用计数依然不为0。

上述代码出现了问题,此时两个对象的引用计数都不是0,不能被 GC 回收掉,但是这两个对象又无法使用,出现了类似“死锁”的情况,这也是 Java 不用引用计数的原因之一。

2.2、可达性分析

本质上是用“时间”换取“空间”,相比引用计数需要消耗更多的额外空间,但总体来说是可控的,不会产生类似“循环引用”的问题。

在写代码的过程中会定义很多变量,就可以从这些变量作为起点入手,尝试进行“遍历”,所谓遍历就是沿着变量中持有的引用类型的成员,再进一步的往下进行访问

JVM 中存在专门的扫描线程,会不停的尝试对代码中已有的变量进行遍历,尽可能多的访问到对象。

使用上图举例,root 指向这棵二叉树的根节点,如果代码中出现了 root.right.right = null,此时二叉树与 f 节点断开,无法再使用 root 出发进行遍历操作访问 f 了,此时就说 f 节点为 “不可达” ,即可称为是垃圾;同样的代码中出现 root.right = null,此时二叉树与 c 节点断开,此时无法使用 root 遍历到达 c 节点,自然也无法到达 f 节点,此时就说 c 和 f 都是垃圾。

因此,所有能被访问到的对象,自然就不是垃圾了,反之剩下遍历了一圈也访问不到的对象,就是垃圾。

3、垃圾回收算法

把标记为垃圾的对象的内存空间进行释放,主要的释放方式有三种

3.1、标记-清除

把标记为垃圾的对象直接释放掉(最朴素的做法),但是会产生内存碎片。如果存在很多内存碎片,很可能导致申请内存空间失败的情况。
因此一般不会使用这种方案,因为“内存碎片问题”是比较致命的

3.2、复制算法

核心是不直接释放内存,而是把不是垃圾的对象,复制到内存的另外一半中,然后再整体将左则空间整体释放掉。

复制算法确实能够规避内存碎片问题,但是也有缺点:

  • 因为需要留有空间进行复制操作,总的可用内存变少了。
  • 如果每次要复制的对象比较多,此时复制的开销就会很大。(只有当该轮 GC 过程中,有大量对象被释放,少数对象存活,此时才适合使用“复制算法”) 

3.3、标记-整理

类似于顺序表删除中间元素的操作(向前搬运),通过这个过程也能够有效解决内存碎片问题,也没有复制算法需要浪费很多空间,但是搬运内存的开销非常大

由于这三种释放方式都存在各自的问题,因此 JVM 并没有直接使用上述方式,而是结合上述思想取长补短,做出了综合性方案,称为“分代回收”。

4、分代回收

引入了“对象的年龄”这样的概念,JVM 中有专门的线程负责周期性扫描/释放,一个对象如果被线程扫描一次,并且“可达性分析”证明可达(不是垃圾),此时将该对象的年龄属性 + 1(初始年龄为0)。JVM 中根据年龄的差异,将整个堆内存分成两个大的部分,“新时代”和“老年代”。而新生代区中又分为“伊甸区”“生存区s0”“生存区s1”

当代码中 new 出一些对象,这些对象会先被创建在伊甸区,而在伊甸区中的对象,绝大部分会死去,只有少量存活,因此此处的垃圾回收算法使用“复制算法”

然后经过一轮 GC 扫描后,会有少数存活的对象通过“复制算法”拷贝到生存区(有两个生存区),后续的 GC 扫描线程会对伊甸区和生存区都进行扫描,伊甸区中少数存活的对象同样会进入生存区,而生存区中仍然存活的对象会拷贝到另外一个生存区。

当对象的年龄达到一定阈值(一般情况默认 15 次)并且依然存活时,此时 JVM 就会认为该对象的生命周期大概率很长,就将这个对象从生成区拷贝到老年代区。

而在老年代的对象,GC 扫描的频率会大大降低。在老年代中结束的对象会按照 JVM的“标记整理”或者“标记清除”方法释放内存。

【博主推荐】 

对 JVM 的类加载机制以及寻找字节码文件的“双亲委派模型”的理解-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/zzzzzhxxx/article/details/136529700?spm=1001.2014.3001.5501【网络编程】理解客户端和服务器并使用Java提供的api实现回显服务器-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/zzzzzhxxx/article/details/136322678?spm=1001.2014.3001.5501【Java多线程】面试常考——锁策略、synchronized的锁升级优化过程以及CAS(Compare and swap)-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/zzzzzhxxx/article/details/136288256?spm=1001.2014.3001.5501

如果觉得作者写的不错,求给博主一个大大的点赞支持一下,你们的支持是我更新的最大动力!

如果觉得作者写的不错,求给博主一个大大的点赞支持一下,你们的支持是我更新的最大动力!

如果觉得作者写的不错,求给博主一个大大的点赞支持一下,你们的支持是我更新的最大动力!

  • 37
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 32
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 32
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Hacynn

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值