Java 中的垃圾收集与内存分配

134 阅读25分钟

概述

垃圾收集(Garbage Collection 简称 GC)需要完成的三件事:

  • 哪些内存需要回收?
  • 什么时候回收?
  • 如何回收?

哪些内存需要回收

程序计数器、虚拟机栈、本地方法栈是线程独享的,随着线程而消亡,而且栈中每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的,因此这几个区域的内存分配和回收都具备确定性,当方法结束或线程结束时,内存就会自动回收。 一般讨论的“内存”分配和回收都是基于:Java 堆和方法区这两个区域。这两个区域所需要的内存并不确定,在运行时不同的实现类、分支条件等所需要的内存都是不一样的。

垃圾存活判定算法

判断一个对象是否存活:若一个对象不被任何对象或变量引用,那么它就是无效对象,需要被回收。

引用计数法

在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。 引用计数法实现简单,判定效率高,在大部分情况下是个不错的算法。但是在 Java 虚拟机中没有选用引用计数法算法,主要是因为它很难解决对象之间循环引用的问题

什么是对象间的循环引用

举个栗子: 对象 objA 和 objB 都有字段 instance,令 objA.instance = objB 并且 objB.instance = objA,由于它们互相引用着对方,导致它们的引用计数都不为 0,于是引用计数算法无法通知 GC 收集器回收它们。

可达性分析法

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

image.png

所有和 GC Roots 直接或间接关联的对象都是有效对象,和 GC Roots 没有关联的对象就是无效对象。 GC Roots 的对象包括:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象

    • 比如方法堆栈中使用的参数、局部变量、临时变量
  • 方法区中类静态属性引用的对象

    • 比如 Java 类的引用类型的静态变量
  • 方法区中常量引用的对象

    • 是指在类中被声明为静态 final 的变量所引用的对象
  • 本地方法栈中 JNI(即通常所说的 Native 方法)引用的对象

  • Java 虚拟机内部的引用

    • 如基本数据类型对应的 Class 对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器
  • 所有被同步锁(synchronized 关键字)持有的对象

  • 反映 Java 虚拟机内部情况的 JM XBean、JVM TI 中注册的回调、本地代码缓存等 GC Roots 并不包括堆中对象所引用的对象,这样就不会有循环引用的问题。

引用类型

在 Java1.2 之前一个对象只有“被引用”和“未被引用两种状态”

在 JDK 1.2 版之前,Java 里面的引用是很传统的定义: 如果 reference 类型的数据中存储的数值代表的是另外一块内存的起始地址,就称该 reference 数据是代表 某块内存、某个对象的引用。

在 Java1.2 之后对引用的概念进行了扩充。这四种引用强度依次逐渐减弱:

  • 强引用(Strongly Reference)
  • 软引用(Soft Reference)
  • 弱引用(Weak Reference)
  • 虚引用(Phantom Reference)

强引用

强引用是最传统的“引用”的定义,类似Object obj = new Object()。只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。

软引用

使用SoftReference类来实现软引用。 用来描述一些还有用,但非必需的对象。在系统将要发生内存溢出之前,会把这些对象列入回收范围进行第二次回收,如果这次回收之后还没有足够的内存才会抛出内存溢出异常。

弱引用

使用WeakReference类来实现软引用。 强度比软引用更弱一些,当 JVM 进行垃圾回收时,无论内存是否充足,都会回收只被弱引用关联的对象。

虚引用

使用PhantomReference类来实现虚引用 虚引用也称幽灵引用或者幻影引用,它是最弱的一种引用关系。不影响对象的生存时间,也无法通过虚引用获取对象实例,它仅仅是用来喂了能在对象被垃圾收集器回收时收到一个通知。

对象的自我拯救(可以不看)

即使在可达性分析算法中判定为不可达的对象,也不是“非死不可”的。标记一个对象死亡首先需要经过两次标记过程:

  1. 发现对象不可达之后进行第一次标记。
  2. 判断对象是否有必要执行finalize()。只有对象覆盖了finalize()方法,并且未被虚拟机调用,才会标记为“有必要执行”,否则标记为“没有必要执行”。 1. 对于有必要执行的finalize()的对象,会放置在名为 F-Queue 的队列之中,虚拟机会创建一个低优先级的 Finalizer 线程去执行 finzlize()方法。这里都是异步执行的,防止执行慢或死循环。 2. 在执行 finalize()方法时,对象有最后一次拯救自己的机会,若在方法中重新与引用链上的任何一个对象建立连接关联(比如把 this 赋值给一个类变量) finalize()能做的所有工作,使用 try -finally 或者其他方式都可以做得更好、 更及时,所以笔者建议大家完全可以忘掉 Java 语言里面的这个方法

垃圾收集算法

分代收集理论

当前商业虚拟机的垃圾收集器,大多数都遵循了“分代收集”,它建立在两个分代假说之上:

  1. 弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。
  2. 强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。
  3. 跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极 少数。 Java 堆一般会划分为新生代(Young Generation)老年代(Old Generation)两个区域。在新生代中,每次垃圾收集 时都发现有大批对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放。 部分收集(Partial GC)的收集类型:
  • 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。

    • 标记-赋值算法
  • 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有 CMS 收集器会有单独收集老年代的行为

    • 标记-清除算法
    • 标记-整理算法
  • 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有 G1 收集器会有这种行为。

  • 整堆收集(Full GC):收集整个 Java 堆和方法区的垃圾收集。

标记-清除算法

算法分为“标记”和“清除”两个阶段: 首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回 收所有未被标记的对象。 image.png 标记-清除算法是最基础的收集算法,是因为后续的收集算法大多都是以标记-清除算法为基础,对其缺点进行改进而得到的。主要缺点有两个:

  1. 执行效率不稳定

    1. 如果 Java 堆中包含大量需要回收的对象,那么就需要进行大量的标记、清除动作,导致标记和清除的效率随着对象数量增长而降低
  2. 内存空间的碎片化问题

    1. 标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

标记-复制算法(新生代)

复制算法将可用内存划分为一样的两块,每次只使用其中一块。当这一块内存用完了,就将还活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。 优点:

  1. 实现简单、运行高效
  2. 不会有内存碎片问题 缺点:
  3. 若内存块中大量都是存活的对象,会产生大量的内存间复制的开销。所以此算法一般使用在新生代。
  4. 内存缩小为原来的一半,浪费空间。 为了解决空间利用率问题,可以将内存分为三块:Eden、From Survivor、To Survivor,比例是 8:1:1(为什么默认是这个比例,因为新生代中大概有 98%熬不过第一轮收集)。回收时,将 Eden 和 Survivor 中还存活的对象一次性复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才使用的 Survivor 空间。这样只会有一个 Survivor 的空间会被浪费(10%)。 image.png

分配担保

由于任何人都没有办法百分百保证每次回收都有不多于 10%的对象存活,当 Survivor 空间不够时,需要依赖老年代进行分配担保。 当 To Survivor 空间没有足够的空间存放 Minor GC 后存活下来的对象,这些对象便将分配担保机制直接进入老年代。

标记-整理算法(老年代)

标记-整理算法和标记-清除算法类似,第一阶段都是标记,均是遍历 GC Roots 将存活的对象笔记。第二阶段不是清除可回收对象,而是将存活的对象都向内存的一端移动,然后清理掉边界以外的内存。

image.png 优势:

  • 没有内存碎片

劣势:

  • 由于老年代中每次回收都有大量的对象存活,移动存活对象是一个极为负重的操作,这种操作需要暂停用户应用程序才能进行。

HotSpot 算法细节实现

根结点枚举

可达性分析判定对象是否存活的标准是从 GC Roots(根结点)出发能够引用到的对象。GC Roots 寻找的过程

  1. 首先会“Stop The World”,暂停用户线程。如果要保证根结点分析的准确性,就一定要保证分析时内存的一致性。
  2. 使用一组 OopMap 的数据结构来存储的根结点。在类加载完成时,虚拟机会在特定位置(安全点 safe point) 记录下栈里和寄存器里哪些位置是引用。收集器在扫描时,就可以得知这些信息,不需要一个全部查找一遍。

安全点和安全区域(安全区域是拉长的安全点)

在安全点中生成了 OopMap,用于 GC Roots 枚举,Stop-the-world 也是通过安全点来实现,再通过 GC Roots 标记内存进行垃圾清理。当 Java 虚拟机收到 Stop-the-world 请求,它便会等待所有的线程都到达安全点,才允许请求 Stop-the-world 的线程进行独占的工作。 Java 虚拟机的实现方式使以主动式中断的思想实现的:当垃圾手机需要中断线程时,不直接对线程操作,而是设置一个标志位,各个线程执行过程时会不停的主动轮训这个标志,一旦发现中断标志为真时,便在最近的安全点上主动挂起。 编译器一般会在方法调用、循环跳转、异常跳转的情况下插入安全点。 Java 线程不同状态的安全点:

  1. 使用 JNI 调用本地方法(Native 方法):

    1. 如果这段本地代码不访问 Java 对象、调用 java 方法或者返回之原 Java 方法,那么 Java 栈不会改变,标志这这段代码是同一个安全点,也叫安全区域。
  2. 解释执行字节码:

    1. 每一个字节码与字节码之间皆可以作为安全点。当有安全点请求时,执行一条字节码便进行一次安全点检测。
  3. 执行即时编译器生成的机器码

    1. 是在生成机器码时,即时编译器插入安全检测,避免机器码长时间没有安全点检测的情况。虚拟机的做法是在生成代码的方法出口以及非计数循环的循环回边(back-edge)处插入安全点检测
  4. 线程阻塞

    1. 处于 Java 虚拟机线程调度器的掌控之下,因此在此状态下线程都属于安全点,也叫安全区域。

安全区域

记忆集于卡表

记忆集适用于解决跨代引用问题的原理和思想。 一般对于收集器只需要通过记忆集判断出某一块非收集区域是否有指向了收集区域的指针就可以了。比如:

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

卡表(Card Table)

卡表的实现是将整个堆(若不是 G1 收集器就是将老年代)划分为一个个大小为 512 字节的卡叶,并且维护一个卡表,用来存储每张卡叶的一个标识位。一个卡叶如果有一个或多个对象字段存在跨代指针,那么我们就认为这张卡是脏的,将对应的卡表元素的值标记为 1。 在虚拟机中卡表的实现是一个字节数组CARD_TABLE [this address >> 9] = 0;

image.png 在垃圾收集发生时,只要筛选出变脏的卡叶,把它们加入 GC Roots 中一并扫描。

卡表元素为何变脏、何时变脏和如何变脏? 为何变脏:有其他分代区域中对象引用了本区域对象时,其对应的卡表元素就应该变脏。 何时变脏:发生在引用类型字段赋值的那一刻。 如何变脏:解释执行的字节码是执行到赋值的字节码后虚拟机插入一条变脏的指令就好了。而编译执行的场景中,通过写屏障,在机器码中进行操作。

写屏障

写屏障(write barrier)是为了解决编译执行场景中卡表如何变脏的问题的。(注意不要和 volatile 字段的写屏障混淆)

实现

写屏障可以看作在虚拟机层面对“引用类型字段赋值”这个动作的 AOP 切面。在引用对象赋值时会产生一个环形(Around)通知。但是写屏障是一个额外的动作,需要保持简洁。 因此,写屏障并不会判断更新后的引用是否指向新生代中的对象,而是一律当成可能指向新生代对象的引用。不过这个开销与 Minor GC 时扫描整个老年代的代价相比还是低得多的。

伪共享(虚共享) --选择性看

卡表在高并发场景下还面临着“伪共享”(False Sharing)问题。 伪共享是处理并发底层细节时一种经常需要考虑的问题,现代中央处理器的缓存系统中是以缓存行(Cache Line) 为单位存储的,当多线程修改互相独立的变量时,如果这些变量恰好共享同一个缓存行,就会彼此影响(写回、无效化或者同步)而导致性能降低,这就是伪共享问题。 在 HotSpot 中,卡表是通过 byte 数组来实现的。对于一个 64 字节的缓存行来说,如果用它来加载部分卡表,那么它将对应 64 张卡,也就是 32KB 的内存。如果同时有两个 Java 线程,在这 32KB 内存中进行引用更新操作,那么也将造成存储卡表的同一部分的缓存行的写回、无效化或者同步操作,因而间接影响程序性能。 为此,HotSpot 引入了一个新的参数 -XX:+UseCondCardMark,用来决定是否开启卡表更新的条件判断。开启会增加一次额外判断的开销,但能够避免伪共享问题,两者各有性能损耗,是否打开要根据应用实际运行情况来进行测试权衡。

if (CARD_TABLE [this address >> 9] != DIRTY)
 CARD_TABLE [this address >> 9] = DIRTY;

并发的可达性分析

当用户线程和收集器并发执行时的可达性分析会产生的问题:

  1. 把原本消亡的对象错误标记为存活

    1. 这不是好事,但其实是可以容忍的,只不过产生了一点逃过本次收集的浮动垃圾而已,下次收集清理掉就好。
  2. 把原本存活的对象错误标记为已消亡,这就是非常致命的后果了,程序肯定会因此发生错误

问题产生原因:

按照三色标记法分析

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

image.png

理论证明,当且仅当以下两个条件同时满足时,会产生“对象消失”的问题:

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

解决方案:

增量更新:增量更新要破坏的是第一个条件,黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象了,扫描结束后再将这些灰色的对象重新扫描一次。 原始快照:原始快照要破坏的是第二个条件,当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次。这也可以简化理解为,无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来进行搜索。

经典垃圾收集器

image.png 一共其中常见的垃圾收集器,上面部分是新生代收集器,下面部分是老年代收集器,一般可以作为搭配使用的使用实线链接。每一款垃圾收集器都有自己的特点,并没有一种收集器可以兼顾所有场景,所以需要了解各个收集器各自的特点针对不同的场景选择合适的垃圾收集器。

Serial 收集器

这是一个最基础、历史最久远的收集器。 优点:

  1. 单线程效率高
  2. 所有收集器中额外内存消耗最小 缺点:
  3. stop the world 在 Serial 收集器执行时需要暂停用户线程

image.png 基于以上的原因,Serial 收集器适合用在资源较少的情况,比如客户端或者微服务一般只会给微服务分配一百兆左右的新生代,这种需要回收的垃圾少,每次仅仅停顿几十毫秒也还是可以接受的。

ParNew 收集器

ParNew 是 Serial 的多线程版本。由多条 GC 线程并行地进行垃圾清理。但清理过程依然需要 Stop The World。

ParNew 追求“低停顿时间”,与 Serial 唯一区别就是使用了多线程进行垃圾收集,在多 CPU 环境下性能比 Serial 会有一定程度的提升。默认 ParNew 的线程数是 CPU 核心数,使用-XX:ParallelGCT hreads 参数来限制垃圾收集的线程数。

image.png 由于只有 ParNew 或 Serial 可以和 CMS 搭配使用,所以在 jdk1.5 之后到 jdk1.8 中,ParNew+CMS 的组合是官方推荐组合。

Parllel Scavenge 收集器

Parallel Scavenge 和 ParNew 一样,都是多线程、新生代垃圾收集器。但是两者有巨大的不同点:

  • Parallel Scavenge:追求 CPU 吞吐量,能够在较短时间内完成指定任务,因此适合没有交互的后台计算。
  • ParNew:追求降低用户停顿时间,适合交互式应用。

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

追求高吞吐量,可以通过减少 GC 执行实际工作的时间,然而,仅仅偶尔运行 GC 意味着每当 GC 运行时将有许多工作要做,因为在此期间积累在堆中的对象数量很高。单个 GC 需要花更多的时间来完成,从而导致更高的暂停时间。而考虑到低暂停时间,最好频繁运行 GC 以便更快速完成,反过来又导致吞吐量下降。

  • 通过参数 -XX:GCTimeRadio 设置垃圾回收时间占总 CPU 时间的百分比。
  • 通过参数 -XX:MaxGCPauseMillis 设置垃圾处理过程最久停顿时间。
  • 通过命令 -XX:+UseAdaptiveSizePolicy 开启自适应策略。我们只要设置好堆的大小和 MaxGCPauseMillis 或 GCTimeRadio,收集器会自动调整新生代的大小、Eden 和 Survivor 的比例、对象进入老年代的年龄,以最大程度上接近我们设置的 MaxGCPauseMillis 或 GCTimeRadio。

Serial Old 收集器

Serial Old 收集器是 Serial 的老年代版本,都是单线程收集器,只启用一条 GC 线程,都适合客户端应用。它们唯一的区别就是:Serial Old 工作在老年代,使用“标记-整理”算法;Serial 工作在新生代,使用“复制”算法。

Parllel Old 收集器

Parallel Old 收集器是 Parallel Scavenge 的老年代版本,追求 CPU 吞吐量。

CMS 收集器

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

  1. 初始标记(CMS initial mark)

    1. 仅仅标记 GC Roots 能直接关联到的对象,速度很快。
    2. “Stop The World”,耗时短,需要停顿用户线程。
  2. 并发标记(CMS concurrent mark)

    1. 从 GC Roots 的直接关联对象开始遍历整个对象图的过程。
    2. 耗时较长,但和用户线程并发执行。
  3. 重新标记(CMS remark)

    1. 这个步骤是为了修正并发标记期间,因用户线程继续运作而导致标记产生变动的那一部分对象的标记记录。
    2. “Stop The World”,耗时较短,仅比初始标记阶段长,需要停顿用户线程。
  4. 并发清除(CMS cuncurrent sweep) 1. 清除死亡对象 2. 并发执行,直接清除不需要移动存活对象。 优点:并发执行、低停顿。 缺点:

  5. 对处理器资源非常敏感,CMS 默认启动的回收线程数是(处理器核心数量 +3)/4。cpu 核心数较少时垃圾收集器占用的资源就会特别多。

  6. 无法处理“浮动垃圾”,所以当 CMS 垃圾收集器运行时还需要提供足够的内存空间给用户线程使用,在 JDK6 中当老年代使用 92%的内存空间后就会激活垃圾收集。但是也会有一种风险,剩余的空间不够内存分配,就会出现“(并发失败)Concurrent Mode Failure”失败进而导致另一次完全“Stop The World”的 Full GC(使用 Serial Old)的产生。

    1. 浮动垃圾:在 CMS 的并发标记和并发清理阶 段,用户线程是还在继续运行的,程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分 垃圾对象是出现在标记过程结束以后,CMS 无法在当次收集中处理掉它们,只好留待下一次垃圾收集时再清理掉。
  7. 基于“标记-清除”算法,会产生大量的内存碎片。当碎片空间太多,没有足够的空间分配大对象时就不得不提前触发一次 Full GC。

    1. 由于 Full GC 时必须整理移动存活对象,无法并发执行,停顿时间会变长,可以通过-XX:CMSFullGCsBefore-Compaction(此参数从 JDK 9 开始废弃),这个参数的作用是要求 CMS 收集器在执行过若干次(数量 由参数值决定)不整理空间的 Full GC 之后,下一次进入 Full GC 前会先进行碎片整理(默认值为 0,表 示每次进入 Full GC 时都进行碎片整理)。

G1 收集器

G1 是一款面向服务端应用的垃圾收集器,它没有新生代和老年代的概念,而是将堆划分为一块块独立的 Region。当要进行垃圾收集时,首先估计每个 Region 中垃圾的数量,每次都从垃圾回收价值最大的 Region 开始回收,因此可以获得最大的回收效率。

Region分为新生代Eden、新生代Survive、老年代Old、大对象Humongous(大对象一般会按照老年代来处理,当一个Humongous空间不够时,会使用连续多个Humongous来存储)

从整体上看, G1 是基于“标记-整理”算法实现的收集器,从局部(两个 Region 之间)上看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。

G1 收集器的工作过程分为以下几个步骤:

  • 初始标记:Stop The World,标记GC Roots能直接关联的对象。
  • 并发标记:从GC Roots直接关联对象开始遍历整个引用链的过程。
  • 最终标记:Stop The World,处理并发阶段遗留下来的少量原始快照记录。
  • 筛选回收:回收废弃对象,此时也要 Stop The World,并使用多条筛选回收线程并发执行。

内存分配与回收策略

对象的内存分配,从概念上讲,应该都是在堆上分配。在经典分代的设计下,新生对象通常会分配在新生代中,少数情况下(例如对象大小超过一定阈值)也可能会直接分配在老年代。

对象优先在Eden分配

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

大对象直接进入老年代

大对象在Serial和ParNew收集器中使用-XX:PretenureSizeThreshold 参数进行配置,指定大于该设置值的对象直接在老年代分配。 在G1收集器中大对象的判定是大于Region块的一半内存就被判定为大对象。

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

虚拟机给每个对象定义了一个对 象年龄(Age)计数器,存储在对象头中。对象在eden区出生,当经过一次GC后仍然存活,就会晋升到Survivor区,且对象每熬过一次GC,对象年龄就会+1岁,直到达到阈值,就会被晋升到老年代。这个阈值默认为15,通过-XX:MaxTenuringThreshold=15设置。

动态对象年龄判定

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

空间分配担保

在Minor GC之前,虚拟机必须先检查老年代的连续空间是否大于新生代对象总大小或者历次晋升的平均大小,若大于就会进行Minor GC,否则将进行Full GC。