JVM垃圾回收

堆空间的基本结构

Java的自动内存管理主要是针对对象内存的回收和对象内存的分配。同时,Java自动内存管理最核心的功能是堆内存对象的分配与回收。

Java堆是垃圾收集器管理的主要区域,因此也被称作GC堆。

从垃圾回收的角度来说,由于现在收集器基本都采用分代垃圾收集算法,所以Java堆被划分了几个不同的区域,这样我们就可以根据各个区域的特点选择合适的垃圾收集算法。

在JDK1.7版本及之前,堆内存被通常分为下面三个部分

  1. 新生代内存(Young Generation)
  2. 老生代(Old Generation)
  3. 永久代(Permanent Generation)

在JDK1.8版本之后Permanent已被Metaspace取代,元空间使用的是直接内存。

图中所示Eden、S0、S1都属于新生代,中间一层属于老生代,底层属于永久代。

为什么虚拟机要使用分代的思想来管理堆内存

虚拟机使用分代思想来管理堆内存主要是为了提高垃圾回收的效率。分代垃圾回收是基于以下几个关键考察:

  1. 大多数对象是短命的:在程序运行过程中,大部分对象的生命周期都很短,这意味着很多对象在创建后不久就会变成垃圾,可以被回收。

  2. 存活时间越长的对象越难以变成垃圾:那些存活了较长时间的对象更有可能继续存活下去。这意味着频繁检查这些对象是否变成垃圾的代价较高。

通过分代管理,垃圾回收器可以根据不同代的对象采用不同的回收策略。例如,新生代的回收可以采用复制算法,因为大部分对象很快就会被回收,复制的代价相对较低。而老年代可以采用标记-清除或者标记-整理算法,因为存活对象越多,复制代价越高。

总结:

  1. 提高回收效率:针对不同代的特点采用不同的回收策略,提高了整体的垃圾回收效率
  2. 减少停顿时间:频繁的Minor GC在新生代进行,而新生代相对较小,回收速度快,从而减少了应用的停顿时间。
  3. 优化内存使用:通过将短命对象快速回收,减少了内存占用,并且长命对象集中在老年代,便于管理。

内存分配和回收原则

对象优先在Eden区分配

大多数情况下,对象在新生代Eden区分配,当Eden没有足够空间进行分配时,虚拟机将发起一次Minor GC。

测试用例:

bash 复制代码
public class GCTest {
  public static void main(String[] args) {
    byte[] allocation1, allocation2;
    allocation1 = new byte[30900*1024];
  }
}

//在分配allocation2
allocation2 = new byte[900*1024];

假设当分配allocation2的时候Eden区内存已经被分配完了,即Eden区没有足够空间进行分配,虚拟机将会发起一次Minor GC、GC期间虚拟机发现allocation1无法存入Survivor空间,所以只好通过分配担保机制 把新生代对象提前转移到老年代中,老年代上的空间足够存放allocation1,所以不会出现Full GC。执行Minor GC后,后面分配的对象如果能够存在Eden区的话,还是会在Eden区分配

大对象直接进入老年代

大对象就是需要大量连续内存空间的对象(比如字符串、数组)。

大对象直接进入老年代的行为是由虚拟机动态决定的,他与具体使用的垃圾收集器和相关参数有关,大对象直接进入老年代是一种优化策略,旨在避免将大对象放入新生代,从而减少新生代的垃圾回收频率和成本。

  • G1垃圾回收器会根据 -XX:G1HeapRegionSize参数设置的堆区域大小和 -XX:G1MixedGCLiveThresholdPercent参数设置的阈值,来决定哪些对象会直接进入老年代。
  • Parallel Scavenge垃圾回收器中,由虚拟机根据当前的堆内存情况和历史数据动态决定。

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

即然虚拟机采用了分代收集的思想来管理内存,那么内存回收就必须能识别哪些对象应放在新生代,哪些对象应该放在老年代。为了做到这一点,虚拟机给每个对象一个对象年龄计数器

大部分情况下,对象都会首先在Eden区域分配,如果对象在Eden出生并经历第一次Minor GC后仍然能够存活,并且能被Survivor容纳,将被移动到Survivor空间(S0或者S1)中,并将对象年龄设为1(Eden区->Survivor区后对象的初始年龄变为1)。

对象在Survivor中每熬过一次Minor GC,年龄就增加1岁,当他的年龄增加到一定程度(默认15),就会被晋升到老年代,对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold设置。

在HotSpot JVM中会动态调整对象晋升年龄阈值。具体来说,这个机制是通过在Minor GC过程中统计不同年龄大小的对象,并动态调整对象晋升到老年代的年龄阈值,以优化内存管理。

  1. 遍历所有对象并按年龄进行累积:在Minor GC过程中,JVM会遍历所有存活对象,并按照对象的年龄对其所占用的内存大小进行累积。
  2. 累积的某个年龄大小超过了Survivor区的50%:JVM会计算每个年龄段的对象的总大小,并将这些大小累积。当累积的对象大小总和超过了Survivor区总大小的50%(可以通过-XX:TargetSurvivorRation = percent参数来设置)时,JVM会记录此时的年龄值。例如:如果某个年龄段的累积大小超过了Survivor区域的50%,这个年龄值会被标记。
  3. 取这个年龄和MaxTenuringThreshold中的更小值:JVM会将上述步骤中标记的年龄值和MaxTenuringThreshold进行比较,取两者中较小的值,作为新的晋升年龄阈值。这个新的阈值将用于决定哪些对象应在下一次GC时晋升到老年代。
  4. 新的晋升年龄阈值:通过上述机制,JVM动态调整对象的晋升年龄阈值,以适应当前应用的内存使用模式,这个动态调整有助于在内存使用高峰期尽早将长期存活的对象晋升到老年代,避免Survivor区的过度使用,从而优化垃圾回收的效率和性能。

主要进行GC的区域

针对HotSpot VM的实现,它里面的GC准确分类只有两大种

  1. Partial GC:并不收集整个GC堆的模式

    • Young GC:只收集 young gen的GC
    • Old GC:只收集old gen的GC,只有CMS的concurrent collection是这个模式。
    • Mixed GC:收集整个young gen以及部分old gen的GC,只有G1是这个模式。
  2. Full GC: 收集整个堆,包括young gen、old gen、perm gen等所有部分的模式。

Major GC通常跟full GC等价,收集整个GC堆。

最简单的分代GC策略,按照Hotspot VM的serial GC的实现来看,触发条件是:

  • young GC:当young gen中的eden区分配满的时候触发,注意young GC中有部分存活对象会晋升到old gen,所以young GC后old gen的占用量通常有所升高。

  • full GC:当准备要触发一次young GC时,如果发现统计数据说之前young GC的平均晋升大小比old gen剩余的空间大,则不会触发young GC而是触发full GC(因为HotSpot VM的GC里,除了CMS的concurrent collection之外,其他能收集old gen的GC都会同时收集整个GC堆,包括young gen,所以不需要事先触发一次单独的young GC);或者,如果有perm gen的话,要在perm gen分配空间但已经没有足够空间,也要触发Full GC。 或者system.gc()、heap dump 带GC,默认也是触发Full GC。

HotSpot VM里其他非并发GC的触发条件复杂一些,不过大致原理与上面一样,但是也有例外,Parallel Scavenge(-XX:+UseParallelGC)框架下,默认是在要触发full GC前先执行一次young GC,并且两次GC之间能让应用程序稍微运行一下,以期降低full GC的暂停时间(因为 young GC会尽量清理young gen的死亡对象,减少了full GC工作量)。

CMS GC中主要是定时去检查old gen的使用量,当使用量超过了触发比例就会启动一次CMS GC,对old gen做并发收集。

空间分配担保

空间分配担保是为了确保在Minor GC之前老年代本身还有容纳新生代所有对象的剩余空间。

JDK6 update24之前,在发生Minor GC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那这一次Minor GC可以确保是安全的。如果不成立,则虚拟机会先查看 -XX:HandlePromotionFailure参数的设置值是否允许担保失败;如果允许,那会继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者-XX:HandlePromotionFailure设置不允许冒险,那这时就要改为进行一次Full GC。

JDK Update24之后的规则变为只要老年代的连续空间大于新生代对象总大小或则会历次晋升的平均大小,就会进行Minor GC,否则进行Full GC。

死亡对象判断方法

堆中几乎放着所有对象的实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)。

引用计数法

给对象中添加一个引用计数器:

  • 每当有一个地方引用他,计数器就加1
  • 当引用失效,计数器就减1
  • 任何时候计数器为0的对象就是不可能再被使用的。

这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是他很难解决对象之间循环引用问题。

可达性算法

这个算法的基本思想就是通过一系列的称为"GC Roots"的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链项链的话,则证明此对象是不可用的,需要被回收。

下图中的Object6~Object10之间虽有引用关系,但他们到GC Roots不可达,因此为需要回收的对象。

哪些对象可以作为GC Roots:

  • 虚拟机栈(栈帧中的局部变量表)中引用的对象
  • 本地方法栈中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 所有被同步锁持有的对象
  • JNI引用的对象

对象可以被回收 ,就代表一定会被回收吗?

即使在可达性分析法中不可达的对象,也并非是"非死不可"的,这时候他们暂时处于"缓刑阶段",要真正宣告一个对象死亡,至少要经历两次标记过程:可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize方法 。当对象没有覆盖finalize方法,或者finalize方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。

被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象和引用链上的任何一个对象建立关联,否则就会被真的回收。
Object类中的finalize方法在JDK9版本及后续版本中finalize方法会被逐渐弃用移除

引用类型总结

无论是通过引用计数法判断对象引用数量,还是通过可达性分析法判断对象的引用链是否可达,判定对象的存活都与"引用"有关

引用类型包括:强引用、软引用、虚引用、弱引用

强引用

以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那么垃圾回收器绝不会回收他。当内存空间不足,Java虚拟机宁愿抛出"OutOfMemoryError"错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。

软引用

如果一个对象只具有软引用,当内存空间足够,垃圾回收器就不会回收他。但是如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收他,该对象就可以被程序使用,软引用可用来实现内存敏感的高速缓存。

软引用可以和一个引用队列联合使用, 如果软引用所引用的对象被垃圾回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。

应用场景:

软应用是一种相对强度稍弱的引用类型,可以通过java.lang.ref.SoftReference类来实现。软引用的对象只有在内存不足时才会被垃圾回收器回收,这使得软应用非常适合实现内存敏感的缓存。

使用软引用来实现缓存,当内存足够时保留缓存对象,不够时自动回收

SoftReference<Object> softRef = new SoftReference<>(new Object());

弱引用

弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描他所管辖的内存区域过程中,一旦发现了只具有弱引用的对象,不管内存空间是否充足,都会回收他。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

应用场景:

  1. 解决循环引用问题:在某些数据结构(如图、树)中,如果两个对象相互引用,可能会导致这些对象无法被回收。使用弱引用可以解决这个问题。例如,在某些缓存实现中,使用弱引用可以确保当没有其他强引用指向缓存的对象时,该对象可以被回收。
  2. 观察者模式:在观察者模式中,观察者通常需要注册到被观察者对象上,如果没有适当的注销机制,这些观察者对象可能不会被回收,导致内存泄漏。使用弱引用来引用观察者可以确保当观察者没有其他强引用时,可以被垃圾回收。
  3. 映射数据结构:在一些数据结构,如WeakHashMap,使用弱引用作为键值,确保当键对象没有强引用时,相关的键值对可以被回收。
bash 复制代码
import java.util.WeakHashMap;

public class WeakHashMapExample {
    public static void main(String[] args) {
        WeakHashMap<Object, String> map = new WeakHashMap<>();
        Object key = new Object();
        String value = "WeakHashMap Value";

        map.put(key, value);
        System.out.println(map); // 输出:{java.lang.Object@...=WeakHashMap Value}

        // 清除强引用
        key = null;

        // 强制进行垃圾回收
        System.gc();

        // 确保垃圾回收已完成
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 检查WeakHashMap中的条目,应该已经被回收
        System.out.println(map); // 输出:{}
    }
}

虚引用

与其他几种引用不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么他就和没有任何引用一样,在任何时候都可能被垃圾回收。

虚引用主要用来跟踪对象被垃圾回收的活动

虚引用和软引用与弱引用的一个区别在于虚引用必须和引用队列联合使用,虚引用主要用于跟踪对象被垃圾回收的过程,以便在对象被回收时进行必要的清理操作。

垃圾收集算法

标记清除算法

标记-清除算法分为"标记"和"清除"阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。

他是最基础的算法,后续的算法都是对其不足进行改进得到,这种垃圾回收算法会带来两个明显问题

  • 效率问题:标记和清除两阶段效率都不高
  • 空间问题:标记清除后产生大量不连续的内存碎片。

标记-清除过程如下:

  • 当一个对象被创建时,给一个标记位,假设为0
  • 在标记阶段,我们将所有可达对象的标记为设置为1
  • 扫描阶段清除的就是标记位为0的对象。

复制算法

为了解决标记-清除算法的效率和内存碎片问题,复制算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块,当其中一块内存使用完后,将存活的对象复制到另一块去,然后再把使用的空间一次清理掉,这样就使每次的内存回收都是对内存区间的一半进行回收。

虽然改进了标记-清除算法,但是仍然存在以下问题

  • 可用内存变小:可用内存缩小为原来的一半。
  • 不适合老年代:如果存活对象数量比较大,复制性能会变的很差。

标记-整理算法

标记-整理算法是根据老年代的特点提出的一种标记算法,标记过程仍然与"标记-清除"算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉边界以外的内存。

由于多了整理这一步,因此效率不高,适合老年代这种垃圾回收频率不是很高的场景。

分代收集算法

当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将Java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。

新生代中,每次收集都会有大量对象死去,所以可以选择"标记-复制"算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率比较高,而且没有额外的空间对他进行分配担保,所以我们必须选择"标记-清除"或者"标记-整理"算法进行垃圾收集。

垃圾收集器

上述收集算法是内存回收的方法论,垃圾收集器是内存回收的具体实现

JDK 默认垃圾收集器

  • JDK1.8 Parallel Scavenge(新生代)+Parallel Old(老年代)
  • JDK9~JDK20: G1

Serial收集器

串行收集器是最基本、历史最悠久的垃圾收集器,他的"单线程"的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是他在垃圾收集工作的时候必须暂停所有的工作线程("Stop The World"),直到收集结束。

新生代采用标记-复制算法,老年代使用标记-整理算法。

Serial收集器问题在于需要暂停所有工作线程,优势在于简单而高效(与其他收集器的单线程相比),由于没有线程交互的开销,可以获得很高的单线程收集效率。

ParNew收集器

ParNew收集器是专门用于年轻代垃圾收集的并行收集器,它采用的是标记-复制算法,老年代采用CMS收集器,采用标记-清除算法。

ParNew收集器工作流程

  • 初始标记
    • 标记从根对象(GC Roots)可达的所有对象。
    • 这个阶段会暂停所有应用线程
    • 由于只标记直接可达对象,速度非常快。
  • 并行标记
    • 这个阶段会并行扫描和标记所有从根对象出发的可达对象。
    • 多个GC线程同时工作,提高标记速度。
    • 应用线程仍然处于暂停状态。
  • 并行清理
    • 这个阶段会并行清理所有未标记的对象,将他们的内存空间回收。
    • 多个GC线程同时工作,提高清理效率。
    • 应用线程仍然处于暂停状态。
  • 最终标记
    • 再次标记在并行阶段应用线程可能导致的引用变化。
    • 应用线程会短暂暂停。
  • 并行压缩
    • 并行的将存活的对象复制到新的内存空间,压缩堆空间以减少碎片。
    • 应用线程处于暂停状态。

Parallel Scavenge收集器

Parallel Scavenge收集器是JVM中的一种用于年轻代垃圾收集的收集器 。与ParNew收集器类似,Parallel Scavenge收集器也采用并行收集算法,但是其设计目标和应用场景有所不同。Parallel Scavenge收集器关注吞吐量,通过最大限度减少垃圾收集的开销来提高应用程序的性能

Parallel Scavenge收集器特点

  • 并行收集:使用多个线程并行进行垃圾收集,可以利用多核处理器的优势,提高垃圾收集的效率

  • 吞吐量优先:主要关注最大化应用程序运行时间和垃圾收集时间的比例,适用于需要高吞吐量的应用程序。

  • 自适应调节:具有自适应调节策略,可以根据系统运行状况动态调整垃圾收集行为。

    Parallel Scavenge 收集器工作流程

  • 初始标记

    • 标记从根对象(GC Roots)可达的所有对象。
    • 这个阶段会暂停所有应用线程
  • 并行复制

    • 并行扫描和复制所有存活的对象到Survivor区或者老年代。
    • 多个GC线程同时工作,提高复制速度。
    • 应用线程仍然处于暂停状态。
  • 更新引用

    • 更新所有存活对象的引用,指向新地址
    • 确保对象之间的引用关系保持正确。
    • 应用线程仍然处于暂停状态。
  • 清理Eden区

    • 清理所有未被复制的对象,即Eden区中的所有对象都被认为是垃圾并回收。
    • 应用线程仍然处于暂停状态。

Serial Old收集器

Serial收集器的老年代版本,同样是一个单线程收集器。他主要有两大用途:一种是在JDK1.5以及以前的版本中与Parallel Scavenge配合使用,另一种是作为CMS收集器的后备方案。

Parallel Old收集器

Parallel Old收集器是JVM中的一种用于老年代垃圾收集的并行收集器。他是Parallel Scavenge收集器的老年代版本,旨在提高老年代垃圾收集的效率,适用于高吞吐量应用。

Parallel Old收集器特点

  • 并行处理:使用多个线程进行垃圾收集,充分利用多核处理器优势,提高垃圾收集效率。
  • 吞吐量优先:和Parallel Scavenge收集器搭配使用,能够显著提高应用程序的吞吐量,适合高负载和大规模应用。
  • 标记-整理算法:采用标记-整理算法,即首先标记所有存活对象,然后将这些对象压缩到堆的一端,释放出连续的内存空间。

Parallel Old 收集器工作流程

  • 初始标记
    • 标记从根对象(GC Roots)可达的所有对象。
    • 这个阶段会暂停所有应用线程
    • 速度很快 ,因为只标记直接可达对象。
  • 并发标记
    • 并行标记老年代中的所有存活对象。
    • 多个GC线程同时工作,提高标记速度。
    • 应用线程继续运行
  • 预清理
    • 处理在并发标记阶段应用程序运行期间发生的引用变化
    • 可能会短暂暂停应用程序
  • 重新标记
    • 再次暂停所有应用线程,完成标记过程中由于应用线程运行而产生的引用变化。
    • 应用线程仍然处于暂停状态。
  • 并行清理
    • 并行清理所有未标记对象,回收他们的内存。
    • 多个GC线程同时工作,提高清理效率。
    • 应用程序继续运行。
  • 压缩
    • 并行地将所有存活的对象压缩到堆的一端,形成连续的内存空间,清理内存碎片
    • 这个阶段也会暂停所有应用线程。

CMS(Concurrent Mark Sweep)收集器

CMS收集器是一种以获取最短回收停顿时间为目标的收集器,他非常符合在注重用户体验的应用上使用。
CMS收集器是Hotspot虚拟机第一款真正意义上的并发收集器,他第一次实现了让垃圾收集线程和
用户线程(基本上)同时工作。

CMS收集器是一种**"标记-清除"算法**实现的,他的运作过程相比于前面几种垃圾收集器来说更加复杂,整个过程分为四个步骤

  • 初始标记:暂停所有其他线程,并记录下直接和root相连的对象,速度很快;
  • 并发标记:同时开启GC和用户线程,用一个闭包结构去记录可达对象,但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程会不断更新引用,所以GC线程无法保证可达性分析的实时性,所以这个算法里会跟踪记录这些发生引用更新的地方
  • 重新标记:重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记发生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初试标记阶段稍长,远远比并发阶段时间短。
  • 并发清除:开启用户线程,同时GC线程开始对未标记的区域做清扫。

    主要优点:并发收集、低停顿
    主要缺点:
    • 对CPU资源敏感
    • 无法处理浮动垃圾
    • 使用回收算法"标记-清除"算法会导致收集结束时会有大量空间碎片产生。

G1收集器

G1收集器是一款面向服务器的垃圾收集器,主要针对配备多核处理器以及大容量内存的机器。以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征。

G1被视为JDK1.7 中HotSpot虚拟机的一个重要特征,具备以下特点:

  • 并行和并发:G1能充分利用CPU多核环境下的硬件优势,使用多个CPU来缩短STW停顿时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
  • 分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但仍然保留了分代的概念
  • 空间整合:与CMS的"标记-清除"算法不同,G1从整体来看是基于"标记-整理"算法实现的收集器,从局部上来看是基于"标记-复制"算法实现。
  • 分区管理:将整个堆划分为多个相同大小的区域(Region),每个区域可以在年轻代或老年代之间动态分配
  • 可预测的停顿:这是G1相比CMS的另一个大优势,降低停顿时间是G1和CMS的共同关注点。但G1除了追求停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。

分区Region:使用 G1 收集器时,它将整个 Java 堆划分成约 2048 个大小相同的独立 Region 块,每个 Region 块大小根据堆空间的实际大小而定,整体被控制在 1MB 到 32MB 之间,且为2的 N 次幂,即1MB,2MB,4MB,8MB,16MB,32MB。

可以通过 XX:G1HeapRegionsize 设定。所有的 Region 大小相同,且在 JVM 生命周期内不会被改变。

虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分 Region(不需要连续)的集合。通过 Region 的动态分配方式实现逻辑上的连续。

G1 收集器的工作流程

G1 收集器的工作流程分为年轻代收集和老年代收集两部分,主要包括以下阶段:

年轻代收集(Young GC)

  • 初始标记:标记从根对象可达的年轻代对象。这个阶段会暂停所有应用线程速度非常快,因为只标记年轻代中的对象。

  • 并行复制:并行扫描和复制所有存活的年轻代对象到新的区域应用线程暂停,多个 GC 线程同时工作,提高复制的速度。

  • 清理年轻代区域:清理所有未被复制的对象,即年轻代区域中的所有垃圾对象。释放内存,供新的对象分配。

老年代收集(Mixed GC)

老年代收集采用标记-整理算法和部分标记-清除算法,主要步骤如下:

  • 初始标记:标记从根对象可达的老年代对象。暂停所有应用线程

  • 并发标记:并发标记整个堆中的所有存活对象,包括年轻代和老年代。应用线程继续运行,垃圾回收线程在后台进行标记。

  • 重新标记:再次暂停所有应用线程,完成标记过程中由于应用线程运行而产生的引用变化。确保标记结果的准确性。

  • 清理和压缩并行清理未标记的对象,回收其内存。并行压缩存活的对象,整理内存碎片,形成连续的内存空间。

G1收集器在后台维护一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限时间内可以尽可能高的收集效率。从JDK9开始,G1垃圾收集器就成为了默认的垃圾收集器。

相关推荐
Daniel 大东8 分钟前
idea 解决缓存损坏问题
java·缓存·intellij-idea
wind瑞14 分钟前
IntelliJ IDEA插件开发-代码补全插件入门开发
java·ide·intellij-idea
HappyAcmen15 分钟前
IDEA部署AI代写插件
java·人工智能·intellij-idea
马剑威(威哥爱编程)20 分钟前
读写锁分离设计模式详解
java·设计模式·java-ee
鸽鸽程序猿21 分钟前
【算法】【优选算法】前缀和(上)
java·算法·前缀和
修道-032322 分钟前
【JAVA】二、设计模式之策略模式
java·设计模式·策略模式
九圣残炎27 分钟前
【从零开始的LeetCode-算法】2559. 统计范围内的元音字符串数
java·算法·leetcode
当归102440 分钟前
若依项目-结构解读
java
hlsd#1 小时前
关于 SpringBoot 时间处理的总结
java·spring boot·后端
iiiiiankor1 小时前
C/C++内存管理 | new的机制 | 重载自己的operator new
java·c语言·c++