第3章 垃圾收集器与内存分配策略

book:《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)周志明》

3.1 概述

程序计数器、虚拟机栈、本地方法栈3个区域线程私有,随线程生死。栈中的栈帧内存可说是在编译时便确定的。几个区域的内存分配与回收都具备有确定性。

Java Heap与方法区的分配是在运行时不断变化的,分配与回收是动态的,因而需要垃圾收集器。

3.2 对象已死?

垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还"存活"着,哪些已经"死去"("死去"即不可能再被任何途径使用的对象)了。

3.2.1 引用计数算法

引用计数法:在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。

引用计数算法虽然占用了一些额外的内存空间来进行计数,但它的原理简单,判定效率也很高,在大多数情况下它都是一个不错的算法。但是单纯的引用计数就很难解决对象之间相互循环引用的问题。举个简单的例子:对象objA和objB都有字段instance,赋值令objA.instance = objB 及 objB.instance = objA。但是他们因为互相引用着对方,导致他们的引用计数都不为0。

Java虚拟机并不是通过引用计数算法来判断对象是否存活

3.2.2 可达性分析算法(根搜索算法)

可达性分析算法的基本思路就是通过一系列成为"GC Roots"的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为"引用链"(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。

如图所示,对象object 5、object 6、object 7虽然互有关联,但是它们到GC Roots都是不可达的,因此它们将会被判定为可回收的对象。
请添加图片描述

在Java技术体系里面,固定可作为GC Roots的对象包括以下几种(前5种更重要):

  • 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
  • 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
  • 在方法区中常量引用的对象,譬如字符串常量(String Table)里的引用。
  • 在本地方法栈JNI(即通常所说的Native方法)引用的对象。
  • 所有被同步锁(sysnchronized关键字)持有的对象。
  • Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointerException、OutOfMemoryException)等,还有系统类加载器。
  • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
  • 根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象"临时性"地加入。

3.2.3 再谈引用

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

  • 强引用是最传统的"引用"的定义,是指在程序代码之中普遍存在的引用赋值,即类似于"Object obj = new Object()"这种引用关系。永远不会被垃圾收集器回收。
  • 软引用是用来描述一些还有用、但非必须的对象。在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,在JDK 1.2版之后提供了SoftReference类来实现软引用。软引用可用来实现内存敏感的高速缓存。
  • 弱引用也是用来描述那些非必须对象,但是他的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。在JDK 1.2版之后提供了WeakReference类来实现弱引用。
  • 虚引用也称为"幽灵引用"或者"幻影引用",它是最弱的一种引用关系。为一个对象设置虚引用关联的唯一目的只是为了能再这个对象被收集器回收时收到一个系统通知。在JDK 1.2版之后提供了PhantomReference类来实现虚引用。任何时候都可能被垃圾回收。

同时,在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。

3.2.4 生存还是死亡?标记算法————finalize()方法

在可达性算法中被判定为不可达对象后,至少要经历两次标记过程对象才会真正死亡:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它会被第一次标记,随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。加入对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为"没有必要执行"。

如果这个对象被判定为确有必要执行finalize()方法,那么该对象将会被放置在一个名为F-Queue的队列之中,并在稍后由一条虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize()方法。finalize()方法是对象逃脱死亡命运的最后一次机会,如果对象在finalize()中重新与引用链上的一个对象建立关联即可避免被回收,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量。从代码清单3-2中我们可以看到一个对象的finalize()被执行,但是它仍然可以存活。

演示代码之后得到以下结论:

  • 对象可以在被GC时自我拯救。自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次。
  • 官方非常不推荐使用finalize方法。finalize()能做的所有工作,使用try-finally或者其他方法都可以做得更好,更及时。

3.2.5 回收方法区

在Java堆中,尤其是在新生代中,对常规应用进行一次垃圾收集通常可以回收70%至99%的内存空间,相比之下,方法区回收囿于苛刻的判定条件,其区域垃圾收集的回收成果往往远低于此。

方法区的垃圾收集主要回收两个部分内容:废弃的常量不再使用的类型

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

  • 判断一个类型是否属于"不再被使用的类"需要满足下面三个条件:

    • 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
    • 加载该类的类加载器已经被回收
    • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

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

3.3 垃圾收集算法

从如何判定对象消亡的角度出发,垃圾收集算法可以划分为"引用计数式垃圾收集"(Reference Counting GC)和"追踪式垃圾收集"(Tracing GC)两大类,这两类也常备称作"直接垃圾收集"和"间接垃圾收集"。本节所有算法均属于追踪式垃圾收集的范畴。

3.3.1 分代收集理论

当前商业虚拟机的垃圾收集器,大多数都遵循了"分代收集"(Generational Collection)的理论进行设计,它建立在两个分代假说之上:

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

这两个分代假说共同奠定了多款常用的垃圾收集器的一致设计原则:收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储

  • 3)跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代来说仅占极少数。

这其实是可根据前两条假说逻辑推理得出的隐含推论:存在互相引用关系的两个对象,是应该倾向于同时生存或者同时消亡的。例如,如果某个新生代对象存在跨代引用,由于老年代对象难以消亡,该引用会使得新生代对象在收集时同样得以存活,进而在年龄增大之后晋升到老年代中,这时跨时代引用也随即被消除了。

依据假说3,我们就不应再为了少量的跨代引用去扫描整个老年代,也不必浪费空间专门记录每一个对象是否存在及存在哪些跨代引用,只需在新生代上建立一个全局的数据结构(该结构被称为“记忆集”,Remembered Set),这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。

注意
对于不同分代的名词定义:

  • 部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,其中又分为:
    • 新生代收集(Minor GC/Yong GC):指目标只是新生代的垃圾收集。
    • 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。
    • 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有GI收集器会有这种行为。
  • 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。

3.3.2 标记-清除算法

原理:标记-清除算法是最基础的垃圾收集算法,该算法分为"标记"和"清除"两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。

标记过程就是对象是否属于垃圾的判定过程。标记过程就是对象是否属于垃圾的判定过程,这在前一节讲述垃圾对象标记判定算法时其实已经介绍过了。

该算法有两个缺点:

  • 执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低。
  • 内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片。

3.3.3 标记-复制算法

原理:该算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉。

对于虚拟机内多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象,而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。

算法缺点:将可用内存缩小为了原来的一半,空间浪费大。

重要知识点1:HotSpot虚拟机的Serial、ParNew等新生代收集器均采用了Appel式回收策略来设计新生代的内存布局。

  • Appel回收原理Appel式回收的具体做法是把新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivo。发生垃圾搜集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1,也即每次新生代中可用内存空间为整个新生代容量的90%(Eden的80%加上一个Survivor的10%),只有一个Survivor空间,即10%的新生代是会被“浪费”的。

重要知识点2:分配担保。

  • Appel式回收还有一个充当罕见情况的“逃生门”的安全设计,当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其他内存区域(实际上大多就是老年代)进行分配担保(Handle Promotion)。如果另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象,这些对象便将通过分配担保机制直接进入老年代

3.3.4 标记-整理算法

原理:标记-整理算法标记的过程仍然与"标记-清除"算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。

  • HotSpot虚拟机里面关注吞吐量的Parallel Scavenge收集器是基于标记-整理算法的,而关注延迟的CMS收集器则是基于标记-清除算法的。

3.4 HopSpot的算法细节实现

3.4.1 根节点枚举

所有收集器在根节点枚举这一步骤都是必须暂停用户线程的,因此毫无疑问根节点枚举与之前提及的整理内存碎片一样会面临相似的"Stop The World"的困扰。即使是号称停顿时间可控,或者(几乎)不会发生停顿的CMS、G1、ZGC等收集器,枚举根节点时也是必须要停顿的。

在HotSpot的解决方案里,是使用一组称为OopMap的数据结构来达到这个目的。一旦类加载动作完成的时候,HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来,在即时编译(见第11章)过程中,也会在特定的位置记录下栈里和寄存器里哪些位置是引用。这样收集器在扫描时就可以直接得知这些信息了 ,并不需要真正一个不漏地从方法区等GC Roots开始查找。

3.4.2 安全点

定义: HotSpot没有为每条指令都生成OopMap,因为将会需要大量的额外存储空间,只是在"特定的位置"记录了这些信息,这些位置被称为安全点(Safepoint)。

安全点不会随意设置。方法调用、循环跳转、异常跳转等都属于指令序列复用,只有具有这些功能的指令才会产生安全点。

对于安全点,另一个需要考虑的问题是,如何在垃圾收集发生时让所有线程都跑到最近的安全点,然后停顿下来。这里有以下两种方案可供选择:

  • 抢先式中断(Preemptive Suspension):不需要线程的执行代码主动去配合,在垃圾收集发生时,系统首先把所有用户线程全部中断,如果发现有用户线程中断的地方不在安全点上,就恢复这条线程执行,让它一会再重新中断,直到跑到安全点上。现在几乎没有虚拟机实现采用抢先式中断来暂停线程响应GC事件。
  • 主动式中断(Voluntary Suspension):主动式中断的思想是当垃圾收集需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志位,各个线程执行过程时会不停地主动去轮询这个标志,一旦发现中断标志为真时自己在最近的安全点上主动中断挂起。轮询标志的地方和安全点是重合的,另外还要加上所有创建对象和其他需要在Java堆上分配内存的地方,这是为了检查是否即将要发生垃圾收集,避免没有足够内存分配新对象。由于轮询操作在代码中会频繁出现,这要求它必须足够高效。HotSpot使用内存保护陷阱的方式,把轮询操作精简至只有一条汇编指令的程度。

3.4.3 安全区域

定义: 安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化。因此,在这个区域中任意地方开始垃圾收集都是安全的。我们也可以把安全区域看作被扩展拉伸的安全点。

当用户线程执行到安全区域里面的代码时,首先会标识自己已经进入了安全区域,那样当这段时间里虚拟机要发起垃圾收集时就不必去管这些已经申明自己在安全区域内的线程了。当线程要离开安全区域时,它要检查虚拟机是否已经完成了根节点枚举(或者垃圾收集过程中其他需要暂停用户线程的阶段),如果完成了,那线程就当做没事发生过,继续执行;否则它就必须一直等待,知道收到可以离开安全区域的信号为止。

3.4.4 记忆集与卡表

讲解分代收集理论的时候,提到了为解决对象跨代引用所带来的问题,垃圾收集器在新生代中建立了名为记忆集(Remembered Set)的数据结构,用以避免把整个老年代加进GC Roots扫描范围(因为分代收集理论的假说二,决定了老年代对象难以死亡,对象多,所以全局扫描收集效率低)。

记忆集实现的精度,有三种:

  • 字长精度:每个记录精确到一个机器字长(就是处理器的寻址位数,如常见的32位或64位,这个精度决定了机器访问物理内存地址的指针长度),该字包含跨代指针。
  • 对象精度:每个记录精确到一个对象,该对象里有字段含有跨代指针。
  • 卡精度:每个记录精确到一块内存区域,该区域内有对象含有跨代指针。

关于卡表与记忆集的关系,记忆集是“抽象”的数据结构,卡表是其具体实现

卡表最简单的形式可以只是一个字节数组,以下这行代码是HotSpot默认的卡表标记逻辑:

CARD_TABLE [this address >> 9] = 0;

字节数组CARD_TABLE的每一个元素都对应着其标识的内存区域中一块特定大小的内存块,这个内存块被称作"卡页"(Card Page)。卡页大小是以2的N次幂的字节数,通过上面代码可以看出HotSpot中使用的卡页是2的9次幂,即512字节。512的16进制为0x0200。
请添加图片描述

一个卡页的内存中通常包含不止一个对象,只要卡页内存内有一个(或多个)对象的字段存在着跨代指针,那就将对应卡表的数组元素的值标识为1,称为这个元素变脏(Dirty),没有则标识为0。在垃圾收集发生时,只要筛选出卡表中变脏的元素,就能轻易得出哪些卡页内存块中包含跨代指针,把它们加入GC Roots中一并扫描。

3.4.5 写屏障

卡表元素何时变脏的答案是很明确的——有其他分代区域中对象引用了本区域对象时,其对应的卡表元素就应该变脏,变脏时间点原则上应该发生在引用类型字段赋值的那一刻。

在HotSpot虚拟机里是通过写屏障(Write Barrier)技术维护表状态的(也就是变脏)。写屏障可以看作在虚拟机层面对"引用类型字段赋值"这个动作的AOP切面(AOP为Aspect Oriented Programming的缩写,意为面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。)在赋值前的部分的写屏障叫作写前屏障(Pre-Write Barrier),在赋值之后的则叫作写后屏障(Post-Write Barrier)。

void oop_field_store(oop* field, oop new_value) {
  // 引用字段赋值操作
  *field = new_value;
  // 写后屏障,在这里完成卡表状态更新
  post_write_barrier(field, new_value);
}

写屏障额外的开销,与Minor GC时扫描整个老年代的代价相比低得多。

除了写屏障的开销外,卡表在高并发场景下还面临着"伪共享"(False Sharing)问题。伪共享是处理并发底层细节时一种经常需要考虑的问题,现代中央处理器的缓存中是以缓存行(Cache Line)为单位存储的,当多线程修改互相独立的变量时,如果这些变量恰好共享同一个缓存行,就会彼此影响(写回,无效化或者同步)而导致性能降低,这就是伪共享问题。

在JDK 7之后,HotSpot虚拟机增加了新的参数-XX:+UseCondCardMark,用来决定是否开启卡表更新的条件判断。开启会增加一次额外判断的开销,但能够避免伪共享问题,两者各有性能损耗,是否打开要根据应用实际运行情况来进行测试权衡。

3.4.6 并发的可达性分析

可达性分析算法理论上要求全过程都基于一个能保障一致性的快照中才能够进行分析,这意味着必须全程冻结用户线程的运行。想解决或者降低用户线程的停顿,就要先搞清楚为什么必须在一个能保障一致性的快照上才能进行对象图的遍历?

为了能解释清楚这个问题,这里引入三色标记(Tri-color Marking)作为工具来辅助推导,把遍历对象图过程中遇到的对象,按照"是否访问过"这个条件标记成以下三种颜色:

  • 白色:表示对象尚未被垃圾收集器访问过。显然在可达性分析刚刚开始的阶段,所有的对象都是白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达。
  • 黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。黑色的对象代表已经扫描过,它是安全存活的,如果有其他对象引用指向了黑色对象,无须重新扫描一遍。黑色对象不可能直接(不经过灰色对象)指向某个白色对象。
  • 灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过。

关于可达性分析的扫描过程,可以看作对象图上一股以灰色为波峰的波纹从黑向白推进的过程,如果用户线程此时是冻结的,只有收集器线程在工作 ,那不会有任何问题。

但如果用户线程与收集器是并发工作的呢?收集器在对象图上标记颜色,同时用户线程在修改引用关系——即修改对象图的结构,这样可能出现两种后果。

  • 把原本消亡的喜爱那个错误标记为存活,可以容忍。
  • 把原本存活的对象错误标记为已消亡,后果很致命,程序肯定会因此发生错误。下面表3-1演示了这样的致命错误具体如何产生的。
    请添加图片描述

已经理论上证明过,当且仅当以下两个条件同时满足时,会产生"对象消失"的问题,即原本应该是黑色的对象被误标为白色:

  • 赋值器插入了一条或多条从黑色对象到白色对象的新引用。
  • 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。

破坏以上两个条件任意一个即可以解决并发扫描时的对象消失问题。 由此产生了两种解决方案:增量更新(Incremental Update)和原始快照(Snapshot At The Beginning, SATB)。

  • 增量更新:要破坏的是第一个条件,当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。

这可以理解为,黑色对象一旦新插入了指向白色对象的引用,它就变回灰色对象了。

  • 原始快照:要破坏的是第二个条件,当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次。

这也可以理解为,无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来进行搜索。

以上无论是对引用关系记录的插入还是删除,虚拟机的记录操作都是通过写屏障实现的。在HotSpot虚拟机中,增量更新和原始快照这两种解决方案都有实际应用。譬如,CMS是基于增量更新来做并发标记的,G1、Shenandoah则是用原始快照来实现。

3.5 经典垃圾收集器

请添加图片描述

这里展示了七种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用,图中收集器所处的区域,则表示它是属于新生代收集器抑或是老年代收集器。

超级重要的总结表格

收集器作用区域应用算法备注
Serial新生代标记-复制见上图
ParNew新生代标记-复制见上图
Parallel Scavenge新生代标记-复制关注吞吐量
Serial Old老年代标记-整理JDK 5及以前与Parallel Scavenge收集器收集器合作,或作为CMS后被预案
Parallel Old老年代标记-整理JDK6开始与Parallel Scavenge收集器收集器合作
CMS老年代标记-清除增量更新、关注延迟
Garbage First新生代+老年代整体(标记-整理)、局部(标记-复制)原始快照
空行空行空行空行

3.5.1 Serial收集器

Serial收集器是最基础、历史最悠久的收集器。该收集器是一个单线程工作的收集器。更重要的是它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。"Stop The World"这项工作是由虚拟机在用户不可知、不可控的情况下,把用户的正常工作的线程全部都停掉,这对很多应用来说都是不可接受的。

虽然有着以上的缺点,但它依然是HotSpot虚拟机运行在客户端模式下的默认新生代收集器,有着优于其他收集器的地方,那就是简单而高效(与其他收集器的单线程相比):

  • 对于内存资源受限的环境,它是所有收集器里额外内存消耗(Memory Footprint)最小的;
  • 对于单核处理器或处理器核心数较少的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率

所以,Serial收集器对于运行在客户端模式下的虚拟机来说是一个很好的选择。

3.5.2 ParNew收集器

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

重要的是:除了Serial收集器外,目前只有ParNew收集器(新生代)能与CMS(老年代)收集器配合工作。

3.5.3 Parallel Scavenge收集器

Parallel Scavenge收集器也是一款新生代收集器,它同样是基于标记-复制算法实现的收集器,也是能够并行收集的多线程收集器。Parallel Scavenge的诸多特性从表面上看和ParNew非常相似。但实际大有区别。Parallel Scavenge非常关注吞吐量这个指标。由于与吞吐量关系密切,Parallel Scavenge收集器也经常被称作“吞吐量优先收集器”。

所谓吞吐量就是处理器用于运行用户代码的时间与处理器总耗时时间的比值,即:

  • 吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 运行垃圾收集时间)

Parallel Scavenge收集器值得关注的三个参数:

  • -XX:MaxGCPauseMillis参数:控制最大垃圾收集停顿时间。
    允许的值是一个大于0的毫秒数,收集器将尽力保证内存回收花费的时间不超过用户设定值。
  • -XX:GCTimeRatio参数:直接设置吞吐量大小。
    它是一个大于0小于100的整数,也就是垃圾收集时间占总时间的比率。譬如把此参数设置为19,那允许的最大垃圾收集时间就占总时间的5%(即1/(1+19))。默认值为99,即允许最大1%(即1/(1+99))的垃圾收集时间。
  • XX:+UseAdaptiveSizePolicy参数:虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。

3.5.4 Serial Old收集器

Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。这个收集器的主要意义也是供客户端模式下的HotSpot虚拟机使用。如果在服务端模式下,它也有可能有两种用途:

  • 一种是在JDK5以及之前的版本中与Parallel Scavenge收集器搭配使用。
  • 另一种就是作为CMS收集器发生失败时的后备预案,在并发收集发生Concurrent Mode Failure时使用。

备注:Parallel Scavenge收集器架构中本身有PS MarkSweep收集器来进行老年代收集(JDK5以前),并非直接调用Serial Old收集器,但是这个PS MarkSweep收集器与Serial Old的实现几乎是一样。

3.5.5 Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。该收集器是JDK6时才开始提供的。

3.5.6 CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。CMS收集器是基于标记-清除算法实现的,它的运作过程分为四个步骤,包括:

  • 1)初始标记(CMS initial mark)
    需要"Stop The World"(暂停线程)。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快。
  • 2)并发标记(CMS concurrent mark)
    并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行
  • 3)重新标记(CMS remark)
    需要"Stop The World"(暂停线程)。重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。方法有两个(如3.4.6节所述),CMS采用的是增量更新
    • 增量更新
    • 原始快照
  • 4)并发清除(CMSconcurrent sweep)
    清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。

CMS是一款优秀的收集器,它最主要的优点在名字上已经体现出来:并发收集、低停顿,一些官方公开文档里面也称之为“并发低停顿收集器”。但是,CMS收集器至少有以下三个明显的缺点:

  • 首先,CMS收集器对处理器资源非常敏感。CMS默认启动的回收线程数是(处理器核心数量+3)/4。当处理器核心数量不足4个时,CMS对用户程序的影响就可能变得很大。
  • 然后,由于CMS收集器无法处理"浮动垃圾"(Floating Grabage),有可能出现"Concurrent Mode Failure"失败进而导致另一次完全"Stop The World"的Full GC的产生。此时需要冻结用户线程的执行,临时启用Serial Old收集器来重新进行老年代的垃圾收集。
  • 最后,CMS是一款基于"标记-清除"算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次Full GC的情况。

3.5.7 Grabage First收集器

G1是一款主要面向服务端应用的垃圾收集器。

目标:在延迟可控的情况下获得尽可能高的吞吐量。从整体上来说是基于标记-整理,但从局部上(两个region之间)是基于复制算法。

G1面向堆内存任何部分来组成回收集(CollectionSet,一般简称CSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式。G1开创的基于Region的堆内存布局是它能够实现这个目标的关键。

虽然G1也仍是遵循分代收集理论设计的,但其堆内存的布局与其他收集器有非常明显的差异:G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间或者老年代空间
Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过一个Region容量一半的对象即可判定为大对象。每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围为1MB~32MB,且应为2的N次幂。而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代的一部分进行看待。

虽然G1仍然保留新生代和老年代的概念,但新生代和老年代不再是固定的了,它们都是一系列区域(不需要连续)的动态集合。G1收集器之所以能建立可预测的停顿时间模型,是因为它将Region作为单次回收的最小单元,即每次收集到的内存空间都是Region大小的整倍数,这样可以有计划地避免在整个Java堆中进行全区域的垃圾收集。
更具体的处理思路是让G1收集器去跟踪各个Region里面的垃圾堆积的"价值"大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间优先处理回收价值最大的那些Region,这也就是"Garbage First"名字的由来。

如果不去计算用户线程运行过程中的动作(如使用写屏障维护记忆集的操作),G1收集器的运作过程大致可划分为以下四个步骤:

  • 初始标记(Initial Marking):标记一下GC Roots能直接关联到的对象,并且修改了TAMS(Top at Mark Start)指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
  • 并发标记(Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理STAB(原始快照)记录下来的在并发时有引用变动的对象。
  • 最终标记(Final Marking):短暂暂停用户线程,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
  • 筛选回收(Live Data Counting and Evacuation):负责更新Region的统计数据,对各个Region的回收价值和成本进行统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活的对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。

G1与CMS的对比:

  • 两者的相同之处:都关注停顿时间的控制
  • G1优点:可指定最大停顿时间;创新性的分region的内存分布;按收益动态确定回收;不会产生垃圾碎片(标记-整理算法),有利程序长时间运行。
  • G1缺点:垃圾收集产生的内存占用(Footprint)和程序运行时的额外执行负载(Overload)都比CMS高。
    • 垃圾收集产生的内存占用更高主要是因为G1的卡表(又称G1记忆集)实现更为复杂。而且每个Region都必须有一份卡表。
    • 额外的执行负载更高,是因为G1不仅有更复杂的卡表操作,更是因为G1在实现时,必须将写前屏障和写后屏障中要做的事情都放到队列里,然后再异步处理。
  • 总体来看,通常在小内存应用CMS优于G1,而大内存应用G1更优。这个分界点大概是6-8G内存。

3.6 低延迟垃圾收集器

衡量垃圾收集器的三项最重要的指标是:内存占用(Footprint)、吞吐量(Throughput)和延迟(Latency),三者共同构成了一个“不可能三角”。

下图浅色阶段表示必须挂起用户线程,深色表示收集器线程与用户线程是并发工作的。
请添加图片描述

3.6.1 Shenandoah收集器

Shenandoah目标:实现一种能在任何堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的垃圾收集器

Shenandoah与G1相同之处:

  • 使用基于Region的堆内存布局。
  • 用于存放大对象的Humongous Region。
  • 默认的回收策略是优先处理回收价值最大的region。

Shenandoah与G1不同之处:

  • G1的回收过程支持多线程并行,但不能和用户线程并发(最核心的功能)。
  • Shenandoah默认不使用分代收集。
  • Shenandoah不用G1的记忆集,而是用连接矩阵(connection matrix)的全局数据结构来记录跨region的引用关系。
    • 连接矩阵可以简单理解为一张二维表格,如果Region N有对象指向Region M,就在表格的N行M列中打上一个标记。

Shenandoah收集器的工作过程大致可以划分为以下九个阶段:

  • 初始标记(Initial Marking):与G1一样,首先标记与GC Roots直接关联的对象,这个阶段仍是“Stop The World”的,但停顿时间与堆大小无关,只有与GC Roots的数量相关。
  • 并发标记(Concurrent Marking):与G1一样,遍历对象图,标记出全部可达的对象,这个阶段是与用户线程一起并发的,时间长短取决于堆中存活对象的数量以及对象图的结构复杂度。
  • 最终标记(Final Marking):与G1一样,处理剩余的SATB(原始快照)扫描,并在这个阶段统计出回收截止最高的Region,将这些Region构成一组回收集(Collection Set)。
    最终标记阶段也会有一小段短暂的停顿。
  • 并发清理(Concurrent Cleanup):这个阶段用于清理那些整个区域内连一个存活对象都没有找到的Region(这类Region被称为Immediate Garbage Region)。
  • 并发回收(Concurrent Evacuation):并发回收阶段是Shenandoah与之前的HotSpot中其他收集器的核心差异。在这个阶段,
    Shenandoah要把回收集里面的存活对象先复制一份到其他未被使用的Region之中。复制对象这件事情如果将用户线程冻结起来再做是相当简单的,但如果两者必须同时并发进行的话,就会变得复杂起来了。其困难点是在移动对象的同时,用户线程仍然可能不停对被移动的对象进行读写访问,移动对象是一次性的行为,但移动之后整个内存中所有指向该对象的引用都还是旧对象的地址,这是很难一瞬间全部改变过来的。对于并发回收阶段遇到的这些困难,Shenandoah将会通过读屏障和被称为“Brooks Points”的转发指针来解决。并发回收阶段运行的时间长短取决于回收集的大小。
  • 初始引用更新(Initial Update Reference):并发回收阶段复制对象结束后,还需要把堆中所有指向旧对象的引用修正到复制后的新地址,
    这个操作称为引用更新。引用更新的初始化阶段实际上并未做出什么具体的处理,设立这个阶段只是为了建立一个线程集合点,确保所有并发回收阶段中进行的收集器线程都已完成分配给它们的对象移动任务而已。初始引用更新时间很短,会产生一个非常短暂的停顿。
  • 并发引用更新(Concurrent Update Reference):真正开始进行引用更新操作,这个阶段是与用户线程一起并发的,时间长短取决于内存中涉及的引用数量的多少。并发引用更新与并发标记不同,它不再需要沿着对象图来搜索,只需要按照内存物理地址的顺序,线性地搜索出引用类型,把旧值改为新值即可。
  • 最终引用更新(Final Update Reference):解决了堆中的引用更新后,还要修正存在于GC Roots中的引用。这个阶段是Shenandoah的最后一次停顿,停顿时间只与GC Roots的数量相关。
  • 并发清理(Concurrent Cleanup):经过并发回收和引用更新之后,整个回收集中所有的Region已再无存活对象,这些Region都变成了Immediate Garbage Regions了,最后再调用一次并发清理过程来回收这些Region的内存空间,供以后新对象分配使用。

以上对Shenandoah收集器这九个阶段的工作过程的描述可能拆分得略为琐碎,读者只要抓住其中三个最重要的并发阶段(并发标记、并发回收、并发引用更新),就能比较容易理清Shenandoah是如何运作的了。

这里省略了关于转发指针和读屏障,以及读屏障的升级(引用屏障)。有需要请翻书。

总结:

  • Shenandoah强项:低延迟时间
  • Shenandoah弱项:高运行负担使得吞吐量下降

3.6.2 ZGC收集器

ZGC目标:ZGC和Shenandoah的目标是高度相似的,都希望在尽可能对吞吐量影响不太大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟。

ZGC的主要特征::ZGC收集器是一款基于Region内存布局的,(暂时)不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的,以低延迟为首要目标的一款垃圾收集器。

首先从ZGC的内存布局说起。与Shenandoah和G1一样,ZGC也采用基于Region的堆内存布局,但与它们不同的是,ZGC的Region具有动态性——动态创建和销毁,以及动态的区域容量大小。在x64硬件平台下,ZGC的Region可以具有大、中、小类容量:

  • 小型Region(Small Region):容量固定为2MB,用于放置小于256KB的小对象。
  • 中型Region(Medium Region):容量固定为32MB,用于放置大于等于256KB但小于4MB的对象。
  • 大型Region(Large Region):容量不固定,可以动态变化,最低4M,但必须为2MB的整数倍,用于放置4MB或以上的大对象。每个大型Region中只会存放一个大对象。

对于并发整理算法的实现,ZGC收集器采用染色指针技术。染色指针是一种直接将少量额外的信息存储在指针上的技术。
尽管Linux下64位指针的高18位不能用来寻址,但剩余的46位指针所能支持的64TB内存在今天仍然能够充分满足大型服务器的需要。鉴于此,ZGC的染色指针技术将这剩下的46位指针宽度,将其高4位提取出来存储四个标志信息,也直接导致ZGC能够管理的内存不可以超过4TB(2的42次幂)。通过这些标志位,虚拟机可以直接从指针中看到其引用对象的三色标记状态、是否进入了重分配集(即被移动过)、是否只能通过finalize()方法才能被访问到。
ZGC的多重映射只是它采用染色指针技术的伴生产物。这里不详解,请看书。

染色指针的三大优势:

  • 染色指针可以使得一旦某个Region的存活对象被移走之后,这个Region立即就能够被释放和重用掉,而不必等待整个堆中多有指向该Region的引用都被修正后才能清理。
  • 染色指针可以大幅减少在垃圾收集过程中内存屏障的使用数量,设置内存屏障,尤其是写屏障的目的通常是为了记录对象引用的变动情况,如果将这些信息直接维护在指针中,显然就可以省去一些专门的记录操作。
  • 染色指针可以作为一种可扩展的存储结构用来记录更多与对象标记、重定向过程相关的数据,以便日后更进一步提高性能。

ZGC的运作过程大致可划分为以下四个大的阶段。全部四个阶段都是可以并发执行的,仅是两个阶段中间会存在短暂的停顿小阶段,这些小阶段,譬如初始化GC Root直接关联对象的Mark Start,与之前的G1和Shenandoah的Initial Mark阶段并没有什么差异。

  • 并发标记(Concurrent Mark):与G1、Shenandoah一样,并发标记是遍历对象图做可达性分析的阶段,前后也要经过类似于G1、Shenandoah的初始标记、最终标记的短暂停顿。不同的是,ZGC的标记是在指针上而不是在对象上进行的,标记阶段会更新染色指针中的Marked 0,Marked 1标志位
  • 并发预备重分配(Concurrent Prepare for Replace):这个阶段需要根据特定的查询条件统计得出本次收集过程中要清理哪些Region,将这些Region组成重分配集(Relocation Set)。ZGC每次回收都会扫描所有的Region,用范围更大的扫描成本换取省去G1中记忆集的维护成本。因此,ZGC的重分配集只是决定了里面存放对象会被重新复制到其他的Region中,里面的Region会被释放,而不能说回收行为就只是针对这个集合里面的Region进行,因为标记过程是针对全堆的。此外,在JDK12的ZGC中开始支持的类卸载以及弱引用的处理,也是在这个阶段完成的。
  • 并发重分配(Concurrent Relocate):重分配是ZGC执行过程中的核心阶段,这个过程要**把重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表(Forward Table),记录从旧对象到新对象的转向关系。**得益于染色指针的支持,ZGC收集器能仅从引用上就明确得知一个对象是否处于重分配集之中,如果用户线程此时并发访问了位于重分配集中的对象,这次访问将会被预置的内存屏障所截获,然后立即根据Region上的转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接引用新对象,ZGC将这种行为称为指针的“自愈”(Self-Healing)能力。这样做的好处是只有第一次访问旧对象会陷入转发,也就是只慢一次,对比Shenandoah的Brooks转发指针,那就是每次对象访问都必须付出的固定开销,简单地说就是每次都慢,因此ZGC对用户程序的运行时负载要比Shenandoah来得更低一些。还有另外一个直接的好处是由于染色指针的存在,一旦重分配集中某个Region的存活对象都复制完毕后,这个Region就可以立即释放用于新对象的分配(但是转发还得留着不能释放掉),哪怕堆中还有很多指向这个对象的未更新指针也没有关系,这些旧指针一旦被使用,它们都是可以自愈的。
  • 并发重映射(Concurrent Remap):重映射所做的就是修正整个堆中指向重分配集中旧对象的所有引用,这一点可从目标角度看是与Shenandoah并发引用更新阶段一样的,但是ZGC的并发重映射并不是一个必须要“迫切”去完成的任务,因为前面说过,即使是旧引用,它也是可以自愈的,最多只是第一次使用时多一次转发和修正操作。重映射清理这些旧引用的主要目的是为了不变慢(还有清理结束后可以释放转发表这样的附带收益),所以说这并不是很“迫切”。因此,ZGC很巧妙地把并发重映射阶段要做的工作,合并到了下一次垃圾收集循环中的并发标记阶段里去完成,反正它们都是要遍历所有对象的,这样合并就节省了一次遍历对象图的开销。
    一旦所有指针都被修正之后,原来记录新旧对象关系的转发表就可以释放掉了。

3.7 选择合适的垃圾收集器

3.7.1 Epsilon收集器

  • Epsilon可以形容为"自动内存管理子系统"。一个垃圾收集器除了垃圾收集这个本职工作之外,它还需要负责堆的管理与布局、对象分配、与解释器的协作、与编译器的协作、与监控子系统协作等职责,其中至少堆的管理和对象的分配这部分功能是Java虚拟机能够正常运作的必要支持,是一个最小化功能的垃圾收集器也必须实现的内容。
  • 近年来大型系统从传统单体应用向微服务化、无服务化方向发展的趋势已越发明显,Java在这方面比起Golang等后起之秀确实有一些先天不足,使用率正渐渐下降。为了应对新的技术潮流,最近几个版本的JDK逐渐加入了提前编译、面向应用的类数据共享等支持。Epsilon也是有着类似的目标,如果读者的应用只要运行数分钟甚至数秒,只要Java虚拟机能正确分配内存,在堆内存之前就会退出,那显然运行负载极小、没有任何回收行为的Epsilon便是很恰当的选择。

补充,部分重要参数:
请添加图片描述
请添加图片描述

3.8 实战:内存分配与回收策略

本节验证实际是使用Serial加Serial Old客户端默认收集器组合下的内存分配和回收策略。这种配置收集器组合也许是开发人员做研发时的默认组合(其实现在研发时也默认使用服务端虚拟机了),但在生产环境中一般不会这样用。

3.8.1 对象优先在Eden分配

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

HotSpot虚拟机提供了-XX:+PrintGCDetails这个收集器日志参数,告诉虚拟机在发生垃圾收集行为时打印内存回收日志,并且在进程退出的时候输出当前的内存区域分配情况。

3.8.2 大对象直接进入老年代

大对象就是指需要大量连续内存空间的Java对象,最典型的大对象便是那种很长的字符串,或者元素数量很庞大的数组。

HotSpot虚拟机提供了-XX:PretenureSizeThreshold参数,指定大于该设置值的对象直接在老年代分配,这样做的目的就是避免在Eden区及两个Survivor区之间来回复制,产生大量的内存复制操作。

注意 -XX:PretenureSizeThreshold参数只对Serial和ParNew两款新生代收集器有效,HotSpot的其他新生代收集器,如Parallel Scavenge并不支持这个参数。如果必须使用此参数进行调优,可考虑ParNew加CMS的收集器组合。

-XX:+UseConcMarkSweepGC
-XX:+UseSerialGC

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

对象通常在Eden区里诞生,如果经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,该对象会被移动到Survivor空间中,并且将其对象年龄设为1岁。对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15),就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。

3.8.4 动态对象年龄判定

大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度,就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。

但是HotSpot虚拟机并不是永远要求对象的年龄必须达到-XX:MaxTenuringThreshold才能晋升老年代。

Hotspot 遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了 survivor 区的 50% 时(默认值是 50%,可以通过-XX:TargetSurvivorRatio=percent 来设置,参见 issue1199 ),取这个年龄和 MaxTenuringThreshold 中更小的一个值,作为新的晋升年龄阈值。

3.8.5 空间分配担保

新生代使用复制收集算法,但为了内存利用率,只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况——最极端的情况就是内存回收后新生代中所有对象都存活,需要老年代进行分配担保,把Survivor无法容纳的对象直接送入老年代。

老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,但一共有多少对象会在这次回收中活下来在实际完成内存回收之前是无法明确知道的,所以只能取之前每一次回收晋升到老年代对象容量的平均大小作为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多空间。

3.8节 几个部分用到的全部代码

/**
 * @author ZK
 */
public class GCMemory {
    private static final int _10MB = 1024 * 1024 * 10;

    public static void main(String[] args) {
//        minorGC();
//        bigObjectGC();
//        longTimeGC();
//        dynamicAgeGC();
        guarantee();
    }

    /**
     * VM参数:-verbose:gc -Xms200M -Xmx200M -Xmn100M -XX:+PrintGCDetails -XX:SurvivorRatio=8  -XX:+UseSerialGC
     * 对象优先在Eden分配Demo
     */
    public static void minorGC() {
        byte[] allocation1, allocation2, allocation3, allocation4;
        allocation1 = new byte[2 * _10MB];
        allocation2 = new byte[2 * _10MB];
        allocation3 = new byte[2 * _10MB];
        allocation4 = new byte[4 * _10MB]; // 出现一次Minor GC
    }

    /**
     * VM参数::-verbose:gc -Xms200M -Xmx200M -Xmn100M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=30M -XX:+UseSerialGC
     * 大对象直接进入老年代Demo
     */
    public static void bigObjectGC() {
        byte[] allocation;
        allocation = new byte[4 * _10MB]; //直接分配在老年代中
    }

    /**
     * VM参数:-verbose:gc -Xms200M -Xmx200M -Xmn100M -XX:+PrintGCDetails -XX:SurvivorRatio=8  -XX:+UseSerialGC -XX:MaxTenuringThreshold=15 -XX:+PrintTenuringDistribution
     * 长期存活的对象将进入老年代Demo
     */
    @SuppressWarnings("unused")
    public static void longTimeGC() {
        byte[] allocation1, allocation2, allocation3;
        allocation1 = new byte[_10MB / 4]; // 什么时候进入老年代决定于XX:MaxTenuringThreshold设置
        allocation2 = new byte[4 * _10MB];
        allocation3 = new byte[4 * _10MB];
        allocation3 = null;
        allocation3 = new byte[4 * _10MB];
    }

    /**
     * VM参数:-verbose:gc -Xms200M -Xmx200M -Xmn100M -XX:+PrintGCDetails -XX:SurvivorRatio=8  -XX:+UseSerialGC -XX:MaxTenuringThreshold=15 -XX:+PrintTenuringDistribution
     * 动态对象年龄判定Demo
     */
    @SuppressWarnings("unused")
    public static void dynamicAgeGC() {
        byte[] allocation1, allocation2, allocation3, allocation4;
        // allocation1+allocation2大于survivor空间一半
        allocation1 = new byte[_10MB / 4];
        allocation2 = new byte[_10MB / 4];
        allocation3 = new byte[4 * _10MB];
        allocation4 = new byte[4 * _10MB];
        allocation4 = null;
        allocation4 = new byte[4 * _10MB];
    }

    /**
     * VM参数:-verbose:gc  -Xms200M -Xmx200M -Xmn100M -XX:+PrintGCDetails -XX:+UseSerialGC -XX:SurvivorRatio=8
     * 空间分配担保Demo
     */
    @SuppressWarnings("unused")
    public static void guarantee() {
        byte[] allocation1, allocation2, allocation3, allocation4,
                allocation5, allocation6, allocation7;
        // 最后进入老年代的只有allocation1和allocation2
        allocation1 = new byte[2 * _10MB];
        allocation2 = new byte[2 * _10MB];
        allocation3 = new byte[2 * _10MB];
        allocation1 = null;

        // 执行这句之前,第一次GC
        allocation4 = new byte[2 * _10MB];
        allocation5 = new byte[2 * _10MB];
        allocation6 = new byte[2 * _10MB];
        allocation4 = null;
        allocation5 = null;
        allocation6 = null;
        // 执行这句之前,第二次GC
        allocation7 = new byte[2 * _10MB];
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值