垃圾回收器相关算法

文章目录

  • 垃圾回收器相关算法
  • 一、标记阶段:引用计数算法
    • 1.1 对象存活判断
    • 1.2 引用计数算法
  • 二、标记阶段:可达性分析算法
    • 2.1 概述
    • 2.3 GC Roots
  • 三、对象finalization机制
    • 3.1 finalization机制
    • 3.2 对象的三种状态
  • 四、MAT与JProfiler的GC Roots溯源
  • 五、清除阶段:标记-清除算法
    • 5.1 背景
    • 5.2 执行过程
    • 5.3 缺点
  • 六、复制算法
    • 6.1 核心思想
    • 6.2 优缺点
  • 七、清除阶段:标记-压缩(Mark-Compact)
    • 7.1 背景
    • 7.2 区别
    • 7.3 优缺点
  • 八、三种算法的小结
  • 九、分代收集算法
    • 9.1 背景
    • 9.2 hotspot虚拟机中的运用
  • 十、增量收集算法、分区算法
    • 10.1 增量收集算法
    • 10.2 分区算法

一、标记阶段:引用计数算法

1.1 对象存活判断

  • 在JVM中当一个对象已经不再被任何的存活对象继续引用时,就可以宣判已经死亡。
  • 判断对象存活的方式有两种:引用计数法算法可达性分析算法。

1.2 引用计数算法

  • 引用计数法比较简单,对每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况。
  • 对于一个对象A,只要有任何一个个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,即表示对象A不可能被使用,可进行回收。
  • 优点:实现简单,垃圾对象便于识别;判断效率高,回收没有延迟性。
  • 缺点:
    • 它需要单独的字段存储计数器,这样会增加存储空间开销。
    • 每次赋值都需要更新计数器,伴随着加法和减法操作,这样增加了时间开销。
    • 引用计数器有一个严重的问题,即无法处理循环引用的情况。这是致命缺陷,导致在java的垃圾回收器中没有使用这类算法。

循环引用:

小结:

  • Java并没有选择使用引用计数算法,因为很难处理循环引用的问题。
  • Python如何解决循环引用?
    • 手动解除:在合适的时机解除引用关系。
    • 使用弱引用weakref,weakref是Python提供的标准库,旨在解决循环引用。

二、标记阶段:可达性分析算法

2.1 概述

相对于引用计数算法,可达性分析算法不仅具备实现简单、执行高效,重要的是解决了循环引用的问题,防止内存泄漏的发生。

2.3 GC Roots

  • GC Roots根集合就是一组必须活跃的引用
  • 基本思路:
    • 可达性分析算法是以根对象为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
    • 使用可达性分析算法后,内存中存活对象都会被根对象集合直接或者间接连接着,搜索所走过的路径称为“引用链”。
    • 如果目标对象没有与任何引用链相连,则是不可达,此对象为垃圾对象。
    • 只有被根对象直接或者间接相连的对象才是存活对象。

在Java中,GC Roots包括以下几类元素

  • 虚拟机栈中引用对象
  • 本地方法栈内引用对象
  • 类静态属性引用对象
  • 字符串常量池中的引用对象
  • 所有被同步锁synchronized持有对象
  • Java虚拟机内部的引用。
    基本数据类型对应的Class对象,一些常驻异常对象(如:NullPointerException、OutOfMemoryError),系统类加载器。
  • 反应Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
  • 除了这些固定的GC Roots集合以外,根据用户所选择的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”加入,共同构成完整的GC Roots集合。比如:分代收集和局部回收。

注意:

如果要使用可达性分析算法来判断内存是否可回收,那么分析工作必须在一个能保障一致性的快照中进行。这点不满足的话分析结果的准确性将无法保证。
这点也是导致GC进行时必须“Stop The World”的一个重要原因。

三、对象finalization机制

3.1 finalization机制

  • Java语言提供了对象终止机制来允许开发人员提供对象被销毁之前的自定义处理逻辑。
  • 垃圾回收无用对象之前,总会先调用这个对象的finalize()方法。
  • finalize()方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理工作,比如关闭文件、套接字和数据库连接等。
  • 不用主动调用对象的finalize()方法,应该交给垃圾回收机制调用。理由:
    • 在finalize()时可能会导致对象复活。
    • finalize()方法的执行时间是没有保障的,它完全有GC线程决定,极端情况下,若不发生GC,则finalize()方法将没有机会执行。
    • 一个糟糕的finalize()会严重影响GC的性能。
  • 由于finalize()方法的存在,虚拟机的对象一般处于三种可能的状态。

3.2 对象的三种状态

如果不在引用链上对象一般来说,此对象需要被回收,但事实上,这个对象有可能在某一个条件下“复活”自己,如果这样,那么对它的回收就是不合理的,为此,定义虚拟机中的对象可能的三种状态。

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

以上3种状态中,是由于finalize()方法的存在,进行区分。只有在对象不可触及时才能被回收。

四、MAT与JProfiler的GC Roots溯源

JProfiler安装包及生成秘钥地址:
链接:https://pan.baidu.com/s/1FXolIAFsW7CU7HowKrHLTQ
提取码:59ig

五、清除阶段:标记-清除算法

5.1 背景

标记-清除算法(Mark-Sweep)是一种非常基础和常见的垃圾收集算法。

5.2 执行过程

当堆中的有效内存空间被消耗尽时,就会停止整个程序(STW),然后进行两项工作:标记、清除。

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

5.3 缺点

  • 效率不算高
  • 在进行GC的时候,需要停止整个应用程序,导致用户体验差。
  • 这种方式清理出来的空闲内存是不连续的,产生内存碎片。需要维护一个空闲列表。

注意:何为清除?

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

六、复制算法

6.1 核心思想

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

6.2 优缺点

优点:

  • 没有标记和清除的过程,实现简单,运行高效。
  • 复制过去以后保证空间的连续性,不会出现“碎片”问题。

缺点:

  • 需要两倍的内存空间。
  • 对于G1这种分拆成为大量region的GC,复制而不是移动,意味着GC需要维护region之间对象的引用关系,不管是内存占用或者时间开销也不小。
  • 对于大量长期存活的对象不适合使用复制算法。

七、清除阶段:标记-压缩(Mark-Compact)

7.1 背景

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

标记-清除算法的确可以用于老年代,但是该算法不仅执行效率低下,而且执行完后会产生内存碎片,因此,标记-压缩算法诞生。

7.2 区别

标记-清除算法是一种非移动式的回收算法,存在内存碎片,内存分配需要维护一个空闲列表。

标记-压缩算法是移动式,解决了内存碎片的问题,内存分配使用的是指针碰撞,但是由于是移动式的,需要维护各引用的地址。

7.3 优缺点

优点:

  • 消除了标记-清除算法中,内存碎片的问题,新对象分配内存时,JVM不需要再维护一个空闲列表,只需要持有一个内存的起始地址即可。
  • 消除了复制算法中,内存减半的高额代价。

缺点:

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

八、三种算法的小结



没有最好的收集算法吗???

答案:没有,具体问题具体分析。
分代收集算法就是具体问题具体处理。

九、分代收集算法

9.1 背景

前面所有算法中,并没有一种算法可以完全代替其他算法,它们都具有自己的优点和特点。因此,分代收集算法应运而生。
分代收集算法,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采用不同的收集方式,以便提高回收效率。一般把java堆分为新生代和老年代,这样可以根据各个代的特点使用不同的回收算法,可以提高垃圾回收的效率。

9.2 hotspot虚拟机中的运用

目前几乎所有的GC都是采用分代收集算法进行垃圾回收。
在hotspot中,基于分代进行不同的垃圾回收算法。

  • 新生代特点:区域相较老年代小,对象生命周期短、存活率低,回收频繁。这个特点比较合适用复制算法。
  • 老年代特点:区域大,对象生命周期长、存活率高,回收不及新生代频繁。这种情况需要用到标记-清除与标记-压缩混合实现。
    • Mark阶段的开销与存活对象的数量成正比。
    • sweep阶段的开销与所关心区域的大小成正比。
    • compact阶段的开销与存活对象的数量成正比。

举例:

以HotSpot中的CMS回收器为例,CMS是基于Mark-Sweep实现的,对于对象的回收效率很高。对于碎片问题,CMS采用基于Mark-Compact算法的Serial Old 回收器作为补偿措施:当内存回收不佳时(碎片导致的Concurrent Mode Failure时),将采用Serial Old执行Full GC以达到对老年代内存的整理。

十、增量收集算法、分区算法

10.1 增量收集算法

上述现有的算法,在垃圾回收过程中,应用软件将处于一个Stop The World的状态,在此状态下,应用程序中用户线程将会被挂起,等待垃圾回收完成,如果回收垃圾较长,挂起的时间将越长,严重影响用户体验和系统稳定性。为解决这一问题,即对实时垃圾收集算法的研究直接导致了增量收集算法诞生。

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

10.2 分区算法

一般来说,在相同条件下,堆空间越大,一次GC时所需要的时间越长,有关GC产生的停顿也就越长。为了更好地控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。
分代算法将按照对象的生命周期长短划分为两部分,分区算法将整个堆划分为连续的不同小区间。
每个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间。

jprofiler工具:https://www.32r.com/soft/73878.html