用5分钟让你了解 JVM 垃圾收集算法

大家好,我是Morning,在CSDN写文,分享一些Java基础知识,一些自己认为在学习过程中比较重要的东西,致力于帮助初学者入门,希望可以帮助你进步。感兴趣的欢迎关注博主,和博主一起学习Java知识。大家还可以去专栏查看之前的文章,希望未来能和大家共同探讨技术。

标记-清除算法

​ 这个算法是最早出现也是最基础的垃圾收集算法,就像它的名字一样,这个算法分为标记 、清除两个步骤。总的来说就是,标记所有被引用的对象。一般是在对象的 Header 中记录为可达对象(从根节点可达)。然后回收掉在其 Header 中没有标记为可达的对象。

​ 需要注意的是,这种方式清理出来的空闲内存不是连续的,产生内存碎片,需要维护一个空闲列表,来记录垃圾对象的地址。下次有新对象要加载时,判断空闲列表中的内存空间是否够,如果够,就覆盖原有的地址

​ 它的优点就是,简单并且容易理解。缺点是,相比其他的算法效率不是很高,容易产生内存碎片
在这里插入图片描述

那么它具体是怎么来标记的呢,这就要提到另一个算法,那就是可达性分析算法

可达性分析算法

​ 可达性标记算法用来判断对象是否存活。它也可以被称为根搜索算法,追踪性垃圾收集。

其基本思路如下:

  1. 可达性分析算法是以根对象集合(GCRoots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
  2. 使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(Reference Chain)
  3. 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象可以标记为垃圾对象。
  4. 在可达性分析算法中,只有能够被根对象集合直接或间接连接的对象才是存活对象

概念枯燥无味,我们来看一个图,在下面这幅图中,对象1、2、3不会被回收,因为他们直接或间接的和Roots有关系,而对象4、5就会被回收。

在这里插入图片描述

那么什么是GC Roots呢?

GC Roots 根集合就是一组必须活跃的引用。

哪些对象可以被作为GC Roots呢?

  1. 虚拟机栈中引用的对象。(例如各个线程被调用的方法中使用到的参数、局部变量)
  2. 本地方法栈内 JNI(本地方法)引用的对象。
  3. 方法区中类静态属性引用的对象。(Java 类的引用类型静态变量)
  4. 方法区中常量引用的对象。(字符串常量池里的引用)
  5. 所有被同步锁 synchronized 持有的对象。
  6. Java 虚拟机内部的引用。(基本数据类型对应的Class对象)
  7. 除了以上这些固定的 GC Roots 集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整 GC Roots 集合。(比如:分代收集和局部回收。)

总之就是,除了堆空间的周边,比如:虚拟机栈、本地方法栈、方法区、字符串常量池等地方对堆空间进行引用的,都可以作为 GC Roots 进行可达性分析。

还有就是,由于 Root 采用栈方式存放变量和指针,所以如果一个指针,它保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那它就是一个 Root。

引用计数算法

​ 既然说到了可达性分析算法,那我们也说一说引用计数算法。引用计数算法比较简单,可能会对你理解可达性分析算法有帮助。

​ 引用计数算法,它对每个对象保存一个整形的引用计数器属性,用于记录对象被引用的情况。也就是说,如果有一个对象object,只要有任何一个对象引用了这个object,那么object的引用计数器就加一,当引用失效时,就减一,只要对象的引用计数器的值为0,那么表示这个对象不可能再被使用,可以回收。

​ 它的优点就是,实现简单,垃圾对象便于辨识(只需要判断引用计数器的值是否为0),判定效率高,回收没有延迟。

​ 一个算法不能十全十美,有优点就有缺点,引用计数算法的缺点就是,他需要一个单独的字段存储计数器,这样的话就增加了存储空间的开销;每次有对象引用(或引用失效)它,就要伴随加法或减法操作,浪费了时间;它还有一个最为致命的缺点,即无法处理对象之间互相循环引用的情况(相互引用导致两个对象的计数器都不为0,一直无法被回收),这使 Java垃圾回收机制没有采用这一算法。

其实可达性分析算法有效的解决了引用 计数算法的缺点,所以被 Java垃圾回收机制采用。

标记-复制算法

​ 将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。

​ 就是说有俩块内存区域(A,B),B是空闲的,垃圾回收时,把A区域中没有回收掉的(绿色的)对象复制到B区域中,然后清空A区域,这是A是空闲的,下一次从B复制到A。这种算法一般适用于存活的对象少,但是垃圾多的情况,所以新生代的回收一般采用这种算法

​ 优点:没有标记和清除过程,实现简单,对于少量的对象来说,运行高效,复制到另一块区域中时,内存是连续的,不会出现 “碎片” 问题。

​ 缺点:需要俩倍空间,对大量的对象来说,对象发生复制会导致效率变低。

在这里插入图片描述

标记-压缩算法

​ 在老年代,大多对象都是存活的,这里如果使用标记-复制算法的话,复制的成本较高,我们之前讲到的标记-清除算法可以在老年代应用,但是这个算法效率低,还容易产生内存碎片。所以设计者设计了标记-压缩算法。

标记还是和其他的算法一样,我们这里主要将压缩,压缩就是将存活的对象整理到内存的一端,按顺序排放。然后,清理边界外所有的空间。

​ 优点就是不会产生内存碎片,也不会发生像复制算法中的内存减半的问题。但是在整理过程中,会移动对象,对象被其他对象引用,则还需要调整引用的地址,移动过程中,需要STW( Stop the World,暂停用户应用程序),效率低一些。

相当于在执行完标记-清除算法后再进行一次内存碎片整理,将内存碎片整理到一端。
在这里插入图片描述

分代收集算法

​ 由于不同的对象的生命周期是不一样的,所以不同生命周期的对象可以采取不同的收集方式,这样可以提高回收效率。Java 堆分为新生代和老年代,这俩个各有各的特点,**新生代区域相对老年代较小,对象生命周期较短、存活率较低,回收较频繁。老年代区域较大,对象生命周期较长、存活率较高,回收没有新生代频繁。**不同的特点导致我们可以使用不同的算法来提高垃圾回收的效率。

​ 根据不同的特点,新生代使用标记-复制算法效率会高一些;老年代使用标记-压缩、标记-清除算法混合实现,这样效率会高一些。

增量收集算法

​ 上面所说的算法在垃圾回收时,用户线程需要停止(STW),等待垃圾回收完成,为了解决这一问题,增量收集算法诞生了。它的基础还是标记-清除、复制算法。

​ 基本思想是,垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。这样的话就可以达到减少用户线程的停顿时间这一目的。这样的改进所面临的问题是,大量的进行线程切换会导致系统资源开销大

在这里插入图片描述

好了,本次的分享到这里就结束了。本次的博文内容比较枯燥,感谢您的阅读。博主会在日后给大家分享其他的知识,和大家一起探讨,有兴趣的可以关注博主。文中有什么不当的地方,欢迎大家在评论区指出,大家一起探讨、学习。🤞🤞🤞

  • 11
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值