对比JVM GC算法的读书笔记

400 查看

GC面临的问题有三个:哪些内存需要回收、什么时候回收和怎么回收

  • 哪些内存需要回收,一般有两种方法

    • 引用计数

      • 对每个对象都有个被引用的次数,单被引用的次数为0的时候,就表示对象需要被回收

      • 引用计数的缺点是没有办法解决循环引用导致的内存泄露问题

    • 可达性分析

      • 现在主流的GC方法都使用可达性分析

      • 以GC root为起点,遍历整个树(或者图),如果没有到达过某个对象,则表示这个对象需要被回收

      • GC Root表示引用链的起点,包括函数调用栈中引用的对象、static静态变量和常量的引用对象、方法区中常量引用的对象,本地方法栈中JNI引用的对象

      • 也可以分为栈中的对象、永久代的对象、本地方法栈中JNI引用的对象

  • object什么时候会被回收

    • 一般而言,只要没有引用指向object,就可以在gc的时候被回收(老年代需要在Full GC,永久代需要在设定的几次Full FC)。但是为了丰富引用的种类,以适应各种应用,JDK1.2中加入了4中引用,但是除了强引用,其生命周期会有所不同,生存能力递减。

      • 强引用(Strong Reference)

        • 即普通引用

      • 软引用(Soft Reference)

        • 当触发OutOfMemoryException之前,会触发第二次GC,回收这些Object

      • 弱引用(Weak Reference)

        • 当触发GC的时候,回收这些Object

      • 虚引用(Phantom Reference)

        • 对Object的生存无意义,当Object被回收时,会触发虚引用的通知

    • 对已object在被回收的时候,还会有一个特例,就是定制Object的finalize方法,使之重新与GC Root关联,可以使Object逃脱回收

      • finalize方法在object被GC的时候会被执行

      • 但是下次被GC的时候,不会再次执行finalize方法(finalize方法在生命周期内只会执行一次),所以不会再次逃脱回收

    • 方法区或者Hotspot的永久代也会进行GC

      • 普通常量池常量只要没有引用指向它,就可以被回收

      • 类的回收判定需要满足3个条件

        • 堆中不存在该类实例,即没有指向该类的引用(按照jvm的内存布局,类实例是会有指向类信息的pointer或者handler的。

        • 加载该类的class loader已被回收。(class loader应该存在操纵其加载的类的方法,即还存在某种联系)

        • 该类对应的java.lang.Class对象没有在任何地方被引用,在任何地方无法通过反射访问该类的方法(如果存在这种引用,则可以通过反射的机制,进行访问该类,这会导致错误,有点像race condition,持有缓存,内容却被改变了)

        • 由这3个条件应该可以推论出:类实例、class loader、对应的java.lang.Class类和类信息都存在某种联系,使得可以通过这些东西操纵或者访问类信息。

  • 对于内存怎么回收的问题,内存的回收算法一般分为三种

    • 标记-清除

      • 先对需要进行回收的内存进行标记,然后在进行清除

    • 复制算法

      • 将内存分为相等的两块,每次只使用其中一块,当当前使用的内存块使用完了之后,将存活的对象复制到另一内存块

      • Hotspot将新生代分为一个Eden和两个Survivor区,默认比例为8:1:1

      • minor gc的时候,将eden去和当前survivor区的存活对象复制到另一块survivor区

      • 如果survivor区不足以放置存活的object,使用分配担保机制,部分object将直接进入老年代

    • 标记-整理

      • 先对存活的object进行标记,然后将object进行整理(统一往一端移动),最后将剩余的内存进行回收

    • 分代收集

      • 就是将内存分为不同的区域,一般分为新生代和老年代两部分,然后在不同的部分应用不同的收集算法

      • 一般来说新生代使用复制算法,因为一般只会有少量object存活

      • 老年代使用标记-清除或者标记-整理算法,因为对象存活率高,没有另外的内存进行分配担保

    • Hostspot的算法实现

      • 枚举根节点

        • 为了避免race condition的问题,这里需要进行stop the world的操作,保证一致性

        • 为了提高标记的效率,降低stop the world的时间,商业vm一般都会使用准确式GC,会使用一些方法记录下引用的准确位置,避免全局内存扫描

        • Hostspot使用的是OopMap的数据结构来达到这个目的

        • OopMap的具体使用方法还需要深入了解

      • 安全点

        • 由于能导致OopMap变化的指令非常多,所以Hostspot设置安全点,只有在安全点才会生成OopMap,这也导致只有在安全点才能停顿下来进行GC

        • 如何使所有线程进入安全点的方法有两个

          • 抢先式中断,基本上不用

            • 先把所有线程中断,如果线程不在安全点,则恢复线程让它到达安全点

          • 主动式中断

            • 设置标记位,标记是否正在进行根节点枚举

            • 在安全点轮询标记位

      • 安全区域

        • 如果线程不运行,如sleep或者blocked状态,安全点就没法解决问题

        • 安全区域指在这个区域内,代码引用关系不会变化

        • 当线程到达安全区域时,会对自己进行标记,GC时,就不需要管安全状态的线程

        • 当线程需要离开安全区域,需要检查根节点枚举标记位

  • 垃圾收集器

    • Serial收集器

      • 简单粗暴的串行收集器

      • 适合内存不大,单CPU(可以避免线程交互开销),对stop the world不太敏感的client环境

    • ParNew收集器

      • Parallel New Generation,新生代并行收集器

    • Parallel Scavenge收集器

      • 为控制吞吐量而生

      • 通过控制stop the world的最大时间和gc时间的最大比例来控制gc时间,控制吞吐量

      • 也可以使用自适应参数

      • 算法主要控制stop the world的时间,但是代价是更频繁的gc和总体更长的gc时间总和

    • Serial Old收集器

      • 老年代的串行收集器

      • 使用的是标记-整体算法

    • Parallel Old收集器

      • 老年代的并行收集器

      • 和Parallel Scavenge收集器一起使用,应用于注重吞吐量和CPU敏感的场合

    • CMS收集器

      • Concurrent Mark Sweep,Mark Sweep,基于标记-清除算法

      • 分为初始标记、并行标记、重新标记、并发清除4个阶段

      • 其中初始标记和重新标记 都会stop the world

      • 耗时最长的并发标记和并发清除都可以和用户线程一起运行

      • 缺点

        • 并CPU资源非常敏感,并发情况下,占用部分CPU资源,会导致吞吐量下降

        • 无法处理浮动垃圾,可能出现“concurrent mode failed”而导致另一次Full GC

          • 浮动垃圾,在并发清理阶段出现的垃圾,没法当次GC清除

          • 需要预留老年代空间,给GC时,用户程序使用

        • 标记-清除算法会导致内存碎片

          • 可以设置是否在Full GC时整理内存,多少次Full GC整理一次内存

    • G1收集器

      • Garbage First

      • 优点有:

        • 并行与并发

        • 分代手机

        • 空间整合

          • 整体来看是标记-整理算法,局部来看是标记-复制算法

        • 可预测的停顿

          • 通过设置GC时间的不得超过一定比例,几乎是实时Java(RTSJ)垃圾收集器的特征

      • G1实现

        • G1将内存分为多个相等的区域

        • 老年代、新生代的概念还存在,但是改为了内存区域的集合,而不是固定的区域

        • G1跟踪各个Region区域里面垃圾堆积的价值(即花费时间回收内存的性价比),维护优先列表

        • 由优先列表建立回收时间预测模型,优先收集性价比高的垃圾

        • 由于分区域回收内存,G1的区域之间相互引用,导致可达性分析耗时的问题相比之前的收集器显得更加突出了

          • 通过remember set来避免全堆扫描

          • remember set记录被其它区域引用的情况(待补充)