JVM垃圾收集器与内存分配策略

766 阅读26分钟

概述

说起垃圾收集器 (Garbage Collection,下文简称GC),Java 内存区域的各个部分,其中 程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭,栈中的栈帧随着方法的进入和退出而有条不紊执行着出栈和入栈操作。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的,因此 这几个区域的内存分配和回收都具备确定性,在这个区域内就不需要过多的考虑如何回收的问题,当方法结束或者线程结束时,内存自然就跟随着回收了。


而 Java 堆和方法区这两个区域则有不确定性:一个接口的多个实现类需要的内存可能是不一样的,一个方法所执行的不同条件分支所需要的内存也可能不一样,只有处于运行区间,我们才能知道程序究竟会创建那些对象,创建多少个对象,这部分内存的分配和回收是动态的。垃圾收集器 所关注的正是这部分内存该如何管理。

在这里插入图片描述

1. 内存分配与回收策略


对象内存的分配,都是在堆上分配的

Java 堆是垃圾收集器管理的主要区域,因此也被称作GC 堆(Garbage Collected Heap).从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代:再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。

堆空间的基本结构:

在这里插入图片描述 如图所示 Eden 区 、 From Survivor0 区、To Survivor1 区都属于新生代,Old Memory属于老年代

1.1 对象优先在Eden分配

大部分情况,对象都会首先在 Eden 区分配,当 Eden 区没有足够的空间进行分配时,虚拟机将发起一次 Minor GC, 在 一次垃圾回收后如果对象还存活,则会进入 s0 或者 s1 ,并且对象的年龄还会加 1 ,当他年龄增加到一定程度(默认为15岁),就会被存放到老年代中。对象存放到老年代的年龄阀值可以通过参数 -XX:MaxTenuringThreshold 来设置。

案例 :

public class GCTest {

    public static void main(String[] args) {
        byte[] allocation1, allocation2,allocation3,allocation4,allocation5;
        allocation1 = new byte[32000*1024];
        allocation2 = new byte[32000*1024];
        allocation3 = new byte[1000*1024];
        allocation4 = new byte[1000*1024];
        allocation5 = new byte[1000*1024];
    }

}

在这里插入图片描述

在这里插入图片描述 GC输出 :

[GC (Allocation Failure) [PSYoungGen: 37244K->928K(76288K)] 37244K->32936K(251392K), 0.0229665 secs] [Times: user=0.10 sys=0.01, real=0.02 secs] 
Heap
 PSYoungGen      total 76288K, used 36825K [0x000000076ab00000, 0x0000000774000000, 0x00000007c0000000)
  eden space 65536K, 54% used [0x000000076ab00000,0x000000076ce0e518,0x000000076eb00000)
  from space 10752K, 8% used [0x000000076eb00000,0x000000076ebe8000,0x000000076f580000)
  to   space 10752K, 0% used [0x0000000773580000,0x0000000773580000,0x0000000774000000)
 ParOldGen       total 175104K, used 32008K [0x00000006c0000000, 0x00000006cab00000, 0x000000076ab00000)
  object space 175104K, 18% used [0x00000006c0000000,0x00000006c1f42010,0x00000006cab00000)
 Metaspace       used 3138K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 345K, capacity 388K, committed 512K, reserved 1048576K

从输出可以看出,,虚拟机将发起一次 Minor GC ,因为 from 区已经占用 8% ,老年代已经 18% 当对象无法存入 survivor 空间,所以只好通过分配担保机制把新生代的对象提前转移到老年代中去,老年代的空间足够放下对象 ,所以不会出现 Full GC。执行玩 Minor GC 后,后面分配的对象如果能够存在 Eden 区,还是会在 Eden区分配内存。


1.2 大对象直接进入老年代


大对象就是指需要大量连续内存空间的 Java 对象,最典型的大对象便是那种很长的字符串,或者元素数量庞大的数组,例如上面的例子 大对象byte[] 数组 ,我们在写程序的时候应注意避免。


在 Java 虚拟机中要避免大对象的原因是,在分配空间时,他容易导致内存还有不少空间时,就提前触发垃圾收集,以获取足够的连续空间才能安置它们,而当复制对象时,大对象就意味着高额的内存复制开销。HotSpot虚拟机提供了 -XX:PretenureSizeThreshold 参数,指定大于该设置值的对象直接在老年代分配,这样做的目的就是避免在Eden 区及两个 Survivor 区之间来回复制,产生大量的复制操作。


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


HotSpot 虚拟机中多数收集器都采用了分代手机来管理堆内存,那内存回收时就必须能决策那些存活对象应存放在新生代,你那些存活对象放在老年代中。为做到这点,虚拟机给每个对象定义一个年龄 (Age)计数器,存储在对象头中,对象通常在 Eden 区诞生,如果经过第一次 Minor GC后仍然存活,并且能被 Survivor 容纳的话,该对象会被移动到 Survivor 空间中,并且将其年龄设置为 1 岁,对象在 Survivor 区没熬过一次 Minor GC, 年龄就增加一岁,当它的年龄增加到一定程度(默认15岁)就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX: MaxTenuringThreshold设置。


1.4 动态对象年龄判定


为了能更好地适应不同程序的内存状况,HotSpot 虚拟机并不是永远要求对象的年龄必须达到 --XX:MaxTenuringThreshold才能晋升老年代,如果在 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到 -XX: MaxTenuringThreshold中要求的年龄。

案例 :

public class GCTest {

    private static final int _1MB = 1024 * 1024;


    public static void main(String[] args) {
        testTenuringThreshold2();
    }
    /**
     * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
     * -XX:MaxTenuringThreshold=15
     * -XX:+PrintTenuringDistribution
     */
    @SuppressWarnings("unused")
    public static void testTenuringThreshold2()
    {
        byte[] allocation1, allocation2, allocation3, allocation4;
        allocation1 = new byte[_1MB / 4];
        // allocation1+allocation2大于survivo空间一半
        allocation2 = new byte[_1MB / 4];
        allocation3 = new byte[4 * _1MB];
        allocation4 = new byte[4 * _1MB];
        allocation4 = null;
        allocation4 = new byte[4 * _1MB]; }

}

执行结果:

Heap
 PSYoungGen      total 9216K, used 7541K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 8192K, 92% used [0x00000007bf600000,0x00000007bfd5d738,0x00000007bfe00000)
  from space 1024K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007c0000000)
  to   space 1024K, 0% used [0x00000007bfe00000,0x00000007bfe00000,0x00000007bff00000)
 ParOldGen       total 10240K, used 8192K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  object space 10240K, 80% used [0x00000007bec00000,0x00000007bf400020,0x00000007bf600000)
 Metaspace       used 3113K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 342K, capacity 388K, committed 512K, reserved 1048576K

1.5 进行GC的区域


针对 Hotspot VM 的实现,它里面的GC可以分为两大类

部分收集(Partial GC):

  • 新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集
  • 老年代收集(Major GC / Old GC):只对老年代进行垃圾收集
  • 混合手机(Mixed GC):对整合新生代和部分老年代进行垃圾收集

整堆收集 :

  • Full GC : 收集整个 Java 堆和方法区。

2. 如何判断对象已死?


在堆里面存放着 Java 世界几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就是要确定哪些对象还 "存活" 着,哪些对象已经 "死去"。

在这里插入图片描述

2.1 引用计数法


很多教科书判断对象是否存活的算法是这样的 : 在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能被使用的。

客观的说,引用计数算法(Reference Counting)虽然占用了一些额外的内存空间来进行计数,但 它的原理简单,判定效率也很高,在大多数情况下它都是一个不错的算法。但是,在Java 领域,至少主流的 Java 虚拟机里面都没有选用引用计数算法进行管理内存,主要原因是,这个看似简单的算法有很多例外的情况要考虑,必须配合大量额外处理才能保证正确的工作,譬如单纯的引用计数 就很难解决对象之间相互循环引用的问题。


/**
 * testGC()方法执行后,objA和objB会不会被GC呢?
 */
public class ReferenceCountingGc {
    Object instance = null;
    public static void main(String[] args) {
        ReferenceCountingGc objA = new ReferenceCountingGc();
        ReferenceCountingGc objB = new ReferenceCountingGc();
        objA.instance = objB;
        objB.instance = objA;
        objA = null;
        objB = null;
		// 假设在这行发生GC,objA和objB是否能被回收?
		 System.gc();

    }
}


2.2 可达性分析


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

在这里插入图片描述

可作为 GC Roots 的对象包括:

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

2.3 引用


无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否引用链可达,判断对象是否存活都和 “引用” 离不开关系 。

在 JDK1.2 之前,Java 里的引用还是很传统的定义:如果 reference 类型的数据中存储的数值代表的是另外一块内存起始地址,就称该reference数据是代表 某块内存、某个对象的引用。这种定义并没有什么不对,只是现在看来有些过于狭隘了,一个对象在这种定义下只有 "被引用" 或者 "未被引用" 两种状态,对于描述一些 "食之无味,弃之可惜" 的对象就显的无能为力了,譬如我们希望能描述一类对象:当内存空间还足够时,能保留在内存之中,如果内存空 间在进行垃圾收集后仍然非常紧张,那就可以抛弃这些对象——很多系统的缓存功能都符合这样的应 用场景。

在 JDK1.2 之后,Java 对引用的概念进行了扩充,将引用分为强引用(Strongly Re-ference)、软 引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)4种,这4种引用强 度依次逐渐减弱。

  • 强引用

    强引用是最传统的 "引用" 的定义 ,是指在程序代码之中普遍存在的引用赋值,即类似“Object obj=new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回 收掉被引用的对象。

  • 软引用

    软引用是用来描述一些还有用,但非必要的对象。只被软引用关联着的对象,在系统将要发生内存溢出钱,会把这些对象列进回收范围之中进行二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。在 JDK 1.2 之后提供了 SoftReference 类实现软引用。

  • 弱引用

    弱引用也是用来描述哪些非必需对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收之被弱引用的关联的对象。在JDK 1.2版之后提供了WeakReference类来实现弱引用。

  • 虚引用

    虚引用也称为 "幽灵引用" 或者 "幻影引用" ,它是最弱的一种引用关系。一个对象是否有需引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来去的一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2版之后提供 了PhantomReference类来实现虚引用。


2.4 对象生存还是死亡


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

被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。


2.5 回收方法区


方法区的垃圾收集主要分为两部分内容:废弃的常量和不再使用的类型。回收废弃常量与回收 Java堆中的对象非常类似。

  • 废弃的常量

废弃的常量 :假如一个字符串 "Java" 曾经进入常量池 中,但是当前系统又没有任何一个字符串对象的值是 "Java",换句话,已经没有任何字符串对象引用常量池的 "Java" ,且虚拟机中也没有其他地方引用这个字面量。如果在这时发生内存回收,而且垃圾收集器判断确有必要的话,这个 "Java" 常量就会被系统清理出常量池。常量池其他类 (接口) 、方法、字段的符号引用也与此类似。

  • 不在使用的类型

判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于“不再被使用的类”的条件就 比较苛刻了。需要同时满足下面三个条件:

  1. 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
  2. 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如 OSGi、JSP的重加载等,否则通常是很难达成的。
  3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方 法。

虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。


3. 垃圾收集算法


在这里插入图片描述


3.1 分代收集算法


当前商业虚拟机的垃圾收集器,大多数都遵循了 "分代收集" (Generational Collection)都理论进行设计的,分代收集名为理论,实质是一套符合大多数程序运行实际情况的经验法则,它建立在两个分代假说之上

  1. 弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。
  2. 强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消 亡。

这两个分代假说共同奠定了多款常用的垃圾收集器的一致设计原则 : 收集器应该将 Java 堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域存储。显而易见,如果一个区域中大多数对象都是朝生夕灭,难以熬过垃圾回收的话,那么把它们集中放在一起,每次回收时只关注如何保留少量存活而不是标记哪些大量将要被回收的对象,就能以较低代价回收到大量的空间如果剩下的都是难以消亡的对象,那把它们集中放在一块, 虚拟机便可以使用较低的频率来回收这个区域,这就同时兼顾了垃圾收集的时间开销和内存的空间有 效利用。

设计者一般至少会把Java堆划分为新生代 (Young Generation)和老年代(Old Generation)两个区域[2]。顾名思义,在新生代中,每次垃圾收集 时都发现有大批对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放


3.2 标记清除算法


最早出现也是最基础的垃圾算法是 "标记-清除" (Mark-Sweep)算法,如图它的名字一样,算法分为 "标记" 和 "清除" 两个阶段 :首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以返过来,标记存活的对象,统一回收所有未标记的对象。标记过程就是对象是否属于垃圾的判定过程

这种垃圾算法会带来2个明显的问题

  • 效率问题
  • 空间问题 (标记清除后产生大量不连续的碎片)

在这里插入图片描述


3.3 标记-复制算法


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

在这里插入图片描述


3.4 标记-整理算法


标记-复制算法在对象存活率较高时就要进行较多的复制操作,效率将会降低。更关键的是,如果 不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存 活的极端情况,所以在老年代一般不能直接选用这种算法。

针对老年代对象的存亡特征,提出了另外一种有针对性的“标记-整 理”(Mark-Compact)算法,其中的标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可 回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内 存。

在这里插入图片描述

4. 垃圾收集器


在这里插入图片描述

如果说收集算法时内存回收的方法论,那么垃圾收集器就是内存回收实践者。

虽然我们会对各个收集器进行比 较,但并非为了挑选一个最好的收集器出来,虽然垃圾收集器的技术在不断进步,但直到现在还没有 最好的收集器出现,更加不存在“万能”的收集器,所以我们选择的只是对具体应用最合适的收集器。


4.1 Serial收集器


Serial 收集器是最基础、历史最悠久的收集器,在 JDK1.3之前 是HotSpot 虚拟机新生代收集器唯一选择。这个收集器是一个单线程工作的收集器,但它的“单线 程”的意义并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强 调在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。

在这里插入图片描述 虚拟机的设计者们当然知道 Stop The World 带来的不良用户体验,所以在后续的垃圾收集器设计中停顿时间在不断缩短(仍然还有停顿,寻找最优秀的垃圾收集器的过程仍然在继续)。

但是 Serial 收集器有没有优于其他垃圾收集器的地方呢?当然有,它简单而高效(与其他收集器的单线程相比)。Serial 收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。Serial 收集器对于运行在 Client 模式下的虚拟机来说是个不错的选择。


4.2 ParNew收集器


ParNew收集器实质上是 Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之外其余的行为包括Serial收集器可用的所有控制参数(例如:-XX:SurvivorRatio、-XX: PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、对象分配规 则、回收策略等都与Serial收集器完全一致

在这里插入图片描述

ParNew收集器除了支持多线程并行收集之外,尤其是JDK 7之前的遗留系统中首选的新生代收集 器,其中有一个与功能、性能无关但其实很重要的原因是:除了Serial收集器外,目前只有它能与CMS 收集器配合工作。


4.3 Parallel Scavenge收集器


Parallel Scavenge收集器也是一款新生代收集器, 它同样是基于标记-复制算法实现的收集器,也是能够并行收集的多线程收集器

  • Parallel Scavenge 和 ParNew 特别之处
-XX:+UseParallelGC

    使用 Parallel 收集器+ 老年代串行

-XX:+UseParallelOldGC

    使用 Parallel 收集器+ 老年代并行

由于与吞吐量关系密切,Parallel Scavenge收集器也经常被称作“吞吐量优先收集器”。Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)。CMS等垃圾收集器关注点更多的是用户线程停顿时间(提高用户体验)。所谓吞吐量就是 CPU 用于运行用户代码的时间与 CPU 总小号时间的比值。

Parallel Scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解,手工优化存在困难的时候,使用 Parallel Scavenge 收集器配合自适应调节策略,把内存管理优化交给虚拟机去完成也是一个不错的选择。这种重中之重重中之重


这是 JDK1.8 默认收集器

使用 java -XX:+PrintCommandLineFlags -version 命令查看

-XX:InitialHeapSize=262921408 -XX:MaxHeapSize=4206742528 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
java version "1.8.0_211"
Java(TM) SE Runtime Environment (build 1.8.0_211-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.211-b12, mixed mode)

JDK1.8 默认使用的是 Parallel Scavenge + Parallel Old,如果指定了-XX:+UseParallelGC 参数,则默认指定了-XX:+UseParallelOldGC,可以使用-XX:-UseParallelOldGC 来禁用该功能


4.4 Seria Old 收集器


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


4.5 Parallel Old收集器


Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。


4.6 CMS收集器


CMS (Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的 Java 应用集在互联网网站或者基于浏览器的 B/S 系统服务器上,这类应用通常都会较为 关注服务的响应速度,希望系统停顿时间尽可能短,以给用户带来良好的交互体验。CMS收集器就非 常符合这类应用的需求。

从名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于标记-清除算法实现的,它的运作 过程相对于前面几种收集器来说要更复杂一些,整个过程分为四个步骤,包括:

  1. 初始标记(CMS initial mark):暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快
  2. 并发标记(CMS concurrent mark):从GC roots的直接关联对象开始遍历整个对象图的过程, 这个过程耗时较长但是不需要停顿用户线程,可以与卡机手机线程一起并发运行
  3. 重新标记(CMS remark):为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一 些,但也远比并发标记阶段的时间短
  4. 并发清除(CMS concurrent sweep):强力删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以整个阶段也是可以与用户线程同时并发的。

在这里插入图片描述

从它的名字就可以看出它是一款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下面三个明显的缺点:

  • 对 CPU 资源敏感
  • 无法处理浮动垃圾
  • 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。

4.6 Garbage First收集器


Garbage First(简称G1)收集器是垃圾收集器技术发展历史上的里程碑式的成果,它开创了收集 器面向局部收集的设计思路和基于Region的内存布局形式。

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

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

G1 收集器的运作大致分为以下几个步骤:

  • 初始标记
  • 并发标记
  • 最终标记
  • 筛选回收

G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来) 。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。


4.6 ZGC收集器


ZGC(Z Garbage Collector)是一款在 JDK 11中新加入的具有实验性质[1]的低延迟垃圾收集器,是由Oracle公司研发的。2018年Oracle创建了 JEP 333将ZGC提交给OpenJDK,推动其进入OpenJDK 11的发布清单之中。

与 CMS 中的 ParNew 和 G1 类似,ZGC 也采用标记-复制算法,不过 ZGC 对该算法做了重大改进。

ZGC运作过程:

  • 并发标记
  • 并发预备重分配
  • 并发重分配
  • 并发重映射


个人博客地址:blog.yanxiaolong.cn『纵有疾风起,人生不言弃』