最新JVM垃圾回收算法详解

目录
  • 1.垃圾回收需要做什么
  • 2.如何判断对象可被回收
    • 2.1 引用计数算法
      • 2.1.2 优点
      • 2.1.2 缺点
    • 2.2 可达性分析算法
      • 2.2.1 算法思路
      • 2.2.2 GC Roots对象(两栈两方法)
      • 2.2.3 优点
      • 2.2.4 缺点
  • 3.判断对象生存还是死亡
    • 3.1 两次标记过程
    • 3.2 finalize()方法
  • 4.HotSpot虚拟机中对象可达性分析的实现
    • 4.1 枚举根节点
    • 4.2 安全点
      • 4.2.1 安全点是什么,为什么需要安全点
      • 4.2.2 安全点的选定
      • 4.2.3 如何在安全点上停顿
    • 4.3 安全区域
      • 4.3.1 为什么需要安全区域
      • 4.3.2 什么是安全区域(Safe Region)
      • 4.3.3 如何用安全区域解决问题
  • 5.垃圾回收算法
    • 5.1 标记清除
    • 5.2 复制算法
    • 5.3 标记整理
    • 5.4 分代收集算法
  • 6.参考

垃圾回收算法

1.垃圾回收需要做什么

Java垃圾回收器需要做的三件事:

1、哪些内存需要回收?即如何判断对象已经死亡;

2、什么时候回收?即GC发生在什么时候?需要了解GC策略,与垃圾回收器实现有关;

3、如何回收?即需要了解垃圾回收算法,及算法的实现--垃圾回收器;

2.如何判断对象可被回收

​ 垃圾收集器对堆进行回收前,首先要确定堆中的对象哪些还"存活",哪些已经"死去"。有两种算法,分别是引用计数算法(Recference Counting)和可达性分析算法(Reachability Analysis)。

2.1 引用计数算法

2.1.1 算法思路

给对象添加一个引用计数器,每当有一个地方引用它,计数器加1;当引用失效,计数器值减1;任何时刻计数器值为0,则认为对象是不再被使用的;

2.1.2 优点

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

2.1.2 缺点

  • 需要单独的字段存储计数器,额外的存储空间开销
  • 需要更新计数器,伴随着加法和减法操作,带来时间开销
  • 无法处理循环引用的情况

循环引用的例子:

public class RefCountGC {
    //这个成员属性唯一的作用就是占用一点内存
    private byte[] bigSize = new byte[5 * 1024 * 1024];//5MB
    Object reference = null;
    public static void main(String[] args) {
        RefCountGC obj1 = new RefCountGC();
        RefCountGC obj2 = new RefCountGC();
        obj1.reference = obj2;
        obj2.reference = obj1;
        obj1 = null;
        obj2 = null;
        //显式的执行垃圾回收行为
        //这里发生GC,obj1和obj2能否被回收? 能被回收,是因为JVM采用的不是引用计数算法。所以obj1和obj2能被回收。这里反向证明了JVM没有采用引用计数算法。
        System.gc();
        try {
            Thread.sleep(1000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

2.2 可达性分析算法

2.2.1 算法思路

​ 通过一系列的GC Roots的对象作为起始点,从这些根节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。

2.2.2 GC Roots对象(两栈两方法)

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象

2.2.3 优点

更加精确和严谨,可以分析出循环数据结构相互引用的情况;主流的编程语言Java c#的选择。

2.2.4 缺点

  • 消耗大量时间

从前面可达性分析知道,GC Roots主要在全局性的引用(常量或静态属性)和执行上下文中(栈帧中的本地变量表);
要在这些大量的数据中,逐个检查引用,会消耗很多时间;

  • GC停顿

可达性分析期间需要保证整个执行系统的一致性,对象的引用关系不能发生变化;
导致GC进行时必须停顿所有Java执行线程(称为"Stop The World");
(几乎不会发生停顿的CMS收集器中,枚举根节点时也是必须要停顿的) Stop The World: 是JVM在后台自动发起和自动完成的; 在用户不可见的情况下,把用户正常的工作线程全部停掉;

3.判断对象生存还是死亡

3.1 两次标记过程

要真正宣告一个对象死亡,至少要经历两次标记过程。

1、第一次标记

在可达性分析后发现到GC Roots没有任何引用链相连时,被第一次标记;并且进行一次筛选:此对象是否必要执行finalize()方法;

(A)、没有必要执行

没有必要执行的情况:(1)、对象没有覆盖finalize()方法;(2)、finalize()方法已经被JVM调用过;这两种情况就可以认为对象已死,可以回收;

(B)、有必要执行

对有必要执行finalize()方法的对象,被放入F-Queue队列中;稍后在JVM自动建立、低优先级的Finalizer线程(可能多个线程)中触发这个方法;

2、第二次标记

GC将对F-Queue队列中的对象进行第二次小规模标记;finalize()方法是对象逃脱死亡的最后一次机会: (A)、如果对象在其finalize()方法中重新与引用链上任何一个对象建立关联,第二次标记时会将其移出"即将回收"的集合;(B)、如果对象没有,也可以认为对象已死,可以回收了;
一个对象的finalize()方法只会被系统自动调用一次,经过finalize()方法逃脱死亡的对象,第二次不会再调用;

3.2 finalize()方法

finalize()是Object类的一个方法,是Java刚诞生时为了使C/C++程序员容易接受它所做出的一个妥协,但不要当作类似C/C++的析构函数;因为它执行的时间不确定,甚至是否被执行也不确定(Java程序的不正常退出),而且运行代价高昂,无法保证各个对象的调用顺序(甚至有不同线程中调用);如果需要"释放资源",可以定义显式的终止方法,并在"try-catch-finally"的finally{}块中保证及时调用,如File相关类的close()方法; 此外,finalize()方法主要有两种用途:

1、充当"安全网"

当显式的终止方法没有调用时,在finalize()方法中发现后发出警告; 但要考虑是否值得付出这样的代价; 如FileInputStream、FileOutputStream、Timer和Connection类中都有这种应用;

2、与对象的本地对等体有关

本地对等体:普通对象调用本地方法(JNI)委托的本地对象;
​ 本地对等体不会被GC回收;​
​ 如果本地对等体不拥有关键资源,finalize()方法里可以回收它(如C/C++中malloc(),需要调用free());​
​ 如果有关键资源,必须显式的终止方法;​
​ 一般情况下,应尽量避免使用它,甚至可以忘掉它。

4.HotSpot虚拟机中对象可达性分析的实现

4.1 枚举根节点

枚举根节点也就是查找GC Roots;

目前主流JVM都是准确式GC,可以直接得知哪些地方存放着对象引用,所以执行系统停顿下来后,并不需要全部、逐个检查完全局性的和执行上下文中的引用位置;

在HotSpot中,是使用一组称为OopMap的数据结构来达到这个目的的;在类加载时,计算对象内什么偏移量上是什么类型的数据;在JIT编译时,也会记录栈和寄存器中的哪些位置是引用;这样GC扫描时就可以直接得知这些信息;

4.2 安全点

4.2.1 安全点是什么,为什么需要安全点

HotSpot在OopMap的帮助下可以快速且准确的完成GC Roots枚举,但是这有一个问题:
运行中,非常多的指令都会导致引用关系变化;如果为这些指令都生成对应的OopMap,需要的空间成本太高;问题解决:

只在特定的位置记录OopMap引用关系,这些位置称为安全点(Safepoint);即程序执行时并非所有地方都能停顿下来开始GC;

4.2.2 安全点的选定

不能太少,否则GC等待时间太长;也不能太多,否则GC过于频繁,增大运行时负荷;所以,基本上是以程序"是否具有让程序长时间执行的特征"为标准选定;"长时间执行"最明显的特征就是指令序列复用,如:方法调用、循环跳转、循环的末尾、异常跳转等; 只有具有这些功能的指令才会产生Safepoint;

4.2.3 如何在安全点上停顿

对于Safepoint,如何在GC发生时让所有线程(不包括JNI线程)运行到其所在最近的Safepoint上再停顿下来? 主要有两种方案可选:

(A)、抢先式中断(Preemptive Suspension)

不需要线程主动配合,实现如下: (1)、在GC发生时,首先中断所有线程; (2)、如果发现不在Safepoint上的线程,就恢复让其运行到Safepoint上; 现在几乎没有JVM实现采用这种方式;

(B)、主动式中断(Voluntary Suspension)

(1)、在GC发生时,不直接操作线程中断,而是仅简单设置一个标志; (2)、让各线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起
​ 而轮询标志的地方和Safepoint是重合的;​ 在JIT执行方式下:test指令是HotSpot生成的轮询指令;​ 一条test汇编指令便完成Safepoint轮询和触发线程中断;

4.3 安全区域

4.3.1 为什么需要安全区域

对于上面的Safepoint还有一个问题: 程序不执行时没有CPU时间(Sleep或Blocked状态),无法运行到Safepoint上再中断挂起;
​ 这就需要安全区域来解决;

4.3.2 什么是安全区域(Safe Region)

指一段代码片段中,引用关系不会发生变化; 在这个区域中的任意地方开始GC都是安全的;

4.3.3 如何用安全区域解决问题

安全区域解决问题的思路:

(1)、线程执行进入Safe Region,首先标识自己已经进入Safe Region;

(2)、线程被唤醒离开Safe Region时,其需要检查系统是否已经完成根节点枚举(或整个GC);如果已经完成,就继续执行;否则必须等待,直到收到可以安全离开Safe Region的信号通知;

这样就不会影响标记结果;

5.垃圾回收算法

5.1 标记清除

标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。

标记: Collector 从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。
清除: Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收。这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里。下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够,就存放。

适用场合:

  • 存活对象较多的情况下比较高效
  • 适用于年老代(即旧生代)

缺点:

  • 容易产生内存碎片,再来一个比较大的对象时(典型情况:该对象的大小大于空闲表中的每一块儿大小但是小于其中两块儿的和),会提前触发垃圾回收而且需要维护一个空闲列表
  • 效率不算高(第一次:标记存活对象;第二次:清除没有标记的对象)
  • 在进行GC的时候,需要停止整个应用程序,导致用户体验差

5.2 复制算法

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

​在新生代,对常规应用的垃圾回收,一 次通常可以回收70%-99%的内存空间。回收性价比很高。所以现在的商业虚拟机都是用这种收集算法回收新生代。

适用场合:

  • 存活对象较少的情况下比较高效
  • 扫描了整个空间一次(标记存活对象并复制移动)
  • 适用于年轻代(即新生代):基本上98%的对象是”朝生夕死”的,存活下来的会很少

优点:

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

缺点:

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

5.3 标记整理

复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。

这种情况在新生代经常发生,但是在老年代更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活的对象较多,复制的成本也将很高。

执行过程:

标记-压缩算法的最终效果等同于标记-清除算法执行完成后,再进行一次内存碎片整理,因此,也可以把它称为标记-清除-压缩(Mark- Sweep-Compact)算法。
二者的本质差异在于标记-清除算法是一种非移动式的回收算法,标记-压缩是移动式的。是否移动回收后的存活对象是一-项优缺点并存的风险决策。
可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一-来,当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销。

优点:

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

缺点:

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

对比三种算法

5.4 分代收集算法

把堆内存分为新生代和老年代,新生代又分为 Eden 区、From Survivor 和 To Survivor。一般新生代中的对象基本上都是朝生夕灭的,每次只有少量对象存活,因此采用复制算法,只需要复制那些少量存活的对象就可以完成垃圾收集;老年代中的对象存活率较高,就采用标记-清除和标记-整理算法来进行回收。

在这些区域的垃圾回收大概有如下几种情况:

在这些区域的垃圾回收大概有如下几种情况:

大多数情况下,新的对象都分配在Eden区,当 Eden 区没有空间进行分配时,将进行一次 Minor GC,清理 Eden 区中的无用对象。清理后,Eden 和 From Survivor 中的存活对象如果小于To Survivor 的可用空间则进入To Survivor,否则直接进入老年代);Eden 和 From Survivor 中还存活且能够进入 To Survivor 的对象年龄增加 1 岁(虚拟机为每个对象定义了一个年龄计数器,每执行一次 Minor GC 年龄加 1),当存活对象的年龄到达一定程度(默认 15 岁)后进入老年代,可以通过 -XX:MaxTenuringThreshold 来设置年龄的值。

当进行了 Minor GC 后,Eden 还不足以为新对象分配空间(那这个新对象肯定很大),新对象直接进入老年代。

占 To Survivor 空间一半以上且年龄相等的对象,大于等于该年龄的对象直接进入老年代,比如 Survivor 空间是 10M,有几个年龄为 4 的对象占用总空间已经超过 5M,则年龄大于等于 4 的对象都直接进入老年代,不需要等到 MaxTenuringThreshold 指定的岁数。

在进行 Minor GC 之前,会判断老年代最大连续可用空间是否大于新生代所有对象总空间,如果大于,说明 Minor GC 是安全的,否则会判断是否允许担保失败,如果允许,判断老年代最大连续可用空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则执行 Minor GC,否则执行 Full GC。

当在 java 代码里直接调用 System.gc() 时,会建议 JVM 进行 Full GC,但一般情况下都会触发 Full GC,一般不建议使用,尽量让虚拟机自己管理 GC 的策略。

永久代(方法区)中用于存放类信息,jdk1.6 及之前的版本永久代中还存储常量、静态变量等,当永久代的空间不足时,也会触发 Full GC,如果经过 Full GC 还无法满足永久代存放新数据的需求,就会抛出永久代的内存溢出异常。

大对象(需要大量连续内存的对象)例如很长的数组,会直接进入老年代,如果老年代没有足够的连续大空间来存放,则会进行 Full GC。

6.参考

1.JVM:引用计数算法和可达性分析算法(https://www.zhifou.net/blogdetail/183)

2.4种JVM垃圾回收算法详解(https://mikechen.cc/7102.html

3.深入理解Java虚拟机(四)之垃圾回收算法(https://www.codenong.com/cs106639755/)

到此这篇关于最新JVM垃圾回收算法详解的文章就介绍到这了,更多相关JVM垃圾回收算法内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 浅谈JVM垃圾回收有哪些常用算法

    一.前言: 垃圾回收: 在未来的JDK中可能G1会为ZGC所取代 先问自己几个问题: 什么是垃圾? 垃圾就是堆内存中(范指)没有任何指针指向的对象实体.不具有可达性. 为什么要回收垃圾? 因为我们的内存是有限的,内存长时间不清理就会导致内存溢出,OOM: 只要是程序正在跑,那么就不断生成新的对象,我们需要GC开辟新的空间分配给新的对象. 我们怎么回收垃圾? 依靠Java的自动内存回收机制,机制的优劣由算法决定: 或者说是机制的适配度由算法和应用场景共同决定. 什么时候回收垃圾? 当堆中的实体对象

  • JVM垃圾回收算法的概念与分析

    前言 在JVM内存模型中会将堆内存划分新生代.老年代两个区域,两块区域的主要区别在于新生代存放存活时间较短的对象,老年代存放存活时间较久的对象,除了存活时间不同外,还有垃圾回收策略的不同,在JVM中中有以下回收算法: 标记清除 标记整理 复制算法 分代收集算法 有了垃圾回收算法,那JVM是如果确定对象是垃圾对象的呢?判断对象是否存活JVM也会有几套自己判断算法了: 引用记数 可达性分析 有了垃圾回收和判断对象存在这两个概念后,再来逐步分析它们. JVM是如何判断对象是否存活的? 要是让开发人员来

  • JVM的垃圾回收算法一起来看看

    目录 垃圾回收算法 概念 1.标记算法 1.1引用计数法(ReferenceCounting) 1.2可达性分析算法(ReachableAnalysis) 2.回收算法 2.1标记清除算法(MarkSweep) 2.2复制算法(Copying) 2.3标记压缩算法(Mark-Compact) 2.4分代回收算法 总结 垃圾回收算法 概念 垃圾回收(Garbage Collection,GC).程序的运行需要资源,无效的对象如果不及时清理就会一直占用资源,所以对内存资源管理就变得十分重要.而Jav

  • 深入理解JVM垃圾回收算法

    目录 一.垃圾标记阶段 1.1.引用计数法 (java没有采用) 1.2.可达性分析算法 二.对象的finalization机制 2.1.对象是否"死亡" 三.使用(MAT与JProfiler)工具分析GCRoots 3.1.获取dump文件 3.2.GC Roots分析 四.垃圾清除阶段 4.1.标记-清除算法 4.2.复制算法 4.3.标记-压缩(整理,Mark-Compact)算法 4.4.以上三种垃圾回收算法对比 4.5.分代收集算法 4.6.增量收集算法 4.7.分区算法G1

  • 最新JVM垃圾回收算法详解

    目录 1.垃圾回收需要做什么 2.如何判断对象可被回收 2.1 引用计数算法 2.1.2 优点 2.1.2 缺点 2.2 可达性分析算法 2.2.1 算法思路 2.2.2 GC Roots对象(两栈两方法) 2.2.3 优点 2.2.4 缺点 3.判断对象生存还是死亡 3.1 两次标记过程 3.2 finalize()方法 4.HotSpot虚拟机中对象可达性分析的实现 4.1 枚举根节点 4.2 安全点 4.2.1 安全点是什么,为什么需要安全点 4.2.2 安全点的选定 4.2.3 如何在安

  • Java 垃圾回收机制详解及实例代码

     Java 垃圾回收机制详解 乍一看,垃圾回收所做的事情应当恰如其名--查找并清除垃圾.事实上却恰恰相反.垃圾回收会跟踪所有仍在使用的对象,然后将剩余的对象标记为垃圾.牢记了这点之后,我们再来深入地了解下这个被称为"垃圾回收"的自动化内存回收在JVM中到底是如何实现的. 手动管理内存 在介绍现代版的垃圾回收之前,我们先来简单地回顾下需要手动地显式分配及释放内存的那些日子.如果你忘了去释放内存,那么这块内存就无法重用了.这块内存被占有了却没被使用.这种场景被称之为内存泄露. 下面是用C写

  • Java基础之垃圾回收机制详解

    一.GC的作用 进行内存管理 C语言中的内存,申请内存之后需要手动释放:一旦忘记释放,就会发生内存泄漏! 而Java语言中,申请内存后会由GC来释放内存空间,无需手动释放 GC虽然代替了手动释放的操作,但是它也有局限性: 需要消耗更多的资源: 没有手动释放那么及时: STW(Stop The World)会影响程序的执行效率 二.GC主要回收哪些内存 (1)堆:主要回收堆中的内存 (2)方法区:需要回收 (3)栈(包括本地方法栈和JVM虚拟机栈):不需要回收,栈上的内存什么时候释放是明确的(线程

  • 掌握PHP垃圾回收机制详解

    php的垃圾回收机制可以简单总结为 引用计数 写时复制 COW机制, 本文主要和大家分享掌握php垃圾回收机制的知识,希望能帮助到大家. 引用计数基本知识 官网的解答如下 每个php变量存在一个叫"zval"的变量容器中一个zval变量容器,除了包含变量的类型和值 ,还包括两个字节的额外信息 is_ref 和 refcount is_ref 是个bool值,用来标识这个变量是否是属于引用集合(reference set).通过这个字节,php引擎才能把普通变量和引用变量区分开来 ref

  • python的内存管理和垃圾回收机制详解

    简单来说python的内存管理机制有三种 1)引用计数 2)垃圾回收 3)内存池 接下来我们来详细讲解这三种管理机制 1,引用计数: 引用计数是一种非常高效的内存管理手段,当一个pyhton对象被引用时其引用计数增加1,当其不再被引用时引用计数减1,当引用计数等于0的时候,对象就被删除了. 2,垃圾回收(这是一个很重要知识点): ①  引用计数 引用计数也是一种垃圾回收机制,而且是一种最直观,最简单的垃圾回收技术. 在Python中每一个对象的核心就是一个结构体PyObject,它的内部有一个引

  • jvm垃圾回收算法详细解析

    目录 前言 几种常用的垃圾回收算法 1.引用计数法 2.根搜索算法 3.标记清除法(Mark-Sweep) 4.复制交换算法(Mark-Sweep) 5.标记压缩算法(Mark-Compact) JVM 分代收集算法 前言 相比C语言,JVM虚拟机一个优势体现在对对象的垃圾回收上,JVM有一套完整的垃圾回收算法,可以对程序运行时产生的垃圾对象进行及时的回收,以便释放JVM相应区域的内存空间,确保程序稳定高效的运行,但在真正了解垃圾回收算法之前,有必要对JVM的对象的引用做一个简单的铺垫 JVM对

  • 深入java垃圾回收的详解

    1.垃圾收集算法的核心思想Java语言建立了垃圾收集机制,用以跟踪正在使用的对象和发现并回收不再使用(引用)的对象.该机制可以有效防范动态内存分配中可能发生的两个危险:因内存垃圾过多而引发的内存耗尽,以及不恰当的内存释放所造成的内存非法引用. 垃圾收集算法的核心思想是:对虚拟机可用内存空间,即堆空间中的对象进行识别,如果对象正在被引用,那么称其为存活对象,反之,如果对象不再被引用,则为垃圾对象,可以回收其占据的空间,用于再分配.垃圾收集算法的选择和垃圾收集系统参数的合理调节直接影响着系统性能,因

  • Java 垃圾回收机制详解(动力节点Java学院整理)

    1. 垃圾回收的意义 在C++中,对象所占的内存在程序结束运行之前一直被占用,在明确释放之前不能分配给其它对象:而在Java中,当没有对象引用指向原先分配给某个对象的内存时,该内存便成为垃圾.JVM的一个系统级线程会自动释放该内存块.垃圾回收意味着程序不再需要的对象是"无用信息",这些信息将被丢弃.当一个对象不再被引用的时候,内存回收它占领的空间,以便空间被后来的新对象使用.事实上,除了释放没用的对象,垃圾回收也可以清除内存记录碎片.由于创建对象和垃圾回收器释放丢弃对象所占的内存空间,

随机推荐