简单易懂JVM系列(二)-垃圾回收概述及算法

一 概述

在1960年麻省理工学院诞生的Lisp是第一门使用动态分配和垃圾收集技术的语言,其作者提出了关于垃圾收集的三个经典问题。

  • 哪些内存需要回收?
  • 什么时候回收?
  • 如何回收?

这三个问题基本贯穿了整个垃圾回收相关技术,我们可以通过回答这三个问题来融会贯通整个垃圾回收技术。

image-20230822115116236

二、对象已死?

Java内存运行时区域中的程序计数器、虚拟机栈、本地方法栈3个区域是线程私有的,随线程生灭而生灭。在基于概念模型中,编译器时这三个区域的内存分配和回收具有确定性,因此不需过多这部分内存回收。

Java堆和方法区的内存分配和回收是动态的,程序创建的对象只能在运行期间才能知道。因此接下来主要探讨如何对这部分内存进行分配和回收。

2.1 引用计数法

**引用计数法:**对每一个对象保存一个整型的引用计数器,每当有一个地方引用它,计数器就加1,引用失效时就减1。

优点:

  • 实现简单,判定效率高。
  • 垃圾对象便于辨识,回收没有延迟性。

缺点:

  • 占用额外内存空间存储字段存储计数器。
  • 增加额外时间开销去保证计数器正确工作,例如增减操作。
  • 无法解决对象之间相互引用问题。例如A引用B,B引用A,这样引用计数法就不会判定A和B为可回收对象。

小结:

  • 许多领域支持引用计数法,例如Python语言,但主流Java虚拟机没有使用引用计数法管理内存。
  • 具体使用需要参考具体业务场景。
2.2可达性分析算法

可达性分析算法基本思路:

  • 首先将一系列称为“GC Roots”的根对象作为起始节点集,再从这些节点根据引用关系向下搜索被根对象集合所连接的目标对象是否可达。
  • 目标对象搜索过程所走过的路径称为“引用链”。
  • 如果某个对象到GC Roots间没有任何引用链项链,则该对象不在可能被使用,可以标记为垃圾对象。例如下图Object5、Object6、Object7通过GC Roots是不可达的,因此判定为可回收对象。

image-20230827110537216)

可作为GC Roots对象包括以下几种

  • 虚拟机栈中引用的对象
    • 比如:各个线程被调用的方法中使用到的参数、局部变量等。
  • 方法区中类静态属性引用的对象
    • 比如:Java 类的引用类型静态变量
  • 本地方法栈内 JNI(通常说的Native方法)引用的对象
  • 方法区中常量引用的对象
    • 比如:字符串常量池(String Table)里的引用
  • 所有被同步锁 synchronized 持有的对象
  • Java 虚拟机内部的引用。
    • 基本数据类型对应的 Class 对象,一些常驻的异常对象(如:NullPointerException、OutOfMemoryError),系统类加载器。
  • 反映 java 虚拟机内部情况的 JMXBean、JVMTI 中注册的回调、本地代码缓存等。

除了这些固定的 GC Roots 集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整 GC Roots 集合。例如:分代收集和局部回收(PartialGC)。

当只针对 Java 堆中的某一块区域进行垃圾回收(比如:典型的只针对新生代),必须考虑到内存区域是虚拟机自己的实现细节,更不是孤立封闭的,这个区域的对象完全有可能被其他区域的对象所引用,这时候就需要一并将关联的区域对象也加入 GCRoots 集合中去考虑,才能保证可达性分析的准确性。

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

2.3 生存或死亡?

Java语言提供了对象终止(finalization)机制允许开发人员提供对象被销毁之前的自定义处理逻辑。

即使在可达性分析算法中被判定为不可达对象,也不是非死不可的,这时它们暂时处于缓刑阶段。因此,定义虚拟机中的对象可能的三种状态。如下:

  • 可触及的:从根节点开始,可以到达这个对象。
  • 可复活的:对象的所有引用都被释放,但是对象有可能在finalize()中复活。
  • 不可触及的:对象的finalize()被调用并且没有复活,或者没有覆盖finalize()方法,那么就会进入不可触及状态(不可复活状态),因为finalize()只可调用一次。

具体过程:

判定一个对象 objA 是否可回收,至少要经历两次标记过程:

  1. 如果对象 objA 到 GC Roots 没有引用链,则进行第一次标记。
  2. 进行筛选,判断此对象是否有必要执行 finalize()方法。
  3. 如果对象 objA 没有重写 finalize()方法,或者 finalize()方法已经被虚拟机调用过,则虚拟机视为“没有必要执行”,objA 被判定为不可触及的。
  4. 如果对象 objA 重写了 finalize()方法,且还未执行过,那么 objA 会被插入到 F-Queue 队列中,并在稍后由一条虚拟机自动建立的、低调度优先级的Finalizer线程去执行finalize()方法。
  5. finalize()方法是对象逃脱死亡的最后机会,稍后 GC 会对 F-Queue 队列中的对象进行第二次小规模标记。如果 objA 在 finalize()方法中与引用链上的任何一个对象建立了联系,那么在第二次标记时,objA 会被移出“即将回收”集合。之后,对象如果再次出现没有引用存在的情况。在这个情况下,finalize 方法不会被再次调用,对象会直接变成不可触及的状态,也就是说,一个对象的 finalize 方法只会被调用一次
2.4 回收方法区

方法区中回收内容

  • 废弃的常量,假如String s = “java”,s进入常量池,但当前系统没有一个字符串对象值是“java”。换句话说就是已经没有任何字符串对象引用常量池中“java”常量,且虚拟机中没有其他方法引用这个字面量。
  • 不再使用的类型,判断一个类型为不再回收类型需要满足三个条件。
    • 该类的所有的实例都已经被回收,也就是java堆中不存在该类及其任何派生子类实例。
    • 加载该类的类加载器都已经被回收。
    • 该类对应的java.lang.Class对象没有任何地方被引用。
    • java虚拟机被允许对满足三个条件的无用类回收,和对象不一样,并不意味着没有引用了就必然回收。

三、垃圾收集算法

3.1 分代收集理论

该理论实质上是一套符合大多数程序运行实际情况的经验法,它建立在俩个分代假说之上:

  1. 弱分代假说:绝大多数对象都是朝生夕灭的。
  2. 强分代假说:熬过越多次垃圾收集过程的对象就越难消亡。

俩个假说奠定了多款常用垃圾收集器设计原则:收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄分配到不同区域之中存储

在Java堆划分出不同区域之后,垃圾收集器才可以每次只回收其中某一个或某些部分区域-----因此才有了Minor GC、Major GC、Full GC这样回收类型的划分;也才能够针对不同区域安排与里面存储对象存亡特征相匹配的垃圾收集算法----因此出现了”标记-清除算法“、”标记-复制算法“、”标记-整理算法“等针对性的垃圾收集算法。接下来就让我们进入垃圾收集算法学习吧。

image-20230828124230947

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

当堆中的有效内存空间被耗尽时候,就会停止STW(stop the world),然后开始实施标记-清除算法。

算法如同名字一样需要分为标记、清除俩步:

  1. 标记:从GC Roots出发开始遍历,标记所有被引用的对象。一般是在对象的Header中标记为可达对象。
  2. 清除:对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收。

image-20230828124230947

缺点:

  • 执行效率不稳定。Java堆中大量对象且其中大部分需要回收,就会产生大量的标记清理操作,执行效率就会下降。
  • 内存碎片化问题。标记、清除操作之后会产生大量内存碎片。
3.3 标记-复制算法(Copying)

背景为了解决标记-清除算法面对大量可回收对象时,执行效率低的问题,1969年Fenochel提出了一种称为“半区复制”的垃圾收集算法,它将可用内存分为大小相等俩块,每次只使用其中一块。当这一块内存用完时,就将存活对象复制到另一块内存中,并将之前内存存储对象全部清除掉。

**核心思想:**将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收

image-20230828125823294

优点:

  • 没有标记和清除过程,执行效率稳定
  • 避免空间碎片化问题

缺点:

  • 占用俩倍内存
  • 对于 G1 这种分拆成为大量 region 的 GC,复制而不是移动,意味着 GC 需要维护 region 之间对象引用关系,不管是内存占用或者时间开销也不小
  • 如果内存中大量对象是存活的,那么就产生大量的内存间复制的开销。
3.4 标记整理算法(Mark-Compact)

背景:复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生,但是在老年代,更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活对象较多,复制的成本也将很高。因此,基于老年代垃圾回收的特性,需要使用其他的算法。

执行过程:

  1. 从根节点出发开始标记所有被引用对象
  2. 将所有存活对象压缩到内存的一端,按顺序排放
  3. 清理边界外所有空间

image-20230828141353606

二者的本质差异在于标记-清除算法是一种非移动式的回收算法,标记-压缩是移动式的。是否移动回收后的存活对象是一项优缺点并存的风险决策。

可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来,当我们需要给新对象分配内存时,JVM 只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销。

优点:

  • 消除了标记-清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM 只需要持有一个内存的起始地址即可。
  • 消除了复制算法当中,内存减半的高额代价。

缺点:

  • 从效率上来说,标记-整理算法要低于复制算法。
  • 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址
  • 移动过程中,需要全程暂停用户应用程序。即:STW
3.4.1 指针碰撞

如果内存空间以规整和有序的方式分布,即已用和未用的内存都各自一边,彼此之间维系着一个记录下一次分配起始点的标记指针,当为新对象分配内存时,只需要通过修改指针的偏移量将新对象分配在第一个空闲内存位置上,这种分配方式就叫做指针碰撞(Bump tHe Pointer)。

3.5 分代收集算法

分代收集算法,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。

目前几乎所有的 GC 都采用分代收集算法执行垃圾回收的。在HotSpot中,内存回收算法必须结合年轻代和老年代。

年轻代特点(Young Gen):区域相对老年代较小,对象生命周期短、存活率低,回收频繁。年轻代中使用标记-复制算法进行内存回收速度最快,也最为合适。

老年代特点(Tenured Gen):区域较大,对象生命周期长、存活率高,回收不及年轻代频繁。老年代一般是由标记-清除或者是标记-清除和标记-整理的混合实现。

  • Mark 阶段的开销与存活对象的数量成正比。
  • Sweep 阶段的开销与所管理区域的大小成正相关。
  • Compact 阶段的开销与存活对象的数据成正比。
3.6 增量收集算法

**背景:**上述现有的算法,在垃圾回收过程中,应用软件将处于一种 Stop the World 的状态。在 Stop the World 状态下,应用程序所有的线程都会挂起,暂停一切正常的工作,等待垃圾回收的完成。如果垃圾回收时间过长,应用程序会被挂起很久,将严重影响用户体验或者系统的稳定性。为了解决这个问题,即对实时垃圾收集算法的研究直接导致了增量收集(Incremental Collecting)算法的诞生。

基本思想:

  • 如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成
  • 总的来说,增量收集算法的基础仍是传统的标记-清除和复制算法。增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作

缺点:

  • 线程切换和上下文转换的消耗,导致垃圾回收的总体成本上升,系统吞吐量下降。
3.7 分区算法

一般来说,在相同条件下,堆空间越大,一次 GC 时所需要的时间就越长,有关 GC 产生的停顿也越长。为了更好地控制 GC 产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次 GC 所产生的停顿。

分代算法将按照对象的生命周期长短划分成两个部分,分区算法将整个堆空间划分成连续的不同小区间。每个区间独立使用和回收。

:本文结合尚硅谷老师讲解和个人理解编写而成,如有错误麻烦指出,十分感谢!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值