Java垃圾回收:算法,策略与垃圾收集器简介

2023年 7月 14日 70.9k 0

基础概念

可达性分析与GC Root

在Java虚拟机(JVM)中,垃圾回收(Garbage Collection,简称GC)主要依赖可达性分析(Reachability Analysis)来确定对象是否还在使用。可达性分析的核心思想是通过一系列称为GC Roots的对象作为起始点,根据引用关系遍历对象图,从而判断对象是否可达。如果一个对象在遍历过程中无法被访问到,那么它被认为是不可达的,JVM会将其视为垃圾对象,可以回收其占用的内存空间。

在Java中,GC Roots主要包括以下几类对象:

  • 虚拟机栈(栈帧中的局部变量表)中引用的对象:局部变量表存储了方法执行过程中使用到的局部变量,这些局部变量可能会引用堆内存中的对象。
  • 方法区中类静态属性引用的对象:类的静态属性存储在方法区,静态属性可能会引用堆内存中的对象。
  • 方法区中常量引用的对象:方法区存储了类的常量池,常量池中的常量可能会引用堆内存中的对象。
  • 本地方法栈中JNI(Native方法)引用的对象:本地方法栈存储了Java Native Interface(JNI)调用过程中使用到的本地变量,这些变量可能会引用堆内存中的对象。
  • 所有被同步锁(synchronized关键字)持有的对象。
  • JVM通过以上GC Roots作为起始点,遍历整个对象图,找到所有可达的对象。不可达的对象即为垃圾对象,可以被回收。

    除了可达性分析,还有一种常见的方式来确定对象是否还在使用,即引用计数(Reference Counting)

    引用计数的基本思想是为每个对象维护一个计数器,用于记录该对象被引用的次数。当创建一个对象时,其引用计数初始值为1;当有新的引用变量指向该对象时,引用计数加1;当引用变量失效或被重新赋值时,引用计数减1。如果一个对象的引用计数变为0,说明该对象不再被任何引用变量指向,可以被视为不再使用,从而进行内存回收。

    引用类型

    在Java内存管理中,引用是一种关系,用于表示一个对象与另一个对象之间的关联。根据引用强度的不同,Java提供了四种引用类型:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)。这些引用类型与垃圾回收机制密切相关,用于在不同的场景下实现不同的内存管理策略。

  • 强引用:强引用是最常见的引用类型,当一个对象被强引用指向时,它不会被垃圾回收器回收。只要强引用关系存在,垃圾收集器就永远不会回收被引用的对象。这种引用关系在程序中普遍存在,例如 Object obj = new Object();
  • 软引用:软引用用于描述一些有用但非必须的对象。只有当内存不足时,才会考虑回收被软引用关联的对象。软引用可以用于实现内存敏感的缓存,当内存充足时缓存中的对象保留,当内存不足时缓存中的对象被回收。Java提供SoftReference类来实现软引用。
  • 弱引用:弱引用比软引用更弱,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。一旦垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。这种引用类型可以用于实现一些短暂的关联,例如缓存。Java提供WeakReference类来实现弱引用。
  • 虚引用:虚引用是最弱的引用类型,被称为“幽灵引用”或“幻影引用”。设置虚引用关联的唯一目的是在对象被回收时能收到一个系统通知。虚引用不能用于获取对象实例,用于实现一些特殊需求的内存管理。Java提供PhantomReference类来实现虚引用。
  • 三色标记算法(Tri-color Marking Algorithm)

    三色标记算法(Tri-color Marking Algorithm)是一种用于垃圾回收的算法,主要用于追踪对象的可达性和辨识需要回收的对象。这种算法在分代收集、标记-整理和标记-清除等垃圾回收策略中被广泛应用。三色标记法的基本原理是将对象分为三种颜色:白色、灰色和黑色,然后通过遍历对象引用关系来标记和识别需要回收的对象。

    以下是三种颜色对象的含义:

  • 白色(White):表示尚未发现或尚未访问的对象。在垃圾回收开始时,所有对象都被认为是白色。白色对象在垃圾回收过程中可能会被回收。
  • 灰色(Gray):表示已发现但尚未访问其所有引用的对象。这意味着灰色对象已经被标记为可达(不是垃圾),但其引用的其他对象尚未处理。
  • 黑色(Black):表示已发现且已访问其所有引用的对象。黑色对象表示已被完全处理,即已经确认它和它引用的所有对象都不是垃圾。
  • 三色标记算法的过程如下:

  • 将所有对象标记为白色。
  • 将所有根对象(GC Roots,例如全局变量、本地变量、线程引用等)标记为灰色。
  • 从灰色对象集合中选择一个对象,检查其所有引用的对象。将这些引用的对象从白色标记为灰色,然后将当前对象标记为黑色。重复此步骤直到灰色对象集合为空。
  • 此时,所有可达对象都已标记为黑色。剩余的白色对象被认为是垃圾,可以被回收。
  • 需要注意的是,为了避免在垃圾回收过程中出现新的对象引用关系,通常会将三色标记算法与写屏障(Write Barrier)技术结合使用。写屏障会记录程序在垃圾回收过程中对对象引用关系的修改,并确保这些修改不会影响到三色标记的正确性。

    总之,三色标记算法是一种有效的垃圾回收算法,通过跟踪对象的可达性来识别需要回收的对象。它在许多现代垃圾回收策略中都有应用,如分代收集、标记-整理和标记-清除。

    Safepoint,Stop-The-World

    Safepoint和Stop-The-World(STW)是Java虚拟机(JVM)中与垃圾收集(GC)相关的两个重要概念。

    Safepoint(安全点):

    Safepoint是Java虚拟机在执行代码时设置的一些特定点,用于确保在这些点上,内存和线程的状态是一致的。当JVM需要执行某些全局操作时,例如垃圾收集、代码优化等,它会在安全点上暂停所有应用程序线程(称为"暂停到达安全点")或确保新线程在启动时进入安全点(称为"安全点轮询")。

    在安全点上,JVM可以确保对象引用和堆内存状态保持一致,这样就可以完成GC、代码优化等操作。通常,安全点位于方法调用、循环回边、异常抛出等位置。这些位置被选为安全点的原因是,在这些点上,线程的执行堆栈和堆内存状态相对简单,便于JVM进行操作。

    Stop-The-World(STW,全停顿):

    Stop-The-World是指JVM在执行某些全局操作时,需要暂停所有的应用程序线程,直到操作完成为止。这种操作通常与垃圾收集有关,例如标记-清除(Mark-Sweep)和复制(Copying)算法。在这些算法中,JVM需要确保所有线程处于安全点,并停止执行,以便对堆内存中的对象进行操作。

    STW会导致应用程序的响应时间增加,因为在STW期间,所有的应用程序线程都无法执行。为了减少STW的影响,JVM采用了多种优化手段,如增量垃圾收集、并发垃圾收集等。这些优化手段可以在一定程度上减少STW的时间,提高应用程序的响应性能。

    垃圾回收算法简介

    Mark-Sweep(标记-清除)算法

    Untitled.png

    标记-清除算法是一种基本的垃圾回收算法。它分为两个阶段:标记阶段和清除阶段。

    • 标记阶段:从根对象(GC Roots)开始,遍历所有可达对象并将它们标记为可达(通常使用三色标记法)。
    • 清除阶段:遍历所有对象,回收未被标记为可达(即垃圾)的对象。

    Copying(标记-复制)算法

    Untitled 1.png

    标记-复制算法试图解决内存碎片问题。它将堆内存分为两个相等大小的区域:From空间和To空间。在垃圾回收过程中,只使用其中一个空间。

    • 标记阶段:与标记-清除算法类似,从根对象开始遍历所有可达对象并将它们标记为可达。
    • 复制阶段:将所有被标记为可达的对象复制到另一个空间(To空间),并更新对象引用。复制完成后,原始空间(From空间)的所有对象都被视为垃圾,可以一次性回收。

    标记-复制算法的优点是避免了内存碎片,但是代价是堆内存的一半空间在任何时候都是闲置的。

    Mark-Compact(标记-整理)算法

    标记-整理算法结合了标记-清除和标记-复制算法的优点。它旨在避免内存碎片,同时不需要额外的内存空间。

    • 标记阶段:与标记-清除算法类似,从根对象开始遍历所有可达对象并将它们标记为可达。
    • 整理阶段:将所有被标记为可达的对象移动到内存的一端(通常是低地址端),并更新对象引用。整理完成后,所有未被标记的对象被视为垃圾,可以一次性回收。

    标记-整理算法在避免内存碎片的同时,不需要额外的内存空间,但可能在整理阶段移动大量对象,导致性能开销。

    分代收集理论(Generational Collection)

    分代收集理论基于一个观察:大多数对象的生命周期都很短。因此,将堆内存划分为不同的区域(代),每个区域具有不同的垃圾回收策略。

    • 年轻代(Young Generation):用于存放新创建的对象。年轻代的回收频率较高,通常使用标记-复制算法进行回收。
    • 老年代(Old Generation):当对象在年轻代中经历了一定次数的回收后,会被提升到老年代。老年代的回收频率较低,通常使用标记-清除或标记-整理算法进行回收。

    分代收集理论的优点是可以针对不同区域的对象特性使用不同的垃圾回收策略,提高垃圾回收的效率。

    在分代收集思想的指导下,产生了“Minor GC”“Major GC”“Full GC”这样的回收类型的划分

    在实际应用中,许多现代编程语言和运行时环境会结合这些算法,以在不同场景下达到更高的垃圾回收效率。例如,Java的HotSpot虚拟机使用分代收集理论,结合标记-复制算法(如在年轻代中的ParNew收集器)和标记-整理算法(如在老年代中的CMS收集器)。

    分代收集理论下的垃圾回收策略

  • Minor GC(小型垃圾回收) 或 Young GC(年轻代垃圾回收):这两个术语可以互换使用,都指针对年轻代的垃圾回收。年轻代主要包括Eden区和两个Survivor区(S0和S1)。Minor GC的频率较高,因为年轻代中的对象通常具有较短的生命周期。在Minor GC过程中,只有年轻代的内存区域会受到影响,老年代不受影响。Minor GC通常使用标记-复制算法进行回收,以减少内存碎片。
  • Major GC(大型垃圾回收) 或 Old GC(老年代垃圾回收):这两个术语可以互换使用,都指针对老年代的垃圾回收。当对象在年轻代中经历了一定次数的回收后,它们会被提升到老年代。老年代的回收频率较低,通常使用标记-清除或标记-整理算法进行回收。需要注意的是,有些情况下,Major GC可能会涉及到年轻代的部分回收,但主要目标仍然是老年代。目前只有CMS收集器会有单独收集老年代的行为。
  • Mixed GC(混合垃圾回收):Mixed GC是在进行老年代垃圾回收时,同时也对年轻代进行回收。这种类型的垃圾回收旨在平衡应用程序的吞吐量和响应性。Mixed GC通常在应用程序的老年代空间利用率较高时发生,以避免触发更耗时的Full GC。
  • Full GC(完全垃圾回收):指的是同时针对年轻代和老年代进行的垃圾回收。Full GC的触发条件包括老年代空间不足、系统调用(如System.gc())或者其他特定的JVM参数设置。Full GC通常比Minor GC、Major GC和Mixed GC更耗时,因为它需要处理整个堆内存空间。在Full GC过程中,应用程序的吞吐量和响应性可能会受到较大影响。
  • 垃圾收集器(Garbage Collector, GC)

    在Java虚拟机(JVM)中,有多种垃圾收集器(Garbage Collector, GC),这些垃圾收集器实现了不同的垃圾回收策略。

  • G1 GC(Garbage-First垃圾回收器):这是一个面向分区的垃圾收集器,它将堆内存划分为多个区域,并按照垃圾密度对区域进行回收。G1 GC在年轻代使用标记-复制算法,对应Minor GC;在老年代使用标记-整理算法,对应Major GC。G1 GC可以进行Mixed GC,即在回收老年代的同时,也对部分年轻代进行回收。在Full GC时,G1 GC会暂停应用程序并回收整个堆内存。
  • ZGC(Z Garbage Collector) 和 Shenandoah GC:这两个垃圾收集器是为了实现低延迟的应用程序而设计的。它们在整个堆内存中都使用并发的标记-整理算法,以减少停顿时间。这些收集器在处理Minor GCMajor GCFull GC时,都尽量与应用程序线程并发执行。
  • 回收范围 算法 并行-串行 回收策略
    Serial(串行收集器) 新生代 标记-复制(Mark-Copy) 暂停所有用户线程 Minor GC
    Serial Old(串行老年代收集器) 老年代 标记-整理(Mark-Sweep-Compact) 暂停所有用户线程 Major GC
    ParNew(并行年轻代收集器) 新生代 标记-复制(Mark-Copy) 暂停所有用户线程 Minor GC
    Concurrent Mark-Sweep(CMS) 老年代 标记-清除(Mark-Sweep) 并行,初始标记、重新标记阶段仍需暂停用户线程 Major GC
    Parallel Scavenge(并行回收) 新生代 标记-复制(Mark-Copy) 并行 Minor GC
    Parallel Old(并行老年代回收) 老年代 标记-整理(Mark-Sweep-Compact) 并行 Major GC
    G1 GC(Garbage-First垃圾回收器) 新生代,老年代 标记-整理(Mark-Sweep-Compact)算法 并行,,除并发标记外,其余阶段也是要完全暂停用户线程 Young GC 和 Mixed GC
    ZGC(Z Garbage Collector) 和 Shenandoah GC 新生代,老年代

    Serial ,Serial Old

    Untitled 2.png

    • Serial收集器是最基础、历史最悠久的收集器,曾经(在JDK 1.3.1之前)是HotSpot虚拟机新生代收集器的唯一选择。
    • Serial收集器是一个单线程的垃圾收集器,进行垃圾收集时,必须暂停其他所有工作线程,直到收集结束 。
    • Serial GC在年轻代使用标记-复制(Mark-Copy)算法,对应Minor GC;在老年代使用标记-整理(Mark-Sweep-Compact)算法,对应Major GC。当进行Full GC时,它会同时回收年轻代和老年代。

    Parallel Scavenge,Parallel Old

    Untitled 3.png

    这是一个并行的垃圾收集器,适用于多核处理器和需要较高吞吐量的场景。与Serial GC类似,Parallel GC在年轻代使用标记-复制算法,对应Minor GC;在老年代使用标记-整理算法,对应Major GC。但Parallel GC可以并行处理垃圾回收,以提高吞吐量。当进行Full GC时,它会同时回收年轻代和老年代。

    ParNew(Parallel New)

    Untitled 4.png

    ParNew(Parallel New)垃圾收集器是一种用于处理年轻代的并行垃圾收集器。它是Serial垃圾收集器的并行版本,ParNew垃圾收集器在年轻代使用标记-复制(Mark-Copy)算法进行垃圾回收。

    ParNew垃圾收集器通常与老年代的Concurrent Mark-Sweep(CMS)垃圾收集器搭配使用,组成一个完整的垃圾回收策略。在这种组合中,ParNew负责处理年轻代的垃圾回收(Minor GC),而CMS负责处理老年代的垃圾回收(Major GC)。

    在配置JVM参数时,可以通过-XX:+UseParNewGC选项启用ParNew垃圾收集器。启用ParNew垃圾收集器后,可以通过调整相关参数来优化垃圾回收性能,例如设置年轻代的大小、调整Survivor空间的比例等。

    Concurrent Mark-Sweep (CMS)

    Untitled 5.png

    • CMS是一款并发、使用标记-清除算法,用于老年代的GC
    • 整个过程分为四个步骤,包括:
      • 初始标记(CMS initial mark)
      • 并发标记(CMS concurrent mark)
      • 重新标记(CMS remark)
      • 并发清除(CMS concurrent sweep)
    • 初始标记、重新标记这两个步骤仍然需要“Stop The World”
    • 初始标记仅仅只是标记一下GC Roots能直接关联到的对象
    • 并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行
    • 重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录
    • 并发清除阶段,清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的

    CMS的一些问题:

    • 浮动垃圾:在CMS的并发标记和并发清理阶段,用户线程是还在继续运行的,程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留待下一次垃圾收集时再清理掉。这一部分垃圾就称为“浮动垃圾”
    • CMS是一款基于“标记-清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。
    • 空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次Full GC的情况。

    Garbage-First(G1)

    Untitled 6.png

    G1收集器的运作过程大致可划分为以下四个步骤:

    • 初始标记(Initial Marking)
    • 并发标记(Concurrent Marking)
    • 最终标记(Final Marking):
    • 筛选回收(Live Data Counting and Evacuation)

    初始标记(Initial Marking)

    • 标记一下GC Roots能直接关联到的对象,
    • 修改TAMS 指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。
    • 该阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。

    并发标记(Concurrent Marking)

    • 从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。
    • 当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。

    最终标记(Final Marking)

    • 对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
    • 增量更新:CMS收集器采用增量更新算法实现,而G1 收集器则是通过原始快照(SATB)算法来实现的

    筛选回收(Live Data Counting and Evacuation):

    • 负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间制定回收计划,选择任意多个Region 构成回收集

    • 把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。

    • 这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的

    • G1收集器除了并发标记外,其余阶段也是要完全暂停用户线程的

    番外:发展历史

    在JDK 5发布时,HotSpot推出了一款在强交互应用中几乎可称为具有划时代意义的垃圾收集器——CMS收集器。这款收集器是HotSpot虚拟机中第一款真正意义上支持并发的垃圾收集器,它首次实现了让垃圾收集线程与用户线程(基本上)同时工作。

    CMS作为老年代的收集器,却无法与JDK 1.4.0中已经存在的新生代收集器Parallel Scavenge配合工作,所以在JDK 5中使用CMS来收集老年代的时候,新生代只能选择ParNew或者Serial收集器中的一个。

    可以说直到CMS的出现才巩固了ParNew的地位,但成也萧何败也萧何,随着垃圾收集器技术的不断改进,更先进的G1收集器带着CMS继承者和替代者的光环登场。G1是一个面向全堆的收集器,不再需要其他新生代收集器的配合工作。

    自JDK 9开始,ParNew加CMS收集器的组合就不再是官方推荐的服务端模式下的收集器解决方案了。官方希望它能完全被G1所取代,甚至还取消了ParNew加Serial Old以及Serial加CMS这两组收集器组合的支持(其实原本也很少人这样使用),并直接取消了XX:+UseParNewGC参数,这意味着ParNew和CMS从此只能互相搭配使用,再也没有其他收集器能够和它们配合了。

    读者也可以理解为从此以后,ParNew合并入CMS,成为它专门处理新生代的组成部分。ParNew可以说是HotSpot虚拟机中第一款退出历史舞台的垃圾收集器。

    参考

    JVM GC发展史

    PS MarkSweep is which garbage collector

    GC Roots 是什么?哪些对象可以作为 GC Root?看完秒懂!_一直Tom猫的博客-CSDN博客_gc roots撖寡情

    没有二十年功力,写不出这一行"看似无用"的代码!

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论