JVM的三种垃圾回收算法

 2022-09-12
原文地址:https://blog.csdn.net/weixin_43473420/article/details/119977982

一、什么是垃圾?

在了解垃圾回收机制之前我们首先要定义一下什么是垃圾,我们内存里大部分的对象都是随着方法的执行而创建,方法执行完毕后这些对象就不会被再次使用了,但是这些生成的对象不会被清除掉,所以我们内存里面的对象会越来越多,这时我们就需要一种机制把这种不会被再次使用的对象清除掉,而这种不会被再次使用的对象我们就称之为垃圾。

二、如何判断对象是否可以被回收?

1. 引用计数法

原理:引用计数法是为对象添加一个引用计数器,然后用一块额外的内存区域来存储每个对象被引用的次数,当对象每有一个地方引用它时,那我们对该对象的引用计数就会加1,反之每有一个引用失效时,我们对该对象的引用计数就会减1, 当对象的被引用次数为0时,那么我们可以认为这个对象是不会被再次使用了,通过这种方式我们能快速直观的定位到这些可回收的对象,从而进行清理。
缺点:1、无法解决循环引用的问题:引用计数法虽然很直观高效,但是通过引用计数法是没办法扫描到一种特殊情况下的“可回收”对象,这种特殊情况就是对象循环引用的时候,比如A对象引用了B,B对象引用了A,除此之外他们两个没有被任何其他对象引用,那么其实这部分对象也属于“可回收”的对象,但是通过引用计数法是没办法定位的。垃圾清理不掉,很容易就OOM了,这是很致命的,所以被淘汰掉了。
2、另外一个方面是引用计数法需要额外的空间记录每个对象的被引用的次数,这个引用数也需要去额外的维护。

2. 可达性分析法

原理:可达性分析法是通过以所有的“GC Roots”对象为出发点,如果无法通过GC Roots的引用追踪到的对象,那我们认为这些对象就不会再次被使用了,现在主流的程序语言都是通过可达性分析法来判断对象是否存活的。

哪些对象对象我们称之为"GC Roots"对象呢? 当然普通的对象肯定是不行的,如果要作为GC Roots 对象那么它自身肯定得满足一个条件,那就是他自己一定在很长一段时间内都不会被GC 回收掉。那么只有满足这个条件的对象才可能作为GC Roots了,GC Roots的类型大致如下:
1、虚拟机栈中的本地变量所引用的对象。
2、方法区中静态属性引用的对象。
3、方法区中常量引用的对象。
4、本地方法中(Native方法)引用的对象。
5、虚拟机内部的引用对象(类记载器、基本数据对应的Class对象,异常对象)。
6、所有被同步锁(Synchronnized)持有的对象。
7、描述虚拟机内部情况的对象(如 JMXBean、JVMTI中注册的回调、本地缓存代码)。
8、垃圾搜集器所引用的对象。

202209122041173071.png

三、垃圾回收算法

标记清除算法

原理:标记清除算法是先找到内存里的存活对象并对其进行标记,然后统一把未标记的对象统一的清理。过程大致如下图。

202209122041182182.png
优点:标记清除算法的特点就是简单直接,速度也非常块,特别适合可回收对象不多的场景。
缺点:1、会造成不连续的内存空间:就像上图清除后的内存区域一样,清除后内存会有很多不连续的空间,这也就是我们常说的空间碎片,这样的空间碎片太多不仅不利于我们下次分配,而且当有大对象创建的时候,我们明明有可以容纳的总空间,但是空间都不是连续的造成对象无法分配,从而不得不提前触发GC。
2、性能不稳定:内存中需要回收的对象,当内存中大量对象都是需要回收的时候,通常这些对象可能比较分散,所以清除的过程会比较耗时,这个时候清理的速度就会比较慢了。

标记复制算法

背景:标记清除算法最大问题是会造成空间碎片,并且只适合需要回收的对象比较少的场景,那么针对这个问题就衍生了标记复制算法,标记复制算法专门针对这两个问题进行了解决。标记清除算法的关注点在可回收的对象身上,而标记复制算法的关注点则放在了存活的对象身上,通过把存活的对象放挪到一个固定的区域,然后对其他区域的对象进行统一清理。

原理:首先它把年轻代内存划分出三块区域,一块用于存放新创建的对象叫Eden区,另外两块则用于存放存活的对象分别叫 S1区和S2区。回收的时候会有两种情况,一种是把Eden和S1区的存活对象复制到S2区,第二种是把Eden和S2区的存活对象复制到S1区 ,也就是说S1区和S2这两块区域同时只会有一块使用,通过这种方式保证始终会有一块空白的q区域用于下次GC时存放存活的对象,而且原来的区域不需要考虑保留存活的对象,所以可以直接一次性清除所有对象,这要既简单直接同时也保证了清除后的内存区域的内存连续性。过程大致如下图。

202209122041192373.png
优点:标记复制算法解决了标记清除算法的空间碎片问题,并且采用移动存活对象的方式,每次清除针对的都是一整块内存,所以清除可回收对象的效率也比较高,但因为要移动对象所以这里会耗费一部分时间,所以标记复制算法的效率还是会低于标记清除算法。

缺点:1、会浪费一部分空间:通过上面的图我们也不难发现,总是会有一块空闲的内存区域是利用不到的,这也造成了资源的浪费。
2、存活对象多会非常耗时:因为复制移动对象的过程是比较耗时的,这个适合不仅需要移动对象本身还需要修改使用了这些对象的引用地址,所以当存活对象多的场景会非常耗时,这也提示我们标记复制法比较适合存活对象较少的场景。
3、需要担保机制:因为复制区总会有一块空间的浪费,而为了减少浪费空间太多,所以我们会把复制区的空间分配控制在很小的区间,但是空间太小又会产生一个问题,就是在存活的对象比较多的时候,这时复制区的空间可能不够容纳这些对象,这时就需要借一些空间来保证容纳这些对象,这种从其他地方借内存的方式我们称它为担保机制。

注:担保机制——分配担保

一般情况下,复制算法将内存空间按照容量划分为两块(如上图所示),两块内存交替使用,当一块内存使用完的时候,就先将存活的对象逐一复制到另一块未使用的内存,然后将当前使用的这块内存一次性清理掉。极端情况下,对象全部存活,但是因为两块内存一样大,所以可以装得下。

为了提高内存利用率,有人提出了复制收集算法的改进方案(如下图所示),将内存空间按照8:1:1的比例划分为3块(比例可以调整),称较大的一块为Edent,较小的两块为Survivor,两个Survivor交替着配合Eden一起使用,每当需要进行垃圾回收,先将存活的对象复制到保留的Survivor,然后将Eden和之前使用的Survivor一起清理掉。由于Survivor的内存空间较小,极端情况下可能不够保存存活下来的对象,为了解决这个问题,需要提供一块额外的空间进行分配担保——把保存不下的对象存储到额外空间。

标记压缩(整理)算法

背景:标记复制算法是完美的补齐了标记清除算法的短板,即解决了空间碎片的问题,又适合使用在大部分对象都是可回收的场景。 但是问题是标记复制算法并不适用于存活对象多的场景,如果使用标记清除算法来处理,那么空间碎片又是一个无法忍受的问题。有了具体的场景和问题,所以就有了标记整理算法,它是专门针对于存活对象多的情况下进行垃圾收集当然同时需要要避免产生空间碎片。

原理:标记整理算法分为标记和整理两个阶段,标记阶段会先把存活的对象和可回收的对象标记出来;标记完之后就是进行整理了,这个阶段会把存活的对象往内存的一端移动,移动完对象后再清除可回收的对象。过程大致如下图。

202209122041201524.png
优点:标记整理法最大的特点就是解决了标记清除法的空间碎片问题,同时也不至于像标记复制法需要空闲的内存空间,所以它非常适合存活对象多的场景。
缺点:标记整理法是三种垃圾回收算法中性能最低的一种,因为标记整理法在移动对象的时候不仅需要移动对象,还要额外的维护对象的引用的地址,这个过程可能要对内存经过几次的扫描定位才能完成,做的事情越多那么必然消耗的时间也越多。

四、各种垃圾回收算法的适用场景

我们了解了三种垃圾回收算法后会发现,没有一个算法是完美,每种算法都有自己的特点,所以我们只能根据具体的场景去选择合适的垃圾回收算法。

1、标记清除算法
特点: 简单、收集速度快,但会有空间碎片,空间碎片会导致后面的GC频率增加。
适合场景:只有小部分对象需要进行回收的,适用于老年代的垃圾回收,因为老年代一般存活对象会比回收对象要多。

2、标记复制算法
特点:收集速度快,可以避免空间碎片,但是有空间浪费,存活对象较多的情况下复制对象的过程等会非常耗时,而且需要担保机制。
适合场景: 只有少量对象存活的场景,这也正是新生代对象的特点,所以一般新生代的垃圾回收器基本都会选择标记复制法。

3、标记整理算法
特点: 相对于标记复制法不会浪费内存空间,相对标记清除法则可以避免空间碎片,但是速度比其他两个算法慢。
适合场景: 内存吃紧,又要避免空间碎片的场景,老年代想要避免空间碎片问题的话通常会使用标记整理法。