垃圾收集(GC)

发布于:2022-11-15 ⋅ 阅读:(517) ⋅ 点赞:(0)

判定一个对象是否存活的方式

1、引用计数法

  • 过程:对象中添加一个计数器,有地方引用就+1,引用失效就-1,为0就代表对象不再存活,可以被回收。
  • 优点:原理简单,判定效率高
  • 缺点:很多例外情况,譬如循环引用问题就不好处理

2、可达性分析法

  • 过程:通过一系列“GC Roots”的根对象作为起始节点集,从这些节点开始根据引用关系向下搜索形成“引用链”,如果某个对象到“GC Roots”没有任何“引用链”,或者从“GC Roots”到这个对象不可达,则表示这个对象可以被回收

  • Java中的“GC Roots”

    • 1、虚拟机内部的引用,参与构建整个JVM体系的部件或类,如:类加载器、系统定义的异常类NullPointerException/OutOfMemoryError等
    • 2、反映虚拟机内部的JMXBean、JVMTI中注册的回调、Native方法缓存等
    • 3、处于方法区中的类静态属性引用的对象和常量引用的对象,因为这些对象是构建这个类的基础
    • 4、在虚拟机栈中引入的对象,因为在这个空间中的对象表示对应的方法在执行中,被占用
    • 5、在本地方法栈中JNI引用的对象,理由同 1
    • 6、被同步锁持有的对象,即被synchronize修饰的对象,因其未释放,必然表示有一部分代码未执行完成,被占用
    • 7、因选择的GC收集器和策略需要,临时加入的一些对象
  • 优点:解决某些特殊条件下的判定问题,如循环引用

  • 缺点:判定较慢,效率不高,可能会出现对象过多引起膨胀

  • 被可达性分析算法判定为不可达的对象后,第一次被标记,然后进行一次筛选,筛选的依据是看该对象是否有必要执行finalize()方法,如果已经执行过,或者其并未复finalize()方法,则判定为“没必要执行”。否则,则需要执行finalize()方法,此时会将这个对象放入F-Queue队列中,然后JVM的Finalizer线程去执行finalize()方法。finalize()在执行时只是加一个标记,随后会再去判断这个对象的引用链是否能连到“GC Roots”上去,如果能连上去,则这个对象将会被移出F-Queue队列,否则就会被回收了。

  • 速记:标记(不可达状态)-> 筛选(是否执行finalize)-> 进队(F-Queue)-> 筛选(仍无引用链到Roots)-> 回收(这个对象)

四种引用类型

  • 强引用,速记关键字:new
    只要关系还存在,垃圾收集器就不会回收被引用的对象

  • 软引用,速记关键字:有用但非必须
    内存不足时,会被回收

  • 弱引用,速记关键字:想收就收
    只要垃圾处理器一工作,就会被回收

  • 虚引用,速记关键字:收时通知
    在垃圾收集器回收时,会收到系统通知

垃圾收集算法

分代收集理论

  • 建立于两个假说之上:
    1、绝大多数对象存活时间都不会很长
    2、存活越多回收周期的对象越难消亡
    3、跨代引用相对于同代引用来说仅占极少数

标记-清除

  • 标记阶段:判定对象是否属于垃圾,即是否需要回收
  • 清除阶段:将判定可以回收的对象清除
  • 缺点:
    1、执行效率不稳定,对象数量越大,执行越慢
    2、产生大量不连续的内存碎片,不利于以后大对象的分配

标记-复制(新生代采用)

  • 标记阶段:标记存活的对象

  • 复制阶段:将存活的对象复制到另一块内存区域,原来的内存区域被清除

  • 缺点:内存空间利用率较低

  • Hotspot虚拟机包含1个Eden + 2个Survivor区域,默认Eden和Survivor的大小比例是8:1。

  • 分配担保机制:如果另一块Survivor空间没有足够的空间存放上一次新生代收集下来的存活对象,则这些对象将直接被分配到老年代,老年代充当担保方

标记-整理(老年代采用)

  • 标记阶段:标记存活的对象
  • 整理阶段:让存活的对象都向内存空间的一端移动,然后直接清理掉边界以外的内存
  • 缺点:移动会导致用户线程的暂停

Hotspot的算法细节

  • 1、枚举GC Roots时,必须要停顿

  • 2、OpsMap存放哪些对象引用处于哪些内存块

  • 3、并非任意点都可以执行垃圾回收,而是在安全点时刻执行

  • 4、确定最近的安全点:抢先式(几乎不用)和主动式,在中断标志为true的时候,线程就在最近的安全点上主动挂起

  • 5、在用户线程Sleep或Blocked状态时,无法按照安全点的判别方法让线程挂起,此时引入安全区域的概念,确定GC的时机,安全区域就是引用关系不发生变化的代码段

  • 6、记忆集:用于记录非收集区域指向收集区域的指针信息,有助于缩减GC Roots扫描的范围。
    卡表Card Table:记忆集的一种具体实现,Card Page大小是512字节,一个卡页内存中的一个或多个对象的至少一个字段存在跨代指针,则,这个卡表的值为 1,我们则称之为脏元素。

  • 7、“写屏障”:用来维护卡表状态,相当于在虚拟机层面上对类型进行赋值操作的一个AOP切面。分为写前屏障和写后屏障。高并发下存在“伪共享”的问题,通过-XX:+UseCondCardMark变量控制是否开启卡表更新的条件判断,开启能解决伪共享的问题。

    • “伪共享”:多变量共享同一个缓存行,此时会相互影响
  • 8、并发条件下的可达性判定:存在两个问题

    • 1)赋值器插入了一条或多条从黑色对象到白色对象的新引用,导致对象“消失”,即将对象误标记为白色
    • 2)赋值器删除了全部从灰色对象到该白色对象的直接或间接引用,也会将对象误标记为白色
    • 于是,形成两种解决方案:增量更新和原始快照

典型垃圾收集器

  • Serial收集器

    • JDK1.3.1之前新生代收集器的唯一选择,单线程工作,收集是会暂停所有工作线程,直到收集结束
    • 简单,消耗内存最小、最高的单线程收集效率
    • 客户端模式下默认的新生代收集器
  • ParNew收集器

    • Serial收集器的多线程版本,JDK7之前遗留系统中首选的新生代收集器,能与CMS收集器配合使用
    • 后来其功能并入CMS收集器,专门处理新生代垃圾回收
  • Parallen Scavenge收集器

    • 基于标记-复制算法实现,可以控制吞吐量的垃圾收集器,有两个参数可以控制吞吐量:
      -XX:MaxGCPauseMillis
      -XX:GCTimeRadio
    • 自动化调节的参数:
      -XX:UseAdaptiveSizePolicy,激活后可动态调节,找到合适的吞吐量和停顿时间
  • Serial Old收集器

    • Serial收集器的老年代版本,客户端模式下使用
  • Parallen Old收集器

    • Parallen Scavenge收集器的老年代版本,基于标记-整理算法实现。
  • CMS收集器

    • 四个步骤

      • 初始标记

        仅标记与GC Roots直接连接的对象,很快,但需要停止用户线程
        
      • 并发标记

        以GC Roots直接关联的对象集合为起点,遍历整个对象图,耗时最长
        
      • 重新标记

        修正并发标记可能造成的对象“消失”的问题
        
      • 并发清除

        删除和清理已经判断死亡的对象
        
    • 三个缺点

      • 默认启动的回收线程数 n = (CPU核心数 + 3) / 4,对于核心数少于4个的机器时,CMS对用户程序的影响很大,降低应用程序的吞吐性能
      • 无法处理重新标记过程后新产生的“浮动垃圾”,导致Full GC,对应一个参数:
        -XX:CMSInitiatingOccupancyFraction
        该参数设置越大,内存回收频率越低,但预留内存分配给新对象的失败率也会升高,性能反而下降
      • 产生大量内存碎片,可通过下列两个参数解决:
        -XX:UseCMSCompactAtFullCollection开关控制内存碎片合并,默认是开启的,JDK9开始废弃
        -XX:CMSFullGCsBeforeCompaction 控制在整理碎片的Full GC前会执行不做碎片整理的Full GC的次数,JDK9开始废弃
  • G1收集器

    • 基于收益最大化算法的垃圾回收算法,将java堆划分成多个大小相等的独立Region,每个Region都可以是Eden、Survivor或者老年代。每个Region拥有两个TAMS指针

    • 建立了可预测的停顿时间模型,每次回收的区域就是Region的整数倍,可以根据维护的优先级进行垃圾回收,通过参数
      -XX:MaxGCPauseMillis限定最大回收停顿时间,默认200ms

    • 四个步骤

      • 初始标记

        标记GC Roots直接关联到的对象,并且修改TAMS指针的值,用户线程会暂停
        
      • 并发标记

        扫描整个堆里的对象图,找出要回收的对象,处理原始快照记录下的引用变动对象,耗时较长,用户线程不会暂停
        
      • 最终标记

        处理并发阶段结束后遗留下来的原始快照记录,用户线程会暂停
        
      • 筛选回收

        各个Region按照回收价值从高到低排序,将原Region中存活的对象复制到新Region,然后删除老Region,用户线程会暂停
        
    • 缺点

      • 每个Region都维护一套卡表,实现复杂,占用空间大
      • G1的写屏障操作是异步的,比CMS复杂度高,且由于跟踪引用变化会产生额外的负担,所以小内存的情况下,CMS仍然比G1有优势
  • Shenandoah收集器

    • 九个阶段

      • 初始标记

        • 标记GC Roots直接关联到的对象
      • 并发标记

        扫描整个堆里的对象图,找出要回收的对象
        
      • 最终标记

        处理并发阶段结束后遗留下来的原始快照记录,在这个阶段统计出回收价值最高的Region
        
      • 并发清理

        清理连一个存活对象都不存在的Region
        
      • 并发回收

        将存活对象复制到一个未被使用的Region中,采用“读屏障”和“转发指针”解决并发的问题
        
      • 初始引用更新

        建立一个线程集合点,确保所有并发回收阶段中进行的收集器线程都以完成分配给他们的对象移动任务
        
      • 并发引用更新

        真正的引用更新
        
      • 最终引用更新

        解决了堆中的引用更新后,还要修正存在于GC Roots中的引用
        
      • 并发清理

        清理连一个存活对象都不存在的Region
        
  • ZGC收集器

    • 概念:基于Region内存布局的,不设分代的,使用了“读屏障”、“染色指针”和“内存多重映射”等技术来实现可并发的标记-整理算法的,以低延迟为首要目标的一款垃圾收集器

    • 三种Region

      • 小型Region:2MB,存放小于256KB的对象
      • 中型Region:32MB,存放小于4MB的对象
      • 大型Region:最小4MB,容量必须是2MB的整数倍,存放大于等于4MB的对象
    • 四个阶段

      • 并发标记

        遍历对象图做可达性分析
        
      • 并发预备重分配

        根据特定的查询条件统计出北侧收集过程要清理哪些Region,将这些Region组成重分配集。
        
      • 并发重分配

        将重分配集中的存活对象复制到新的Region上,并未重分配集中的每个Region维护一个转发表,记录从旧对象到新对象的转向关系
        
      • 并发重映射

        修正整个堆中指向重分配集中旧的对象的所有引用
        

常用JVM参数

  • -XX:PrintGCDetails 打印GC详情

  • -XX:SurvivorRadio Eden/Survivor 空间比例

  • -XX:PretenureSizeThreshold 指定大于该设置值的对象直接在老年代分配

  • -XX:MaxTenuringThreshold 晋升到老年代的年龄阈值

  • -XX:HandlePromotionFailure 是否允许担保失败,如果允许,则再检查老年代是否够用,够用则尝试一次Minor GC,否则进行一次Full GC

  • -XX:UseAdaptiveSizePolicy 动态调整java堆中各个区域的大小以及进入老年代的年龄

  • -XX:ParallelGCThreads 用户线程冻结期间并行执行的收集器线程数

  • -XX:ConcGCThreads=n 并行标记、并发整理的执行线程数

  • -XX:InitiatingHeapOccupancyPercent 设置触发标记周期的Java堆占用率阈值,默认45%