Skip to content

四种引用

  • 强引用(“Strong” Reference)
    就是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾收集器不会碰这种对象。对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为 null,就是可以被垃圾收集的了,当然具体回收时机还是要看垃圾收集策略。
  • 软引用(SoftReference)
    适合用来做缓存 是一种相对强引用弱化一些的引用,可以让对象豁免一些垃圾收集,只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象。JVM 会确保在抛出 OutOfMemoryError 之前,清理软引用指向的对象。
    软引用通常用来实现内存敏感的缓存,如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。
  • 弱引用(WeakReference) 用于ThreadLocal防止内存泄漏。
    并不能使对象豁免垃圾收集,仅仅是提供一种访问在弱引用状态下对象的途径。这就可以用来构建一种没有特定约束的关系, 比如,维护一种非强制性的映射关系,如果试图获取时对象还在,就使用它,否则重现实例化。它同样是很多缓存实现的选择。
  • 幻象引用
    有时候也翻译成虚引用,你不能通过它访问对象。幻象引用仅仅是提供了一种确保对象被 finalize 以后,做某些事情的机制, 比如,通常用来做所谓的 Post-Mortem 清理机制,也有人利用幻象引用监控对象的创建和销毁。

如何判断对象是否可以被回收?什么时候被回收?

  • 引用计数器法:为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。但是他有一个缺点是不能解决循环引用的问题。
  • 可达性分析算法:从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的。当前主流的商用程序语言(Java、C#,上溯至前面提到的古老的Lisp)的内存管理子系统,都是通过可达性分析(Reachability Analysis)算法来判定对象是否存活的。
    这个算法的基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。
    如图所示,对象object 5、object 6、object 7虽然互有关联,但是它们到GC Roots是不可达的,因此它们将会被判定为可回收的对象。 Java 虚拟机使用该算法来判断对象是否可被回收,在 Java 中 GC Roots 一般包含以下内容:
    在Java技术体系里面,固定可作为GC Roots的对象包括以下几种:
  • 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
  • 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
  • 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
  • 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。·Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
  • 所有被同步锁(synchronized关键字)持有的对象
  • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等

回收方法区

除了这些固定的GC Roots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整GC Roots集合。
譬如分代收集和局部回收(Partial GC),如果只针对Java堆中某一块区域发起垃圾收集时(如最典型的只针对新生代的垃圾收集),必须考虑到内存区域是虚拟机自己的实现细节(在用户视角里任何内存区域都是不可见的),更不是孤立封闭的,所以某个区域里的对象完全有可能被位于堆中其他区域的对象所引用,这时候就需要将这些关联区域的对象也一并加入GC Roots集合中去,才能保证可达性分析的正确性。
些人认为方法区(如HotSpot虚拟机中的元空间或者永久代)是没有垃圾收集行为的,《Java虚拟机规范》中提到过可以不要求虚拟机在方法区中实现垃圾收集,事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在(如JDK 11时期的ZGC收集器就不支持类卸载),方法区垃圾收集的“性价比”通常也是比较低的:
在Java堆中,尤其是在新生代中,对常规应用进行一次垃圾收集通常可以回收70%至99%的内存空间,相比之下,方法区回收囿于苛刻的判定条件,其区域垃圾收集的回收成果往往远低于此。
方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型。回收废弃常量与回收Java堆中的对象非常类似。举个常量池中字面量回收的例子,假如一个字符串“java”曾经进入常量池中,但是当前系统又没有任何一个字符串对象的值是“java”,换句话说,已经没有任何字符串对象引用常量池中的“java”常量,且虚拟机中也没有其他地方引用这个字面量。
如果在这时发生内存回收,而且垃圾收集器判断确有必要的话,这个“java”常量就将会被系统清理出常量池。常量池中其他类(接口)、方法、字段的符号引用也与此类似。
判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于“不再被使用的类”的条件就比较苛刻了。需要同时满足下面三个条件:

  • 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
  • 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的。
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
    Java虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而并不是和对象一样,没有引用了就必然会回收。关于是否要对类型进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class以及-XX:+TraceClass-Loading、-XX:+TraceClassUnLoading查看类加载和卸载信息,
    其中-verbose:class和-XX:+TraceClassLoading可以在Product版的虚拟机中使用,-XX:+TraceClassUnLoading参数需要FastDebug版的虚拟机支持。 在大量使用反射、动态代理、CGLib等字节码框架,动态生成JSP以及OSGi这类频繁自定义类加载器的场景中,通常都需要Java虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力。

1. 标记 - 清除

将存活的对象进行标记,然后清理掉未被标记的对象。 不足:

  • 标记和清除过程效率都不高;
  • 会产生大量不连续的内存碎片,导致无法给大对象分配内存。

2. 标记 - 整理

让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

3. 复制

将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。
主要不足是只使用了内存的一半。 现在的商业虚拟机都采用这种收集算法来回收新生代,但是并不是将新生代划分为大小相等的两块,而是分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象一次性复制到另一块 Survivor 空间上,最后清理 Eden 和使用过的那一块 Survivor。
HotSpot 虚拟机的 Eden 和 Survivor 的大小比例默认为 8:1,保证了内存的利用率达到 90%。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 空间就不够用了,此时需要依赖于老年代进行分配担保,也就是借用老年代的空间存储放不下的对象。

  • 分代收集 现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。 一般将堆分为新生代和老年代。
  • 新生代使用: 复制算法
  • 老年代使用: 标记 - 清除 或者 标记 - 整理 算法

分代收集算法和分区收集算法区别?

当前商业虚拟机的垃圾收集器,大多数都遵循了“分代收集”(Generational Collection)的理论进行设计,分代收集名为理论,实质是一套符合大多数程序运行实际情况的经验法则,它建立在两个分代假说之上:
1)弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。
2)强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。
这两个分代假说共同奠定了多款常用的垃圾收集器的一致的设计原则:收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。
显而易见,如果一个区域中大多数对象都是朝生夕灭,难以熬过垃圾收集过程的话,那么把它们集中放在一起,每次回收时只关注如何保留少量存活而不是去标记那些大量将要被回收的对象,就能以较低代价回收到大量的空间;如果剩下的都是难以消亡的对象,那把它们集中放在一块,虚拟机便可以使用较低的频率来回收这个区域,这就同时兼顾了垃圾收集的时间开销和内存的空间有效利用。
在Java堆划分出不同的区域之后,垃圾收集器才可以每次只回收其中某一个或者某些部分的区域——因而才有了“Minor GC”“Major GC”“Full GC”这样的回收类型的划分;也才能够针对不同的区域安排与里面存储对象存亡特征相匹配的垃圾收集算法——因而发展出了“标记-复制算法”“标记-清除算法”“标记-整理算法”等针对性的垃圾收集算法。这里笔者提前提及了一些新的名词,它们都是本章的重要角色,稍后都会逐一登场,现在读者只需要知道,这一切的出现都始于分代收集理论。
把分代收集理论具体放到现在的商用Java虚拟机里,设计者一般至少会把Java堆划分为新生代(Young Generation)和老年代(Old Generation)两个区域。
顾名思义,在新生代中,每次垃圾收集时都发现有大批对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放。如果读者有兴趣阅读HotSpot虚拟机源码的话,会发现里面存在着一些名为“Generation”的实现,如“DefNewGeneration”和“ParNewGeneration”等,这些就是HotSpot的“分代式垃圾收集器框架”。
原本HotSpot鼓励开发者尽量在这个框架内开发新的垃圾收集器,但除了最早期的两组四款收集器之外,后来的开发者并没有继续遵循。导致此事的原因有很多,最根本的是分代收集理论仍在不断发展之中,如何实现也有许多细节可以改进,被既定的代码框架约束反而不便。其实我们只要仔细思考一下,也很容易发现分代收集并非只是简单划分一下内存区域那么容易,它至少存在一个明显的困难:对象不是孤立的,对象之间会存在跨代引用。
假如要现在进行一次只局限于新生代区域内的收集(Minor GC),但新生代中的对象是完全有可能被老年代所引用的,为了找出该区域中的存活对象,不得不在固定的GC Roots之外,再额外遍历整个老年代中所有对象来确保可达性分析结果的正确性,反过来也是一样。遍历整个老年代所有对象的方案虽然理论上可行,但无疑会为内存回收带来很大的性能负担。为了解决这个问题,就需要对分代收集理论添加第三条经验法则:
3)跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极少数。 这其实是可根据前两条假说逻辑推理得出的隐含推论:存在互相引用关系的两个对象,是应该倾向于同时生存或者同时消亡的。举个例子,如果某个新生代对象存在跨代引用,由于老年代对象难以消亡,该引用会使得新生代对象在收集时同样得以存活,进而在年龄增长之后晋升到老年代中,这时跨代引用也随即被消除了。
依据这条假说,我们就不应再为了少量的跨代引用去扫描整个老年代,也不必浪费空间专门记录每一个对象是否存在及存在哪些跨代引用,只需在新生代上建立一个全局的数据结构(该结构被称为“记忆集”,Remembered Set),这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。
此后当发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GCRoots进行扫描。虽然这种方法需要在对象改变引用关系(如将自己或者某个属性赋值)时维护记录数据的正确性,会增加一些运行时的开销,但比起收集时扫描整个老年代来说仍然是划算的。
注意
刚才我们已经提到了“Minor GC”,后续文中还会出现其他针对不同分代的类似名词,为避免读者产生混淆,在这里统一定义:

  • 部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,其中又分为:
  • 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。
  • 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为。另外请注意“Major GC”这个说法现在有点混淆,在不同资料上常有不同所指,读者需按上下文区分到底是指老年代的收集还是整堆收集。
  • 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为。
  • 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。
    ParNew: 一款多线程的收集器,采用复制算法,主要工作在 Young 区,可以通过 -XX:ParallelGCThreads 参数来控制收集的线程数,整个过程都是 STW 的,常与 CMS 组合使用。
    CMS: 以获取最短回收停顿时间为目标,采用“标记-清除”算法,分 4 大步进行垃圾收集,其中初始标记和重新标记会 STW ,多数应用于互联网站或者 B/S 系统的服务器端上,JDK9 被标记弃用,JDK14 被删除。
    分区收集算法 分区算法则将整个堆空间划分为连续的不同小区间, 每个小区间独立使用, 独立回收. 这样做的 好处是可以控制一次回收多少个小区间 , 根据目标停顿时间, 每次合理地回收若干个小区间(而不是 整个堆), 从而减少一次 GC 所产生的停顿。
    G1: 一种服务器端的垃圾收集器,应用在多处理器和大容量内存环境中,在实现高吞吐量的同时,尽可能地满足垃圾收集暂停时间的要求。
    ZGC: JDK11 中推出的一款低延迟垃圾回收器,适用于大内存低延迟服务的内存管理和回收,SPECjbb 2015 基准测试,在 128G 的大堆下,最大停顿时间才 1.68 ms,停顿时间远胜于 G1 和 CMS。

三色标记整理

三色标记算法是一种JVM的垃圾标记算法,CMS/G1垃圾回收器就是使用的这种算法,它可以让JVM在不发生或者尽可能短的发生STW(Stop The World)的情况下进行垃圾的标记和清除。
顾名思义,三色标记算法是将Java堆中的对象分为了三种颜色,分别是:

  • 白色:白色对象代表没有被标记过的对象,在GC标记阶段刚开始的时候所有对象都是白色对象;而在GC标记阶段结束的时候,所有白色对象表示没有被引用的对象(即垃圾)。
  • 灰色:灰色对象表示该对象已经被标记过了,但是其引用的对象还没有被完全标记,JVM需要遍历其子对象来找到可达对象和垃圾。
  • 黑色:黑色对象表示该对象及其子对象全部都被标记过了,说明该对象已经完成了标记,JVM无需继续处理它 如上图所示,以CMS垃圾回收流程为例展示三色标记算法中每种颜色对应的意义:
  • 初始状态:初始状态时,GC标记还没开始,此时所有的Java对象都是白色对象,即未标记对象;
  • 初始标记:在初始标记阶段为了缩短STW时间,只标记了GC Roots直接可达的对象。那么这个过程中标记的GC Roots直接可达的对象就是灰色对象,因为这些对象可能包含子对象还并未被完全标记。
  • 并发标记:在并发标记阶段GC线程会从灰色对象出发,依次标记其所含的子对象,知道所有子对象都被标记完了之后,这些对象就称为了黑色对象。
  • 阶段1:此时GC Roots直达对象A、B、C被标记称黑色,而它们的子对象D、F、G被标记成灰色;
  • 阶段2:接着D、F、G对象被标记为黑色,而子对象H被标记成灰色;
  • 阶段3:H也被标记成黑色,此时所有对象都被标记成了黑色,对象标记完成;
  • 垃圾清理:在前面的标记过程中,由于所有的灰色对象都有GC Roots可以到达,所以这些黑色对象都是存活对象,而白色对象则是垃圾,需要被清理。
    三色标记算法的优势在于是一种增量型的垃圾标记算法,一步步的标记出整个堆中的所有垃圾而无需长时间的STW,造成系统卡顿。

三色标记算法存在的问题
由于在并发标记阶段,GC线程和用户线程是并发运行的,随时可能发生对象之间引用变化从而导致“多标”和“漏标”的问题。
多标 如上图所示,在并发标记阶段,当对象G已经被标记成灰色对象之后,此时C到G得引用被干掉了。
那么此时,C的子对象G和H应该被标记为白色,因为从GC Roots到他们的引用已经消失了,它们已经成为了垃圾对象。
但是由于G已经被标记成为了灰色对象,所以GC线程并不知道G和H需要被标记为白色,而是继续将这两个对象标记成了黑色对象。
最终结果导致,G和H这部分对象依旧存活,不会被本轮GC所清理,也就产生了浮动垃圾。
漏标
如上图所示,在并发标记阶段,当G已经被标记成了灰色对象,F已经被标记成为了黑色对象,而此时G->H的引用被断开,但是又新建立了一个F->H的引用。
那么此时,由于F已经被标记成为了黑色,那么F的子对象H就不会被扫描和标记到,因此就产生了“漏标”。这样就会导致H对象一直是白色,最后被当作垃圾被清除,这样直接会影响到程序运行的正确性,因此是绝对要被避免的。
实际上,漏标记只有同时满足以下两个条件才能发生:

  1. 一个或者多个黑色对象重新引用了白色对象,即黑色对象成员变量增加了新的引用;
  2. 灰色对象断开了白色对象的引用(直接或者间接),即灰色对象原来的成员变量引用发生了变化; 漏标的过程对应的Java代码如下所示:
Java
// 此时应用线程执行了以下操作
ObjectH H = objG.fieldH;  // H 被引用
objG.fieldH = null;      // 断开引用,G为灰色,H为白色
objF.fieldH = H;         // F 引用 H,G变为黑色,但是H依旧为白色

在上面的三个步骤当中,我们只需要在任意一步的时候讲被漏标的对象H存储起来,然后作为一个灰色对象放入到一个集合当中。等到并发标记结束重新标记的时候,遍历这个集合再次标记剩余对象就可以解决漏标的问题。
但是,这样一来“重新标记”就需要STW,因为只要有用户线程在执行就有可能会出现新的漏标记的情况发生,导致永远无法完成GC的标记过程。不过好在前面的“并发标记”过程已经将大部分的垃圾标记出来了,所以“重新标记”过程所耗费的时间较低。
所以说三色标记算法也无法完全解决STW问题,只能尽可能缩短STW的时间,从而减少系统的停顿。
如何解决漏标问题
对于多标问题来说,由于像CMS这类垃圾回收算法本身就会产生浮动垃圾,所以不算特别严重的问题,只需要等到下一次GC再回收即可。 但是漏标问题就相对严重得多,它会错误的回收正常的对象,导致程序错误,因此必须避免漏标的情况发生,下面主要介绍漏标的解决方案。
内存屏障方案
首先回顾下漏标的过程对应的Java代码如下所示:

Java
// 此时应用线程执行了以下操作
ObjectH H = objG.fieldH;  // H 被引用 (读操作)
objG.fieldH = null;      // 断开引用,G为灰色,H为白色 (写操作)
objF.fieldH = H;         // F 引用 H,G变为黑色,但是H依旧为白色 (写操作)

针对漏标问题,JVM 团队采用了读屏障与写屏障的方案。读屏障拦截了第一步,而写屏障用于拦截第二和第三步。它们的目的是在读写操作前后记录漏标对象H。下面是读屏障和写屏障的相关实现:
读屏障——读取之前记录漏标对象H

c
oop oop_field_load(oop* field) {
    pre_load_barrier(field); // 读屏障-读取前操作
    return *field;
}

void pre_load_barrier(oop* field, oop old_value) {  
    if ($gc_phase == GC_CONCURRENT_MARK && !isMarked(field)) {
        oop old_value = *field;
        remark_set.add(old_value); // 记录读取到的对象
    }
}

这里的读屏障直接针对第一步 ObjectH H = objG.fieldH; ,在读取成员变量之前先记录下来。这种做法是保守但安全的,因为重新引用的前提是获取到该白色对象,此时读屏障发挥了作用。
但是读屏障并不是所有对象的读写操作都会执行,
只有在“并发标记”阶段($gc_phase == GC_CONCURRENT_MARK)并且对象未被标记(白色对象,!isMarked(field))的情况下才执行。这保证了读屏障的触发是有条件的,并不会对所有对象的读操作都进行记录。
写屏障——写入之前记录漏标对象H

c
void oop_field_store(oop* field, oop new_value) { 
    *field = new_value; // 赋值操作
}

写屏障是在给某个对象的成员变量赋值操作前后进行处理,类似于 Spring AOP 的概念:

c
void oop_field_store(oop* field, oop new_value) {  
    pre_write_barrier(field); // 写屏障-写前操作
    *field = new_value; 
    post_write_barrier(field, value); // 写屏障-写后操作
}

这样的设计在写操作前后加入了相应处理,保证了在写操作时对相关对象的状态进行记录和处理。
写屏障同样是在并发标记阶段执行的。在写操作前后加入了处理,包括写前操作 pre_write_barrier 和写后操作 post_write_barrier。写屏障的触发同样是有条件的,不是所有对象的写操作都会记录。
增量更新(Incremental Update)与原始快照(Snapshot At The Beginning,SATB)方案

  • 增量更新(Incremental Update)
    在对象D的成员变量引用发生变化时(例如 objF.fieldH = H;),通过写屏障,将F新的成员变量引用对象H记录下来:
java
void post_write_barrier(oop* field, oop new_value) {  
    if ($gc_phase == GC_CONCURRENT_MARK && !isMarked(field)) {
        remark_set.add(new_value); // 记录新引用的对象
    }
}

这种做法的思路是不要求保留原始快照,而是针对新增的引用将其记录下来等待遍历,即增量更新。增量更新破坏了漏标的条件一:“一个或多个黑色对象重新引用了白色对象”,从而保证了不会漏标。

  • 原始快照(Snapshot At The Beginning,SATB) 当对象E的成员变量引用发生变化时(例如 objG.fieldH = null;),通过写屏障,将E原来成员变量的引用对象G记录下来:
java
void pre_write_barrier(oop* field) {
    oop old_value = *field; // 获取旧值
    remark_set.add(old_value); // 记录原来的引用对象
}

在原来成员变量引用发生变化之前,记录下原来的引用对象。这种做法的思路是尝试保留开始时的对象图,即原始快照(Snapshot At The Beginning,SATB)。当某个时刻的GC Roots确定后,当时的对象图就已经确定了。如果期间发生变化,则可以记录起来,保证标记依然按照原本的视图来。SATB破坏了漏标的条件二:“灰色对象断开了白色对象的引用(直接或间接的引用)”,从而保证了不会漏标。
主流垃圾回收器的漏标处理方案
基于可达性分析的GC算法,尤其是在并发标记的情境下,各种垃圾收集器采用了不同的漏标处理方案。在Java HotSpot VM中,具体的处理方式如下:

  • CMS(Concurrent Mark-Sweep)——写屏障 + 增量更新: 在并发标记阶段,CMS采用写屏障机制,通过在对象引用发生变化时进行增量更新,将新的引用记录下来。这有助于防止漏标。增量更新相较于其他方案可能会引入更多的浮动垃圾,但在重新标记阶段无需深度扫描已被删除引用的对象。
  • G1(Garbage-First)——写屏障 + 原始快照: G1在并发标记时采用写屏障,并选择了原始快照的方式。这意味着在对象引用发生变化时,将原始引用记录下来。相较于增量更新,原始快照在重新标记阶段可能会更高效,因为不需要深度扫描已被删除引用的对象。这对于G1来说是一个合适的选择,因为它的对象分布在不同的区域,而不像CMS一样集中在一个老年代区域。
  • Shenandoah——写屏障 + 原始快照: 类似于G1,Shenandoah在并发标记时采用写屏障,并选择了原始快照的方式。这种设计可以提高效率,因为在重新标记阶段不需要深度扫描已被删除引用的对象。
  • ZGC(Z Garbage Collector)——读屏障 + 染色指针: ZGC采用了染色指针技术,通过读屏障来处理并发标记。这种设计可以显著减少内存屏障的使用数量,特别是写屏障。染色指针直接在指针中维护引用变动的信息,避免了一些记录操作,对性能有显著的帮助。
    这些选择基于不同垃圾收集器的特点和设计目标,以在并发标记过程中尽可能降低对应用程序的影响,并提高垃圾回收的效率。

JVM内存分配策略

对象优先在 Eden 分配 大多数情况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC。
大对象直接进入老年代 大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。
经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。
-XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 区和 Survivor 区之间的大量内存复制。
长期存活的对象进入老年代 为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。
-XX:MaxTenuringThreshold 用来定义年龄的阈值。
动态对象年龄判定 虚拟机并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。
空间分配担保 在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。
如果不成立的话虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 设置不允许冒险,那么就要进行一次 Full GC。

Released under the MIT License.