JVM4:垃圾收集器和垃圾收集算法

本文详细解析了Java垃圾回收的基本原理,包括垃圾收集器的主要工作目标,如何判断对象是否可回收,以及各种垃圾回收算法的特点。同时,介绍了HotSpot虚拟机的垃圾收集器实现,如Serial、ParNew、ParallelScavenge、SerialOld、ParallelOld、CMS和G1收集器的工作机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

垃圾收集器主要考虑的工作是什么内存该回收、什么时候进行回收以及该怎么样进行回收?

通过之前的内容我们都知道,程序计数器、JVM栈及本地方法栈都是线程私有的,执行完毕自动销毁,不需要过多考虑内存回收问题;而堆和方法区是属于共享的区域,运行时创建的对象等信息都存放在这些区域中,垃圾收集器主要也是关注这部分内存的使用情况。

什么内存该回收

垃圾收集器回收的是一些已经无用的对象,判断对象无用的方法主要有:

  1. 引用计数法

    为新建的对象添加引用计数器,每当有个地方引用它时计数器加1,引用失效时则减1。当回收时,发现计数器为0,则回收此对象所占用的内存。

    a.优点:实现简单,效率高

    b.缺点:解决不了循环引用的场景。如:

    /**
     * Title: ReferemceCountGC
     * Description: vm args:-XX:+PrintHeapAtGC
     *
     * @author lin.xu
     * @date 2017/12/4.
     */
    public class ReferemceCountGC {
    
      public Object instance = null;
    
      private static final int _1MB = 1024 * 1024;
    
      private byte[] bigSize = new byte[2 * _1MB];
    
      public static void main(String args[]) {
        ReferemceCountGC objA = new ReferemceCountGC();
        ReferemceCountGC objB = new ReferemceCountGC();
    
        objA.instance = objB;
        objB.instance = objA;
    
        System.gc();
      }
    }
    
  2. 可达性分析法

    JVM根据选定的“GC Roots”,以“GC Roots”作为起点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain)。当对象和“GC Roots”之间没有引用链的时候,则表示对象不可达。

    不可达的对象会被进行第一次标记并进行筛选,筛选的条件是对象是否有必要执行finalize()方法。若没有覆盖finalize()方法或者finalize()方法已执行过,对象被第二次标记将等待下次回收。若对象覆盖finalize()方法,对象将被放进F-Queue队列中,由JVM开启低优先级的Finalizer线程去执行,稍后GC将会在F-Queue中进行第二次标记。如果对象在finalize()方法中与引用链上的对象建立了联系,则被移除“即将回收”的集合。

    Java语言中,可以选择如下对象作为“GC Roots”:

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

对于判定对象是否可用与“引用”也有关。传统的将引用分为有引用与无引用,但是有些对象是可能接下来会有用的。所以JVM希望当内存空间足够时,则保留这部分对象,如果进行回收后,内存还是不够的话,则回收这些对象。Java对引用进行了扩充,分为如下几种:

  • 强引用(Strong Reference)

    类似Object obj = new Object()这类的引用,只要存在,收集器就不会回收此对象

  • 软引用(Soft Reference)

    用来描述还有用但并非必需的对象。对于软引用关联的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围进行第二次回收

  • 弱引用(Weak Reference)

    用来描述非必需对象。无论内存是否足够,都会回收弱引用关联的对象

  • 虚引用(Phantom Reference)

    称为幽灵引用或幻影引用。无法通过虚引用获取对象实例,其唯一目的是能在这个对象被收集器回收时获取系统通知

方法区也是可以进行对象回收的,其回收内容包括废弃常量和无用的类。但相较于新生代,回收效率很低。回收废弃量与Java堆中的对象回收类似,但是针对类的回收就要严格很多:

  • 该类所有的实例均已被回收,即Java类中不存在该类的任何实例;
  • 加载该类的ClassLoader已经被回收;
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法通过反射来访问该类的方法

怎样进行回收

首先我们来看看回收的算法。

垃圾收集算法

  1. 标记-清除算法

    分为标记和清除两个步骤。首先标记需要回收的内存空间,之后将标记的内存空间进行回收。

    不足之处:效率问题,标记和清除效率都不高;空间问题,清除过程中由于未整理内存空间,使得内存空间里很多碎片空间,存储大对象时可能不得不进行一次Full GC操作。

  2. 复制算法

    将内存一分为二,每次只使用其中一半。当其中一半内存快使用完时,将还存活的对象复制到另一半中内存中,把已使用的内存清空。通常用于新生代的回收。

  3. 标记-整理算法

    对内存进行标记,将存活对象都向内存一端移动,然后直接清理存活对象边界外的内存空间。

  4. 分代收集算法

    根据内存区域进行不同的垃圾回收算法。新生代只有少量内存存活可以采用复制算法;老年代回收效率低可以采用标记-清除或者标记-整理算法。

HotSpot虚拟机的算法实现

  1. 枚举根节点

    难点:

    1. 逐个检查GC Roots到对象的引用会很耗时;
    2. 可达性分析对执行时间的敏感还体现在GC停顿上。因为在枚举根节点时,为了保持一致性,需要停顿所有的Java执行线程。

    解决办法:

    HotSpot在类加载完成后,采用OopMap的数据结构存储对象内什么偏移量上是什么类型的数据,在JIT编译过程中,也会在特定的位置记录栈和寄存器中哪些位置是引用。GC扫描时可以依据OopMap来获取信息。

  2. 安全点(Safe Point)

    有了OopMap可以快速枚举出根节点,但是不能为每个指令生成OopMap,否则开销太大。HotSpot是在特定的位置记录OopMap信息,这个特定的位置就指安全点。GC只有等所有线程执行到安全点的时候才会停止用户线程的执行。

    需要考虑的问题是如何让线程跑到安全点时停止下来。两种方案可选:

    1. 抢先式中断(Preemptive Suspension)

      GC发生时,所有线程全部中断,如果有线程不在安全点中断,则恢复让其继续执行至安全点。现几乎没有虚拟机采用这种方式。

    2. 主动式中断(Voluntary Suspension)

      GC需要执行的时候,会在安全点设置个标志。当线程执行时,都去轮询这个标志,发现中断标志为真时就主动挂起。

  3. 安全区域(Safe Region)

    安全点解决了运行时的线程GC问题。但是某些线程可能处在Sleep状态或者Block状态,那它不能响应中断的请求。这样HotSpot采用安全区域解决。安全区域是指在一段代码片段之中,对象引用关系不会发生变化,这个区域任何地方都是GC安全的。

    具体过程就是:线程执行进入了Safe Region区域代码中,标志自己进入安全区域。当JVM发起GC时,忽略处于Safe Region的线程。当线程需要离开安全区域,首先要检测GC是否完成,没完成需要继续等待,直到收到可以离开Safe Region区域的信号为止。

垃圾收集器

  1. Serial收集器

    最基本、最悠久的收集器。用于新生代,单线程的收集器,需要Stop The World。适用于运行在Client模式下的JVM。

参数描述
UseSerialGC使用Serial + Serial Old的收集器组合进行内存回收
SurvivorRatio新生代中Eden与Survivor区域的容量比值。默认为8,代表Eden:Survivor=8:1
PretenureSizeThreshold直接晋升到老年代的对象大小。设置这个参数后,大于这个参数的对象将直接分配到老年代
MaxTenuringThreshold晋升到老年代的对象年龄。每个对象在坚持一次Minor GC后,年龄加1,当超过这个参数值则进入老年代
HandlerPromotionFailure是否允许分配担保失败,即老年代的剩余空间不足以应付新生代的整个Eden和Survivor区的所有对象都存活的极端情况
  1. ParNew收集器

    Serial收集器的多线程版本。用于新生代,新生代回收并行,老年代回收串行;新生代复制算法,老年代标记-整理算法。适用于CPU多核的情形。

参数描述
UseParNewGC使用ParNew + Serial Old的收集器组合进行内存回收
ParallelGCThreads设置并行GC时进行内存回收的线程数
SurvivorRatio新生代中Eden与Survivor区域的容量比值。默认为8,代表Eden:Survivor=8:1
PretenureSizeThreshold直接晋升到老年代的对象大小。设置这个参数后,大于这个参数的对象将直接分配到老年代
MaxTenuringThreshold晋升到老年代的对象年龄。每个对象在坚持一次Minor GC后,年龄加1,当超过这个参数值则进入老年代
HandlerPromotionFailure是否允许分配担保失败,即老年代的剩余空间不足以应付新生代的整个Eden和Survivor区的所有对象都存活的极端情况
  1. Parallel Scavenge收集器

    类似于ParNew收集器,主要关注吞吐量。与ParNew收集器重要区别是:提供自适应的调节策略。当设置UseAdaptiveSizePolicy后,JVM会根据监控信息,动态调整新生代大小、Eden与Survivor比例、晋升老年代对象大小等参数以提供更合适的GC停顿时间或者最大的吞吐量。适合用于后台运算不需要太多交互的任务。

参数描述
UseParallelGC虚拟机运行在Server模式下的默认值。打开此开关后,使用Parallel Scavenge + Serial Old(PS MarkSweep)的收集器组合进行内存回收
MaxGCPauseMillis设置GC的最大停顿时间
GCTimeRatioGC时间占总时间的比率,默认值为99,即允许1%的GC时间
ParallelGCThreads设置并行GC时进行内存回收的线程数
SurvivorRatio新生代中Eden与Survivor区域的容量比值。默认为8,代表Eden:Survivor=8:1
PretenureSizeThreshold直接晋升到老年代的对象大小。设置这个参数后,大于这个参数的对象将直接分配到老年代
MaxTenuringThreshold晋升到老年代的对象年龄。每个对象在坚持一次Minor GC后,年龄加1,当超过这个参数值则进入老年代
HandlerPromotionFailure是否允许分配担保失败,即老年代的剩余空间不足以应付新生代的整个Eden和Survivor区的所有对象都存活的极端情况
UseAdaptiveSizePolicy动态调和智能Java堆中各个区域的大小以及进入老年代的年龄
  1. Serial Old收集器

    老年代、单线程收集器,采用“标记-整理”算法。主要适用于Client模式下;如果在Server模式,一是作为Parallel Scavenge收集器搭配适用;一是当CMS在并发收集发生Concurrent Mode Failure时作为后备预案使用

  2. Parallel Old收集器

    Parallel Scavenge收集器的老年代版本,多线程收集器,采用“标记-整理”算法。适用于注重吞吐量及CPU资源敏感的场合。

参数描述
UseParallelOldGC打开此开关后,使用Parallel Scavenge + Parallel Old的收集器组合进行内存回收
  1. CMS收集器

    CMS基于“标记-清除”算法实现,老年代收集器。适用于重视服务的响应速度,希望系统停顿时间越短,以给用户更好体验的场景。运作过程分以下4步:

    1. 初始标记:标记GC Roots能直接关联到的对象。需要STOP THE WORLD;
    2. 并发标记:GC Roots Tracing的过程,依据GC Roots找出存活的对象;
    3. 重新标记:修正并发标记过程中由于用户线程继续执行而导致标记变动的部分。需要STOP THE WORLD;
    4. 并发清理

    优点:
    并发收集、低停顿

    缺点:

    1. 对CPU资源非常敏感。CMS启动回收的线程数是(CPU数量+3)/4,当CPU数量很少时,回收线程占用的资源越多,用户线程就占用资源越少;
    2. 无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次的Full GC。并发清理时用户线程仍在执行,那就可能有垃圾产生。这部分垃圾(浮动垃圾)只能在下次收集过程中清理掉;在并发标记时还有用户线程在执行,老年代中就可能被存入对象,如果老年代内存不足,则会抛出“Concurrent Mode Failure”,这时会临时启动Serail Old收集器来进行老年代的垃圾收集。
    3. 由于采用“标记-清理”算法,会产生大量空间碎片。
参数描述
UseConcMarkSweepGC打开此开关后,使用ParNew + CMS + Serial Old的收集器组合进行内存回收。Serial Old收集器将作为CMS收集器出现Concurrent Mode Failure失败后的后备收集器使用
CMSInitiatingOccupancyFraction设置CMS收集器在老年代空间被使用多少后触发垃圾回收。默认为68%。
UseCMSCompactAtFullCollection设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片整理
CMSFullGCsBeforeCompaction设置CMS在进行若干次垃圾收集后再启动一次内存碎片整理
  1. G1收集器

    G1是一款面向服务端应用的垃圾收集器,是当今收集器技术发展的最前沿成果之一。

    与其他收集器所不同的是,G1收集器是将堆划分为多个Region,每个Region中分配着新生代和老年代(不一定是连续空间)。G1收集器运作大致步骤如下:

    1. 初始标记(Initial Marking)

      标记下GC Roots能直接关联的对象,并修改TAMS(Next Top at Mark Start)值,让下一阶段用户线程并发执行时知道在正确的Region中存放对象。需要停止用户线程。

      堆中每个Region都会维护一个Remembered Set,当有Reference写入时,检查引用的对象是否在同个Region中。如果是,则通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set中。这样在枚举根节点时可以不对全堆进行扫描。

    2. 并发标记(Concurrent Marking)

      从GC Roots出发,找出可到达的对象。

    3. 最终标记(Final Marking)

      修正在并发标记过程中由于用户线程继续执行导致变动的对象。JVM将并发标记过程中存储在Remembered Set Logs中的记录与Rembered Set合并,重新计算可到达的对象。并行执行。

    4. 筛选回收(Live Data Counting and Evacuation)

      对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。可并发并行。

    G1收集器特点如下:

    1. 并发与并行:并发标记,最终标记中的并行标记,并行回收
    2. 分代收集:新生代、老年代依然存在于G1收集器中
    3. 空间整合:整体上采用“标记-整理”算法,局部看(两个Region之间)采用复制算法
    4. 可预测的停顿:G1在垃圾回收时会依据用户指期望的停顿时间来选择回收效率最高的Region

内存分配与回收策略

  1. 对象优先在Eden分配

  2. 大对象直接进入老年代

    对象大于PretenureSizeThreshold参数指定大小的对象直接进入老年代

  3. 长期存活的对象将进入老年代

    对象年龄超过MaxTenuringThreshold参数指定大小的对象直接进入老年代

  4. 动态对象年龄判断

    如果在Survivor中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象直接进入老年代

  5. 空间分配担保

    在发生Minor GC之前,需要判断老年代可用连续空间是否大于新生代所有对象总空间。如果大于,直接进行Minor GC。如果不成立,则判断是否设置了空间担保(HandlePromotionFailure),若设置了空间担保,则判断老年代可用空间是否大于历次存储老年代的平均值,大于的话则进行MinorGC,不大于或者没有设置空间担保,需要进行Full GC。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值