JVM垃圾回收算法

前言

都说艺术源于生活,殊不知科技也源于生活,我们平时生活会产生垃圾,而jvm工作中也会产生垃圾,那么jvm产生的垃圾是什么?回收的区域有哪些?是如何回收的?回收之后去了哪里?回收算法有哪些?今天就来说道说道!

一、什么是垃圾?

垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是要被回收的垃圾
如果不及时清理内存中的垃圾,那么这些垃圾会一直占用内存空间直到程序结束,被占用的空间也无法被重复利用,而内存空间是有限的,随着创建的对象越来越多,内存迟早被耗尽,最终导致内存溢出。
这就像我们平时产生的生活垃圾一样,如果产生的垃圾不及时清理,屋里的空间渐渐被垃圾填满,最后我们自己都进不去家门,这种感觉实在是太糟糕了。所以我们平时产生的垃圾需要及时清理,内存也是同样的道理。

二、回收的区域

众所周知,jvm内存区域分为两种类型,一种是私有区域,一种是共享区域,私有区域存放的是线程私有的数据,而共享区域存放的是线程间共享的数据,而垃圾收集回收的也是共享区域的数据,私有区域即pc寄存器,虚拟机栈,本地方法栈,共享区域即堆区和方法区,而堆区是垃圾回收的重中之重
为啥只回收共享,不回收私有区域呢?我的理解是和他们所负责的功能有关,有句话叫:栈管运行,堆管存储,私有区域不怎么存储数据,而是用来管理运行的,那运行所需要的数据就是存在共享区域的,运行就像路上的车水马龙,自然不会有垃圾,因为不会有存储,而自己家就是用来管存储的。
那么对堆区和方法区回收的频率如何呢?
从次数上来说:频繁收集年轻代,较少收集老年代,基本不动方法区

三、回收的过程

总的来说,回收的过程有2个阶段,第一是标记阶段,第二是清除阶段。
标记阶段就是判断内存中哪些对象还活着,哪些对象已经死亡,对死亡的对象进行标记,只有被标记为死亡的对象,才会被回收掉,释放所占空间。
清除阶段就是对标记阶段标记为死亡的对象进行清理的动作,这些动作我们统称为垃圾回收算法。

四、回收算法

4.1 标记阶段

标记阶段也有对应的算法,一是引用计数算法,二是可达性分析算法。

4.1.1 引用计数算法

这个算法比较简单,就是对每个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况(拿个小本本记录下谁喜欢我,不对谁引用了我)。对于一个对象,只要有任何一个对象引用了此对象,此对象的引用计数器就加1,当引用失效时,计数器就减1,当值为0时,则说明对象不被使用,就可以回收。
但此算法却不能解决循环引用的问题,这是致命的缺陷,也是因为这个缺陷,Java中并未使用此算法,但Python是使用的此算法。等等,你问我什么是循环引用,这个问题你要是不知道,那我,那我就画个图给你讲讲明白!
图片来源于尚硅谷康师傅,侵删

4.1.2 可达性分析算法

相比于引用计数算法,此算法就不仅可以解决循环引用的问题,还可以防止内存泄露的问题。此算法还被称为根搜索算法,或追踪性垃圾收集算法。这也是Java和C#选择的标记算法。
话说回来,什么是可达性或根搜索,说句人话就是从根节点出发,凡是接触到的都是存活的对象,就像下图中这娇艳欲滴的葡萄一样,凡是提溜起来之后没有掉的就是存活对象,掉了的就是不可达对象,话说看的我都流哈喇子了~~~
在这里插入图片描述
刚才说了从根节点出发,那根节点又是啥?根节点又叫GC Roots,那哪些可以作为根节点呢?这就要罗列一下了:

  • 虚拟机栈中引用的对象:比如各个线程中被调用的方法中使用到的参数,局部变量等。
  • 方法区中静态属性引用的对象,比如Java类中引用类型静态变量。
  • 方法区中常量被引用的对象,比如字符串常量池里的引用。
  • 所有被同步锁synchronized持有的对象
  • 本地方法栈内JNI引用的对象。
  • Java虚拟机内部的引用,比如基本类型对应的包装类,一些常驻的异常对象,系统类加载器。

small tip:由于Root采用栈的方式来存放变量和指针,所以如果一个指针保存了堆区中的对象,而本身又不存在堆区中,那就可以看做是一个Root。换言之,jvm内存区域包括pc寄存器,虚拟机栈,本地方法栈,堆区,方法区这五个部分,除了pc寄存器不存储引用,其他的是三个部分:虚拟机栈,本地方法栈,方法区都可以作为GC Roots,那如何眼见为实看到GC Roots呢,可以通过MAT分析工具打开保存的Dump文件进行分析,具体我就不详细说明了,这里附上MAT的下载地址官网说明

当然,我这里只是举例了常见的固定的GC Roots,实际上根据用户所选的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”的加入,共同构成完整的GC Roots集合。
另外还需要根据不同虚拟机的实现细节,来综合考虑存放对象的区域被其他区域对象所引用时,需要将关联的区域对象一并加入GC Roots集合中,这样才能达到可达性分析的准确性。

注意点:使用可达性分析算法时,必须要保证在一致性快照中进行,如果不能满足就无法保证分析的准确性。这也是所有垃圾收集器都不得不面对的点,在进行标记时,需要STW(Stop The World),即便你是CMS垃圾收集器也不行。

4.1.3 对象是存活还是死亡

如果从所有的根节点都无法访问到某个对象,那这个对象就不再被使用了,那么这个对象就可以被回收。但事实上,此对象也并非“非死不可”,也就是说对象在某些状态下“复活”。那就牵扯到了对象的三种状态:

  • 可触及的:从根节点出发,可以达到这个对象
  • 可复活的:对象的所有引用都被释放,但是对象有可能在fianalize()复活(就像一位骑着白马的少年奔驰在法场前,对着即将落下的刀喊了一句“刀下留人”的感觉,画面感来了有没有~~)。
  • 不可触及的:对象的fianalize()调用后没有复活,对象为不可触及的。不可触及的对象不可能再被复活了,因为fianalize()免死令牌只能被使用一次。

因此只有对象是不可触及时才能被回收。

4.2 清除阶段

当成功区分出内存中对象是不可触及的还是存活对象后,GC接下来就是执行垃圾回收,释放掉被无用对象占用的内存,以便有足够的空间可以为新来的对象分配空间。
目前JVM比较常见的垃圾收集算法是:标记-清除算法(Mark-Sweep),复制算法(Copying),标记压缩算法(Mark-Compact),除此之外还有分代收集算法,增量收集算法,分区算法,这几种算是一种思想,而非真正的垃圾收集算法。
这里小小的说明一下,我们这里说的清除阶段使用的算法中,会把标记阶段也囊括进来,因为标记和清除说白了是两个阶段,但在垃圾收集器使用的时候会使用打包的标记阶段和清除阶段作为一个整体的算法,这两个阶段需要配合一起工作才好。

4.2.1 标记-清除算法(Mark-Sweep)

此算法就是非常基础和场景的垃圾收集算法,分为两个步骤,第一步是标记,第二是清除。当堆中的有效空间被消耗殆尽时,就会STW。

  • 标记:垃圾收集器开始从引用根节点开始遍历,标记所有被引用的对象,一般在对象头中记录为可达对象。
  • 清除:垃圾收集器对堆内存从头到尾进行线性的遍历,如果发现某个对象在其header中没有标记为可达对象,就执行清除。

这里补充说明一下清除的操作,所谓的清除并非是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里。下次有新对象需要加载时,判断垃圾的位置空间是否足够,如果够,就存放。

此算法的缺点是清理之后的空间内存是不连续的,会产生内存碎片,需要维护一个空闲列表来记录那些区域是可用的。也就是说如果使用的垃圾收集器是采用的标记清除算法,那么在分配对象的时候就是使用空闲列表的方式。

4.2.2 复制算法(Copying

为了解决标记清除算法产生内存碎片和效率的问题,出现了复制算法。复制算法的思想是将内存空间分为两块,每次只使用其中的一块,在垃圾回收是将正在使用的内存中活着的对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存块的角色,最后完成垃圾回收。
图片来源于尚硅谷康师傅,侵删

  • 优点:就是不会出现内存碎片的问题,在创建对象的时候使用指针碰撞的方式分配空间。
    另外也有说没有标记和清除的过程,没有清除过程我理解,没有标记过程我不太理解,如果没有标记,如何知道哪个对象是存活的呀,如果有大佬知道还望解惑~~~

  • 缺点:就是需要两倍的空间;对象每次被移动需要修改引用地址;而对于G1这种分成大量的region的GC,每次复制的时候,都需要维护不同region之间对象的引用,不论是内存占用还是时间开销也不小。

根据复制算法的特点,如果内存中存活的对象很多,那复制算法的结果就不会很理想,因为复制的成本很高,所以我们的希望是复制算法可以用在对象存活率很低的内存区域,说到这里是否想到了点什么?没错,就是我们现在hotspot虚拟机使用的年轻代存活对象的复制(存活对象在两个Survivor区的复制和交换),据IBM所做实验,年轻代有80%的对象都是朝生夕死的,这不巧了嘛~ 使用复制算法效率杠杠的~

4.2.3 标记压缩算法(Mark-Compact)

在复制算法的基础上和老年代存活对象多的特性上,演变出了标记压缩算法,这个算法继承了标记清除算法和复制算法的优点,但相应的也会有一定的缺点。
执行过程:

  • 第一阶段和标记-清除算法一样,从根节点开始标记所有被引用的对象。
  • 第二阶段将所有存活的对象压缩到内存的一端,按照顺序排列。
  • 第三阶段就是清除边界外的空间。
    图片来源于尚硅谷康师傅,侵删
    从执行过程可以看出来,此算法的最终效果等同于标记清除算法执行之后,再进行一次内存碎片整理,所以又可以称为标记-清除-压缩算法。和标记清除算法的区别是,标记清除是非移动式,而标记压缩是移动式的。因为有了后面的压缩操作,所以在创建对象分配空间的时候无需维护空闲列表。
  • 优点:刚才也说了,标记压缩算法集众家之所长,结合了标记清除和复制算法的优点,比如:消除了标记清除中内存碎片的问题,消除了复制中2倍内存的问题。
  • 缺点:缺点就是效率问题,以及复制算法的缺点。效率问题就是它比复制算法多了一个标记的阶段,比清除多了一个内存整理的阶段复制算法的缺点就是需要移动对象的引用地址(Java中采用直接指针来进行访问定位,如果采用句柄那就不需要移动,但却需要单独开辟空间去维护句柄地址)。

基础回收算法总结

特点\算法标记-清除(Mark-Sweep)复制(Copying)标记-压缩(Mark-Compact)
速度中等最快最慢
空间开销少(有碎片)对象的2倍大小(无碎片)少(无碎片)
移动对象

五、垃圾收集算法思想

我曾想过一个问题,为啥要好几种算法,就没有一种可以直接走天下的算法嘛?后面才知道我狭隘了,之所以是有各种算法,那必然存在即合理。不同的算法有不同的适应场景,没有最好,只有最合适的算法,就像垃圾收集器一样,常用的经典垃圾收集器有7种,都是根据时代,业务的需要而不断演变而来的,现在的时代也是如此,很多事情都没有标准答案,适合自己的才是最好的。

5.1 分代收集算法

在前面的三种算法中,每种都有自己的特点,那如何发挥这三种算法的特点呢,分代收集算法(其实是一种思想,估计勉强称之为算法吧)应运而生。此算法依据不同对象的生命周期的长短而采用不同的回收算法。这样做的目的就是提高回收的效率,提升用户体验。
在这里插入图片描述

我们知道,几乎所有的GC都是采用分代收集算法来进行垃圾回收。而堆区分为年轻代和老年代,根据不同的区域的特点采用不同的回收方式(前面复制算法也提到过)。

  • 针对年轻代就采用复制算法,因为复制算法正好适合生命周期短,存活率低,回收频率高的区域。
  • 针对老年代可采用标记清除或标记压缩算法,适合老年代生命周期长,存活率高,回收频率低的区域。

5.2 增量收集算法

在上述现有的算法的执行中,应用软件处于一种STW的状态,如果回收时间过长,程序就会有较长时间的停顿,严重影响用户体验和系统的稳定性,为了解决这个问题,增量收集算法诞生了,此算法的思想就是垃圾收集线程每次只收集一小片区域,就切换到应用线程,这样停顿时间就短了,用户体验就变好了,如此不断的反复,直到垃圾收集完成。
总的来说,此算法还是采用的标记和复制算法。但使用这种算法也导致了系统吞吐量的下降,以及线程切换和上下文切换带来的系统开销。

5.3 分区算法

分区算法是JDK9的默认垃圾收集器G1的所使用的垃圾收集思想,目的是为了控制GC产生的停顿时间,将一块打的区域分割为多个小块,每个小块都是独立使用的,独立进行回收,根据用户设置的停顿时间,每次合理的回收若干个价值最大的小块,而不是回收整个堆空间,从而减少一次GC所产生的停顿时间。
图片来源于尚硅谷康师傅,侵删
这种算法的好处是可以灵活的根据设置的停顿时间控制一次性回收多少个小块,换言之,就是哪些可以回收的小块回收时所花费的时间接近设置的停顿时间,就将这些小块回收。

----------------------你知道的越多,不知道的越多-------------------

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值