JVM|垃圾回收算法


文章目录

  • 垃圾回收算法
  • 垃圾标记阶段算法
    • 引用计算法
    • 可达性分析算法
  • 垃圾回收阶段算法
    • 标记 - 清除算法
      • 执行过程
      • 优缺点
    • 标记 - 复制算法
      • 执行过程
      • 优缺点
      • 应用场景
    • 标记 - 整理算法
      • 执行过程
      • 优缺点
  • 回收算法小结

垃圾回收算法 垃圾收集算法是分为两大类的。
  1. 垃圾标记阶段算法
  2. 垃圾回收阶段算法
垃圾标记阶段算法 垃圾标记阶段:主要是为了判断对象是否存活
在堆里面存放着几乎所有的Java对象实例,在GC执行垃圾回收之前,首先需要区分出内存中那些是存活对象,那些是已经死亡的对象。
只有被标记为已经死亡的对象,GC才会在执行垃圾回收的时候,释放掉其所占用的内存空间,因此这个过程我们就称为 垃圾标记阶段
那么在JVM中究竟是如何标记一个死亡对象呢?
简单的来说,当一个对象已经不再被任何的存活对象继续引用时,就可以宣判为已经死亡。
判断对象存活一般有两种方式:引用计算法和可达性分析算法
引用计算法 引用计算法(Reference Counting)是比较简单的,是对每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况
对于一个对象A,只要有任何一个对象的引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,那就表示对象A是不再被引用了,就可以进行回收了。
优点:
  1. 实现简单,垃圾对象便于辨识
  2. 判定效率高,回收没有延迟
缺点
  1. 它需要一个单独的字段存储计数器,这样的做法增加了存储空间的开销
  2. 每次赋值都需要更新计数器,伴随着加法额减法的操作,这增加了时间开销。
  3. 引用计数器还有一个严重的问题,即无法处理循环应用的情况。这是一条致命的缺点,就会导致Java的垃圾回收器中没有使用这类算法。
当成功 区分内存中存活对象和死亡对象之后,接下来就是GC的任务就是执行垃圾回收,来释放掉无用对象所占用的内存空间。以便有足够的空间来为新的对象分配内存。
【JVM|垃圾回收算法】垃圾收集算法是可以划分为 “引用计数式垃圾收集” 和 “追踪式垃圾收集” 两大类,这两大类也常常被称作“直接垃圾收集”和“间接垃圾收集”。由于引用计数式垃圾收集在主流的虚拟机中均未涉及,所在在这只说明追踪式垃圾收集。
可达性分析算法 可达性分析算法:也可以称为根搜索算法、追踪性垃圾收集
  1. 相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生。
  2. 相较于引用计数算法,这里的可达性分析就是 Java、C#选择的。这种类型的垃圾收集通常也叫作追踪性垃圾收集(Tracing Garbage Collection)
在当前主流的商用程序语言(Java、C#)的内存管理子系统,都是通过可达性分析算法来判定对象是否存活。
基本思路
这个算法的基本思路就是通过一系列的“GC Roots”的根对象作为起始节点集合,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”,如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象时不可达的,那就说明这个对象时不可能再被引用的。
JVM|垃圾回收算法
文章图片

在Java技术体系里面,固定可作为GC Roots的对象包括以下几种:
  1. 在虚拟机栈(栈帧中的本地变量表)中引用的对象。
    比如:当前正在运行的方法所使用到的参数、局部变量、临时变量等。
  2. 在方法区中类静态属性引用的对象。
    例如:Java类的引用类型静态变量。
  3. 在方法区中常量引用的对象。
    例如:字符串常量池(String Table)里的引用。
  4. 在本地方法栈中JNI(即通常所说的Native)引用的对象。
  5. Java虚拟机内部的引用。
    例如:基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
  6. 所有被同步锁(synchronized 关键字)持有的对象。
  7. 反映Java虚拟机内存情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
除了这些固定的GC Roots集合以外,根据用户所选用的垃圾收集器以及当前回收的区域不同,还可以有其他对象“临时性”地加入,共同构成完整的GC Roots集合。
总结: 简单一句话就是,除了堆空间的周边,(比如:虚拟机栈、本地方法栈、方法区、 字符串常量池等地方对堆空间进行引用的,)都可以作为 GC Roots 进行可达性分析。
垃圾回收阶段算法 目前在JVM中比较常见的三种垃圾收集算法是:
  1. 标记 - 清除算法
  2. 标记 - 复制算法
  3. 标记 - 整理算法
标记 - 清除算法 这个算法是最早出现也是最基础的垃圾收集算法,后续的收集算法大多都是以标记 - 清除算法为基础,对其缺点进行改进而得到的。
执行过程
当堆中的有效内存空间(available memory)被耗尽的时候,就会体质整个程序(也叫做 stop the world),然后进行两项工作,第一项则是标记,第二项则是清除。
标记: Collector从引用根节点(GC Roots)开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。(注意:标记的是被引用的对象,也就是可达对象,并非标记的是即将被清除的垃圾对象。)
清除: Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在Header中没有被标记为可达对象,就对其进行回收。
JVM|垃圾回收算法
文章图片

优缺点
优点:非常基础和常见的垃圾收集算法容易理解
缺点:
  1. 执行效率不稳定
    如果Java堆中包含了大量的对象,而且其中大部分都是需要回收的,这时必须大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降级
  2. 内存空间碎片化
    标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大的对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
  3. 在进行GC的时候,需要停止整个应用程序,用户体验较差。
清除的具体操作:
这里所说的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里。下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够,就存放(就是直接覆盖在原有的地址上)。
标记 - 复制算法 标记 - 复制算法常被简称为复制算法。
主要是为了解决标记 - 清除算法面对大量可回收对象时执行效率低的问题。
执行过程
它将可用内存划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次性清理掉。交换两个内存的角色,最后完成垃圾的回收。
JVM|垃圾回收算法
文章图片

优缺点
优点:
没有标记和清除过程,实现简单,运行高效。
复制过去以后保证空间的连续性,不会出现“碎片的问题”。
缺点:
这个算法的确定也是相当的明显,将可用内存缩小为原来的一半,空间浪费巨大。
对于G1这种分拆成为大量 region 的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用或者时间开销也不小。
应用场景
如果系统中的垃圾对象很多,复制算法需要复制的存活对象数量并不会太多,那么效率就会显得较高。
如果在老年代中,因为大量的对象存活,那么复制的对象将会有很多,效率很低。
在新生代当中,对常规应用的垃圾回收,一般通常是可以回收70% ~ 99%的内存空间。
因为回收性价比很高。所以现在的商用Java虚拟机大多都优先采用了这种收集算法去回收新生代。
IBM公司曾有一项专门研究新生代的“朝生夕灭”的特点做了更量化的诠释——新生代中的对象有98%都是熬不过第一轮收集。因此不需要按照1:1的比例来划分新生代的内存空间。
在主流的HotSpot虚拟机中默认Eden和Survivor的大小比例就是8:1,分为伊甸园和幸存者区。
但是任何人都没有办法保证每次回收都只有不多于10%的对象存活,因此这种回收还有一个充当罕见情况的“逃生门”的安全设计,当Survivor空间不足以容纳一次Minor GC 之后存活的对象时,就需要依赖其他内存的区域(实际大多就是老年代)进行分配担保。
标记 - 整理算法 标记 - 复制算法在对象存活率较高时就要进行较多的复制操作,效率将会降低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般都不能直接选用这种算法。
标记 - 清除算法的确可以应用在老年代中,但是该算法不仅执行效率低、下,而且在执行完内存回收后还会产生内存碎片,所以 JVM 的设计者需要在此基础之上进行改进。
针对老年代对象的死亡特征,在1974年 Edwaed Lueders提出了另外一种有针对性的“标记 - 整理”(Mark-Compact)算法。
执行过程
第一阶段是和标记 - 清除算法是一样的,从根节点开始标记所有被引用的对象。
第二阶段是将所有的存活的对象都向内存空间的一段移动,然后直接清理掉边界以外的内存。
JVM|垃圾回收算法
文章图片

标记 - 清除算法与标记 - 整理算法的本质差异在于前者是一种非移动式的回收算法,而后者是移动的。因此,也可以把它称为标记 - 清除 -压缩算法(Mark - Sweep - Compact)算法。
而是否移动回收后的存活对象是一项优缺点并存的风险决策。
可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存就会被清理掉。如此一来,当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可。这比维护一个空闲列表显然少了许多开销。
优缺点
优点:
消除了标记 - 清除算法中,内存区域分散的特点,我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可。
消除了复制算法当中,内存减半的高额代价。
缺点:
从效率上来说,标记 - 整理算法要低于复制算法。
移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址。
移动过程中,需要全程暂停用户应用程序。即:Stop The World(STW)
回收算法小结 从效率上来讲,标记 - 复制算法是当之无愧的老大,但是却浪费了太多的内存。
而为了尽量兼顾上面提到的上指标,标记 - 整理算法相对来说更平滑一些,但是效率上却不尽人意,它比标记 - 复制算法多出了一个标记阶段,比标记 - 清除多了一个整理内存的阶段
标记 - 清除 标记 - 复制 标记 - 整理
速度 中等 最快 最慢
空间开销 少(但会堆积碎片) 通常可用内存是总内存一半(不堆积碎片) 少(不堆积碎片)
移动对象
番外篇: —> 了解对象的自我拯救
上一篇: —>Java垃圾回收
下一篇: —>JVM垃圾回收的分代收集思想

    推荐阅读