likes
comments
collection
share

JVM垃圾收集与内存分配

作者站长头像
站长
· 阅读数 25

一、前言

Java自动内存管理,通俗易懂的解释就是,虚拟机自动给对象分配内存以及自动回收分配给对象的内存。不需要程序员插手这件事,虚拟机会自动管理,不同的虚拟机会有不同的实现方式。大部分情况下,Java程序员无需关心内存,无需为对象去写 delete/free 代码。但如果出现内存泄漏或溢出,当垃圾收集成为系统追求更高性能的瓶颈时,如果不了解虚拟机使用内存的方法,修复问题将非常艰难。所以,我们就必须对这些 “自动化” 的技术有所了解,知其然知其所以然。

二、对象是否存活?

垃圾收集器要做的第一件事情就是确定哪些对象是否 “存活” 着,哪些对象是否已经 “死去”。判断对象存活或死亡的算法一般谈到的有两种:引用计数算法可达性分析算法

2.1 引用计数算法

引用计数算法很简单,它实际上是通过在对象中分配一个空间来保存该对象被引用的次数。如果该对象被其它对象引用,则它的引用计数加一,如果删除对该对象的引用,那么它的引用计数就减一,当该对象的引用计数为0时,那么该对象就会被回收。

客观来说,引用计数算法原理简单易懂,判断对象存活或死亡效率高,虽然占用了一些额外的内存空间来进行计数,但是在大多数情况下都是一个不错的算法。但是,在主流的 Java虚拟机中,都没有选用引用计数算法来管理内存。主要原因是,这个看似简单的算法,有很多例外情况要进行考虑,必须要配合大量额外的处理才能保证正确的工作,比如对象之间 相互循环引用 的问题。

public class User {

    public Object instance = null;

    public static void main(String[] args) {
        // 引用计数加1
        User userA = new User();
        // 引用计数加1
        User userB = new User();
        // 引用计数加1
        userA.instance = userB;
        // 引用计数加1
        userB.instance = userA;
        // 引用计数减1
        userA = null;
        // 引用计数减1
        userB = null;
    }
}

上述代码如果是引用计数算法,new出来的这两个User对象其实已经用不到了,可以判断为 “死亡” ,但是引用计数都为1,回收不掉。

2.2 可达性分析算法

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

JVM垃圾收集与内存分配 objectF、objectG、objectH之间虽然有引用,但是到GC Roots是不可达的,因此会被判定为可回收的对象。

那么在Java体系中,什么对象可以作为 GC Roots呢?

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
  • 方法区中类静态属性引用的对象,譬如 Java 类中的引用类型静态常量。
  • 方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
  • 在本地方法中 JNI (即通常所说的 Native 的方法)引用的对象
  • Java 虚拟机内部的引用,如基础数据类型对应的 Class 对象,一些常驻的异常对象(NullPointException,OutOfMemoryError)等,还有系统类加载器。
  • 所有 被同步锁(Synchronized关键字)持有的对象
  • 反映 Java 虚拟机内部情况的 JMXBean、JVMTI中注册的回调、本地代码缓存等。

2.3 Java中的引用

我们了解到,无论是引用计数算法还是可达性分析算法,都与 “引用” 脱不开关系,我们仔细想一下,一个对象只有 “被引用” 和 “未被引用” 两种状态,是不是太过狭隘,对于描述一些 “食之无味,弃之可惜” 的对象就显得无能为力。譬如我们希望一种对象:内存空间足够时,能保留在内存中,如果内存空间在内存回收后仍然非常紧张,那么就可以抛弃。

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

  • 强引用:是我们最常见的引用方式,Object obj = new Object()这种赋值就是强引用。无论在任何情况下,哪怕内存溢出,只要强引用关系还存在,垃圾回收器就不会回收被强引用的对象。
  • 软引用:刚刚提过所 “希望” 的一种对象,软引用就能满足。软引用是对强引用的弱化版。在内存充足时,引用可达,内存不足时候,会把这些对象列进回收返回之中进行二次回收。
  • 弱引用:它的强度比软引用更弱,当被垃圾收集器扫描到时就会被回收,用来描述那些非必须的对象。
  • 虚引用:也被称为 “幽灵引用” 或 “幻象引用” , 是引用中最弱的一种。虚引用必须和引用队列(ReferenceQueue)联合使用,其规定在构造方法中必须增加引用队列,虚引用的 get() 方法直接返回 null,因此无法通过虚引用来获取对一个对象的真实引用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中,因此使用虚引用可以来判断对象是否即将被回收。
public class User {
    public Object instance = null;

    public static void main(String[] args) {
        // 强引用
        User stronglyReference = new User();
        // 软引用
        SoftReference<User> userSoftReference = new SoftReference<>(new User());
        // 弱引用
        WeakReference<User> userWeakReference = new WeakReference<>(new User());
        // 虚引用
        PhantomReference<User> userPhantomReference = new PhantomReference<>(new User(), new ReferenceQueue<User>());
    }
}

2.4 finalize()方法

finalize()Objectprotected方法,子类可以覆盖该方法以实现资源清理工作,GC在回收对象之前调用该方法。

一些被可达性分析算法判定为不可达的对象,也不是 “非死不可” 的,这时它们还暂时处于 “缓刑” 阶段,要真正宣布一个对象死亡,至少要经历两次标记过程:对象在进行可达性分析后发现没有与 GC Roots 相连接的引用链,将会被第一次标记,随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法,假如对象没有覆盖finalize()方法,或者finalize()方法已经被调用过了(任何一个对象的finalize()方法都只会被系统自动调用一次),那么虚拟机将这两种情况视为 “没有必要执行”。

如果这个对象有必要执行finalize()方法,那么对象将被放置在一个名为F-Queue的队列之中,并在稍后由一条由虚拟机自动建立的、低调度优先级的Finalizer线程区执行它们的finalize()方法。

“执行” 是指虚拟机会触发这个方法开始运行,但并不承诺一定会等待它运行结束。这么做的原因是如果一个对象的finalize()方法执行缓慢,或者发生了死循环,或者运行出现错误,将很有可能导致F-Queue队列中的其他对象永久处于等待之中,设置导致整个内存回收子系统发生崩溃。

finalize()方法是对象逃脱死亡命运的最后一次机会,稍后收集器将对F-Queue队列中的对象进行第二次小规模的标记,如果对象在finalize()方法中成功与引用链上的任一对象建立关联,那么第二次标记就会将它移出 “即将回收的集合”,它将逃脱成功。

finalize()方法一般不用!被执行的不确定性太大,不受控!虚拟机不承诺一定会等待它运行结束。

三、垃圾收集算法

之前文章写过关于运行时数据区的一篇文章,感兴趣的可以看一下: 一文搞懂Java内存区域——运行时数据区

我们知道Java内存区域发生内存回收的区域主要是。元空间垃圾收集的 “性价比” 通常是比较低的,《Java虚拟机规范》中提到过可以不要求虚拟机在方法区实现垃圾收集。方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型。本文主要介绍的是 Java堆 的垃圾收集。

从如何判断对象消亡的角度出发,垃圾收集算法可以划分为 “引用计数式垃圾收集” (Reference Counting GC)和 “追踪式垃圾收集。” (Tracing GC),这两类也被称为 “直接垃圾收集” 和 “间接垃圾收集”。主流 Java 虚拟机都未涉及引用计数式垃圾收集算法,所以本文介绍的是追踪式垃圾收集。

3.1 分代收集理论

Java堆分为新生代(伊甸园区和幸存者区)和老年代,那么为何要这么分呢,这一切的出现都始于分代收集理论,我们先来看一下。

3.1.1 什么是分代收集理论

分代收集名为理论,实质是一套符合大多数程序运行实际情况的经验法则,它建立在两个分代假说之上:

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

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

3.1.2 第三条假说

假如要现在进行一次只局限于新生代区域内的收集(Minor GC),但新生代中的对象是完全有可能被老年代所引用的,为了找出该区域中的存活对象,不得不在固定的GC Roots之外,再额外遍历整个老年代中所有对象来确保可达性分析结果的正确性,反过来也是一样。遍历整个老年代所有对象的方案虽然理论上可行,但无疑会为内存回收带来很大的性能负担。为了解决这个问题,就需要对分代收集理论添加第三条经验法则:

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

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

3.1.3 如何解决跨代引用

依据这条假说,我们就不应再为了少量的跨代引用去扫描整个老年代,也不必浪费空间专门记录 每一个对象是否存在及存在哪些跨代引用,只需在新生代上建立一个全局的数据结构(该结构被称 为“记忆集”,Remembered Set),这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。此后当发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GCRoots进行扫描。虽然这种方法需要在对象改变引用关系(如将自己或者某个属性赋值)时维护记录数据的正确性,会增加一些运行时的开销,但比起收集时扫描整个老年代来说仍然是划算的。

3.2 标记-清除算法

“标记-清除”(Mark-Sweep)算法是最早出现也是最基础的垃圾收集算法。

算法过程分为 “标记”、“清除” 两个阶段:首先标记出所有需要回收的对象,标记完成后,统一回收掉标记好的对象。 当然也可以反过来标记存活的对象,回收所有未被标记的对象。标记的过程就是判断对象是否存活的过程。

标记-清除算法容易实现,后续算法大多都以标记清除算法为基础,对其缺点进行改造而实现。他的缺点主要有两个:

  1. 执行效率不稳定。如果Java堆中包含大量的对象,并且大部分对象是需要回收的,此时就需进行大量的标记清除动作,导致标记、清除两个过程的执行效率都随着对象的数量增长而降低。
  2. 内存空间碎片化问题。标记、清除之后会产生大量的不连续的内存碎片,空间碎片太多可能导致以后程序运行过程要分配较大对象时无法找到足够的连续内存而不得不提前触发一次垃圾收集动作。 JVM垃圾收集与内存分配

3.3 标记-复制算法

为解决标记清除算法面对大量可回收对象时执行效率低的问题,首先提出了了一种 “半区复制” 的垃圾收集算法。它将内存按容量会分为两块大小相等的区域,每次只使用其中的一块。当这一块内存用完了,就将还存活的对象对象复制到另一块上面去,然后把已经使用过的这一块内存空间一次清理掉

算法需要复制的就是占少数的存活对象,而且每次都是针对整个半区进行复制,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。这种复制算法的缺点就是将可用内存缩小到了原来的一半,空间浪费未免太多了一点。 JVM垃圾收集与内存分配

现在商用的 Java 虚拟机大多都采用了这种算法来回收新生代。曾有公司做过研究,新生代中的对象有98%熬不过第一轮收集,因此并不需要按照1:1的比例来划分新生代的内存空间。

1989年,Andrew Appel针对具备“朝生夕灭”特点的对象,提出了一种更优化的半区复制分代策 略,现在称为“Appel式回收”。Appel式回收具体是把新生代划分为一块较大的Eden空间 和两块较小的Survivor空间,每次分配内存时只使用Eden和其中的一块Survivor。当发生垃圾收集时,将Eden和Survivor中仍然存活的对象一次性复制到另一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间。 HotSpot默认Eden和Surivor的大小比例是8:1。

Appel式回收分配担保机制:如果Survivor空间没有足够空间存放上一次新生代收集下来的存活对象,这些对象便将通过分配担保机制直接进入老年代,这对虚拟机来说就是安全的。

3.4 标记-整理算法

针对老年代对象存亡特征,1974年Edward Luerders提出了另外一种有针对性的标记-整理(Mark-Compact)算法。标记过程与标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让存活对象向内存空间一端移动,然后直接清理掉边界以外的内存。 JVM垃圾收集与内存分配 标记-清除算法与标记-整理算法的本质差异在于前者是一种非移动式的回收算法,而后者是移动式的。是否移动回收后的存活对象是一项优缺点并存的风险决策:

  • 若移动存活对象,尤其是在老年代这种每次回收都有大量对象存活的区域,移动存活对象并更新所有引用这些对象的地方将会是一种极为负重的操作,而且这种对象移动操作必须全程暂停用户应用程序才能进行。
  • 若完全不考虑移动和整理存活对象的话,弥散于堆中的存活对象导致的空间碎片化问题只能依赖更为复杂的内存分配器和内存访问器来解决。内存的访问是用户程序最频繁的操作,假如在这个环节上增加了额外的负担,就会直接影响应用程序的吞吐量

还有一种“和稀泥式”解决方案可以不在内存分配和访问上增加太大额外负担,做法是让虚拟机平时多数时间都采用标记-清除算法,暂时容忍内存碎片的存在,直到内存空间的碎片化程度已经大到影响对象分配时,再采用标记-整理算法收集一次,以获得规整的内存空间。HotSpot虚拟机里边关注吞吐量的 Paraller Scavenge 收集器是基于标记-整理算法的,而关注延迟的CMS收集器则是基于标记-清除算法的。

四、垃圾收集器

《Java虚拟机规范》中对垃圾收集器应该如何实现并没有做出任何规定,因此不同厂商、不同版本的虚拟机所包含的垃圾收集器可能会有很大的差别,不同虚拟机一般也会提供各种参数供用户配置所使用的垃圾收集器。

4.1 Serial

Serial收集器,最基础、历史最悠久的收集器。是一个单线程工作的收集器,只会使用一个处理器或一条收集线程去完成垃圾收集工作,在进行工作的时候必须暂停其他所有工作线程,Stop The World,直到收集结束。 Serial收集器简单而高效,消耗内存少,Serial收集器对于运行在客户端模式下的虚拟机来说是一个新生代收集器很好的选择。

4.2 ParNew

ParNew收集器,实质上是Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之外,其余的行为包括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一致,在实现上这两种收集器也共用了相当多的代码。还能与CMS收集器配合工作。

4.3 Parallel Scavenge

Parallel Scavenge收集器,同样基于标记-复制算法实现,也是能够并行收集的多线程收集器。Parallel Scavenge的特点是它的关注点与其他收集器不同,其他收集器尽可能缩短垃圾收集时用户线程的停顿时间,Parallel Scavenge收集器的目标是达到一个可控制的吞吐量

4.4 Serial Old

Serial Old收集器,是Parallel Scavenge收集器的老年代版本,基于标记-复制算法实现,支持多线程并发收集。

4.4 CMS

CMS收集器,CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,CMS收集器是基于标记-清除算法实现的,运作过程相比较前几种更加复杂,整个过程分为四个步骤:

  1. 发现初始标记(CMS initial mark):需要Stop The World,仅仅标记GCRoots能够直接关联的对象,速度很快。
  2. 并发标记(CMS concurrent mark):从GC Roots的直接关联对象开始遍历整个对象图的过程,耗时长,但是不需要停顿用户线程,可以与垃圾收集现线程并发执行。
  3. 重新标记(CMS remark):需要Stop The World,修正并发标记期间,因为用户程序继续运作导致标记发生变动的那部分对象,停顿时间通常比初始标记长,但也远比并发标记短。
  4. 并发清除(CMS concurrent sweep):清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段可以与用户线程并发的。

4.5 Garbage First

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

HotStop开发团队最初赋予它的期望是(在比较长期的)未来可以替换掉JDK 5中发布的CMS收集器。JDK 9 发布之日,G1 成为服务端模式下的默认垃圾收集器

4.5.1 停顿时间模型

设计者们希望做出一款能够建立起 “停顿时间模型” 的收集器,停顿时间模型的意思是能够支持指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过 N 毫秒这样的目标,这几乎已经是实时 Java (RTSJ) 的中软实时垃圾收集器的特征了。

要实现这个目标,首先要有一个思想上的改变,在G1垃圾收集器出现之前的所有收集器,在垃圾收集目标范围要么是整个新生代,要么是整个老年代,在要么就是整个 Java 堆。G1跳出了这个樊笼,它可以面向堆内存任何部分来组成回收集(Collection Set,简称CSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的 Mixed GC 模式

4.5.2 何为Region?

虽然G1也仍是遵循分代收集理论设计的,但其堆内存的布局与其他垃圾收集器有非常明显的差异:G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的 Java 堆划分为多个大小相等的独立区域(Region),每一个 Region 都可以根据需要,扮演新生代的 Eden 空间、Survivor空间、或者老年代空间。收集器能够对扮演不同角色的 Region 采取不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能够获取更好的收集效果。

Region 中还有一类特殊的 Humongous空间,专门用来存储大对象。而对于那些超过了整个 Region 容量的超级大对象,将会被存放在N个连续的 Humongous Region 之中。

4.5.3 Garbage First 名字的由来

G1收集器之所以能够建立可预测的停顿时间模型,是因为将Region作为单词回收的最小单元,即每次收集到的内存空间都是 Region 大小的整数倍,这样可以有计划地避免在整个 Java 堆中进行全区域的垃圾收集。更具体的处理思路是让G1收集器去跟踪各个Region里面的垃圾堆积的 “价值” 大小,价值即回收所得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间(默认200毫秒,使用参数-XX:MaxGCPauseMillis指定),优先处理回收价值受益最大的那些Region。这就是Garbage First名字的由来。

JVM垃圾收集与内存分配

4.5.4 G1运作过程

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

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

五、内存分配与回收策略

Java 的 自动内存管理,解决的无非是自动给对象分配内存以及自动回收分配给对象的内存。《Java虚拟机规范》中并未规定新对象的创建和存储细节,这取决于虚拟机当前使用的是哪一款垃圾收集器,以及虚拟机中与内存相关的参数决定的。

5.1 对象优先在Eden分配

大多数情况下,对象在新生代Eden区分配。当Eden区没有足够的空间时进行分配时,虚拟机将发起一次Minor GC。HotSpot虚拟机可以通过 -XX:+PrintGCDetails 参数告诉虚拟机在发生垃圾手机行为时打印内存回收日志,并且在进程退出的时候输出当前的内存各区域分配情况。

5.2 大对象直接进入老年代

需要大量连续内存空间的Java对象,对虚拟机内存分配来说,是一个不折不扣的坏消息,更坏的消息则是遇到一群“朝生熄灭”的大对象,我们写程序时应注意避免。HotSpot提供了 -XX:PretenureSizeThreshold 参数(部分垃圾收集器支持),指定大于该设置值的对象直接在老年代分配,避免在Eden和两个Survivor区之间来回复制,产生大量的内存复制操作。

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

虚拟机给每个对象定义了一个对象年龄(Age)计数器,存储在对象头中。对象通常在Eden区诞生,如果经过一次Minor GC后仍然存活。并且能够被Survivor容纳的话,该对象就会被移动到Survivor空间中,对象年龄设置成1岁。对象在Survivor区中没熬过一次Minor GC,年龄就增加1岁,当对象的年龄到达一定数值(默认15)时,就会被晋升到老年代中。可以通过 -XX:MaxTenuringThreshold 参数设置该值。

5.4 动态对象年龄判定

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

5.5 空间分配担保

在发生Minor GC之前,虚拟机必须检查老年代最大可用的连续空间是否大于新生代所有对象大小的总和,如果大于,那么这次Minor GC是安全的。如果小于,虚拟机会先查看 -XX:HandlePromotionFailure 参数设置的值是否允许担保失败,如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,则尝试进行一次有风险的 Minor GC,如果小于,则就要进行 Full GC。