JVM中的垃圾回收详解!几种垃圾回收算法的详细分析说明

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第5天,点击查看活动详情

JVM中的垃圾

  • 定义: JVM内存中随着方法执行创建的对象,在方法执行完成后不再引用,也没有被清除掉,依旧保存在内存中,这种不会被再次引用的对象就是JVM中的垃圾
    • JVM内存中大部分对象都是随着方法的执行而创建,方法执行完毕后这些对象就不会被再次引用. 但是这些对象不会被清除掉,就会导致JVM内存中的对象越来越多
    • 此时,需要一种机制将这些不会被再次引用用的对象清除掉,这些不会被再次引用的对象就是垃圾

JVM中的垃圾定义

  • JVM中通过一种机制清除那些不会再次使用的对象,在清除垃圾对象之前,需要判断哪些对象是可回收垃圾,需要进行垃圾回收清除

引用计数法

原理分析

  • 引用计数法就是为每个创建的对象添加一个引用计数器,使用一块额外的内存区域来存储每个对象被引用的次数
  • 当对象每次被引用时,这个对象的引用计数就增加1. 当对象的一个引用失效时,这个对象的引用计数就减少1
  • 当对象的引用计数为0时,可以认为这个对象不会被再次引用了,这个对象就可以进行垃圾回收了

优点

  • 直观高效
  • 通过引用计数法可以快速直观地定位到需要进行回收的垃圾对象,从而进行清理

问题

  • 无法解决循环引用的问题:
    • 引用计数法无法扫描到循环引用这种特殊情况下的可回收对象
    • 比如两个对象进行循环引用,除此以外,没有被其余任何对象引用,这时,这部分对象也是可回收对象
    • 此时,这种对象无法通过引用计数法进行定位进行垃圾回收
    • 垃圾无法回收,就会导致内存泄漏OOM问题
  • 维护成本比较大:
    • 引用计数法需要额外的内存空间记录每个对象被引用的次数
    • 并且这个引用数也要进行额外的维护

可达性分析法

  • 现在的主流程序设计语言都是通过可达性分析法来判断哪些对象是垃圾回收对象来进行垃圾回收的

原理分析

  • 可达性分析法以所有的GC Roots对象为出发点
    • 如果无法通过GC Roots的引用追踪到的对象,就认为这些对象不会被再次引用了
    • 此时,这些对象就可以进行垃圾回收了

在这里插入图片描述

  • 红色的是存活对象,不可以进行垃圾回收来清除的对象
  • 白色的是可以进行垃圾回收来清除的对象

GC Roots对象

  • GC Roots对象的条件:
    • GC Roots对象一定要在很长一段时间内都不会被GC回收,只有满足这个条件才能作为GC Roots对象
    • 普通的对象不能作为GC Roots对象
  • GC Roots对象的类型:
    • 虚拟机栈中的本地变量所引用的对象
    • 方法区中静态属性引用的对象
    • 方法区中常量引用的对象
    • 本地方法Native方法中引用的对象
    • 虚拟机内部引用的对象. 比如类记载器,基本数据对应的Class对象和异常对象
    • 同步锁Synchronnized持有的所有对象
    • 虚拟机内部情况描述的对象. 比如JXBean,JVMTI中注册的回调,本地缓存代码
    • 垃圾收集器所引用的对象

JVM中的垃圾回收

  • JVM中判断出哪些对象是可回收垃圾对象之后,就要进行垃圾对象的回收清除

标记清除算法

原理分析

  • 标记清除算法: 首先找到内存中的存活对象并对这些对象进行标记,然后统一将未标记的对象进行回收清除
  • 标记清除算法包含两个阶段:
    • 标记阶段: 对存活对象进行标记,确定出所有存活的对象
    • 清除阶段: 将未标记的对象清除

在这里插入图片描述

优点

  • 标记清除算法简单直接,速度非常快
  • 标记清除算法特别适合可回收对象不多的场景

问题

  • 容易造成不连续的内存空间,产生大量碎片,导致频繁的垃圾回收:
    • 清除后的内存中会有很多不连续的空间,也就是造成大量的空间碎片
    • 大量的空间碎片不但不利于下一次的分配,而且创建大对象时,尽管有足够容纳的空间,但是由于空间不连续造成对象无法进行分配,提前进行GC垃圾回收,导致频繁的垃圾回收
  • 性能不稳定:
    • 当内存中需要回收的对象较多,大量的对象都需要进行垃圾回收时,通常情况下,这些垃圾对象可能比较分散,造成清除过程非常耗时,导致垃圾回收清除的效率很低

标记复制算法

  • 标记清除算法最大的问题是会造成空间碎片并且只适合需要回收的对象比较少的场景. 针对这两个问题,就有了标记复制算法,标记复制算法专门针对这两个问题进行了解决
    • 标记清除算法的关注点是可回收的对象
    • 标记复制算法的关注点是存活的对象
  • 标记复制算法通过将存活的对象放到一个固定的区域,然后对其余区域的对象进行统一的清理

原理分析

  • 标记复制算法:
    • 标记复制算法中首件将年轻代内存划分出三块区域:
      • Eden区: 用于存放新创建的对象
      • S1区和S2区: 这两块用于存放存活的对象
    • 标记复制算法回收时的两种情况:
      • 一种是将Eden区和S1区存活的对象复制到S2区
      • 一种是将Eden区和S2区存活的对象复制到S1区

标记复制算法的回收时的两种情况说明S1区和S2区两块区域同时只会有一块区域使用,通过这种方式保证始终会有一块空白的区域用于下次垃圾回收GC时存放存活的对象,所以可以直接一次性清除所有的对象

  • 标记复制算法这样既简单直接同时也保证了清除后内存区域的连续性

在这里插入图片描述

优点

  • 标记复制算法解决了标记清除算法的空间碎片问题
  • 标记复制算法清除可回收对象的效率比较高. 标记清除算法采用移动存活对象的方式,每次清除都是针对一整块内存统一清除
    • 标记复制算法因为移动存活对象时会耗费一定的时间.总体来说,标记复制算法的效率会低于标记清除算法

问题

  • 造成内存的浪费:
    • 标记复制算法需要额外的内存作为复制区,总是会有一块空闲的内存区域无法利用. 造成内存资源的浪费
  • 存活对象较多时效率低:
    • 标记复制算法中复制移动对象的过程非常耗时. 不仅需要移动对象本身,还需要修改引用了这些对象的引用地址
    • 当存活对象越多时,标记复制算法复制移动对象的耗时越明显
    • 因此,标记清除算法适合存活对象比较少的场景
  • 需要担保机制:
    • 由于复制区总会有一块内存空间的浪费.为了控制内存空间的浪费,会将复制区的内存空间分配控制在很小的区间
    • 内存空间分配控制在很小的区间时会导致在存活对象较多时,分配的复制区的内存空间不能够容纳足够的存活对象
    • 这时,需要从其余的地方借用一些空间来保证容纳这些存活对象.这种从其余的地方借用内存的方式就是担保机制

担保机制

  • 一般情况下,标记复制算法将划分出的内存空间按照容量分为大小相等的两块空间. 两块内存空间交替使用,当一块内存空间使用完后,就先将存活的对象逐一复制到另一块未使用的内存空间中,然后将当前使用的这块内存空间一次性回收清除掉. 在出现当前内存空间中全部对象都是存活对象的极端情况时,因为两块内存空间大小相等,所以依旧满足条件
  • 担保机制: 分配担保
    • 为了提高内存利用率,提出来标记复制算法的改进方案:
      • 将内存空间按照一定的比例,比如 8 : 1 : 1 8:1:1 的比例分成3块
      • 较大的一块为Eden,较小的两块为Survivor.两块Survivor交替配合Eden一起使用
      • 垃圾回收时,先将存活对象复制到保留的Survivor中,然后将Eden和原来使用的Survivor一起回收清除掉
      • 由于Survivor的内存空间极小,极端情况下不能够容纳足够的存活对象,为了解决这个问题,就需要使用担保机制,使用一块额外的内存空间进行分配担保来将无法容纳的存活对象存放到额外的内存空间中

标记整理算法

  • 标记清除算法存在空间碎片的问题
  • 标记复制算法解决了空间碎片的问题,但是标记复制算法不适用于存活对象较多的情况
  • 在这样的具体场景和问题下,针对对于存活对象较多的情况进行垃圾回收清除并避免产生空间碎片,就有了标记整理算法

原理分析

  • 标记整理算法: 分为两个阶段
    • 标记阶段: 将存活对象和可回收对象标记出来
    • 整理阶段: 根据标记将存活对象向内存的一端移动,移动完存活对象后再回收清除可回收对象

在这里插入图片描述

优点

  • 标记整理法解决了标记清除法的空间碎片的问题
  • 标记整理法不需要空闲的内存空间,解决了标记复制法的内存浪费的问题
  • 标记整理法非常适合存活对象较多的场景

问题

  • 标记整理算法的性能较低:
    • 标记整理算法的复杂度大,执行步骤多
    • 标记整理算法在移动对象时不仅需要移动对象,还需要额外维护对象的引用的地址,这个过程需要对内存经过几次的扫描定位才能够完成

分代收集算法

  • 分代收集算法是目前大部分JVM的垃圾收集器使用的算法
  • 分代收集算法几种垃圾回收算法的集合体,指的是对不同的代使用不同的垃圾回收算法进行垃圾回收清除

原理分析

  • 分代收集算法:
    • 根据对象存活的生命周期将内存划分为若干个不同的区域. 对不同的代使用不同的算法实现
    • 一般将堆区分为新生代Young Generation, 老年代Tenured Generation和永久代Permanent Generation
      • 新生代Young Generation: Eden
        • 存放新创建的对象,对象生命周期非常短,几乎是使用完就立即回收
        • 使用标记复制算法进行垃圾回收
      • 老年代Tenured Generation: Old
        • 新生代区多次回收后存活下来的对象就会被移到老年代区
        • 由于对象存活率高,几乎没有额外的空间进行分配担保,使用标记清除算法或者标记整理算法
      • 永久代Permanent Generation: Java 8以后移除. 主要存放加载的类的信息,生命周期长,几乎不会被回收

垃圾回收总结

  • 这几种垃圾回收算法都有各自的特点,没有任何一种算法是完美的.需要根据具体的使用场景选择合适的垃圾回收算法

标记清除算法

  • 特点: 简单直接,收集速度快.但是会产生空间碎片,导致之后的频繁的进行垃圾回收GC操作
  • 使用场景:
    • 存活对象较多.只有小部分垃圾对象需要进行回收操作的
    • 老年代的垃圾回收. 老年代中存活对象通常比可回收对象要多

标记复制算法

  • 特点: 垃圾收集速度较快,可以避免空间碎片.但是会造成内存空间浪费,在存活对象较多的情况下非常耗时,并且需要担保机制
  • 使用场景:
    • 存活对象较少的
    • 新生代的垃圾回收. 新生代中存活对象较少.通常新生代的垃圾回收器都会使用标记复制算法

标记整理算法

  • 特点: 和标记清除算法相比,不会造成空间碎片.和标记复制算法相比,浪费内存空间.但是标记整理算法的效率低于标记清除算法和标记复制算法
  • 使用场景:
    • 内存资源紧缺,需要避免空间碎片问题的
    • 老年代通常会使用标记整理算法避免空间碎片问题

猜你喜欢

转载自juejin.im/post/7086434645637595143