引用计数法
对象主要是在堆上分配的,我们可以把它想象成一个池子,对象不停地创建,后台的垃圾回收进程不断地清理不再使用的对象。当内存回收的速度,赶不上对象创建的速度,这个对象池子就会产生溢出,也就是我们常说的 OOM。
**把不再使用的对象及时地从堆空间清理出去,是避免 OOM 有效的方法。**那 JVM 是如何判断哪些对象应该被清理,哪些对象需要被继续使用呢?
先是想了一个办法,叫做**"引用计数法"**。类比日常生活中有人办业务需要来申请仓库,就找个计数器记下次数 1,后续哪个业务用到呢都需要登记一下,继续加 1,每个业务办完计数器就减一。如果一个仓库(对象使用的内存)的计数到降了 0,就说明可以人使用这个仓库了,我们就可以随时在方便的时候去归还/释放这个仓库。(需要注意:一般不是一个仓库到 0 了就立即释放,出于效率考虑,系统总是会等一批仓库一起处理,这样更加高效。)

但是呢,如果业务变得更复杂。仓库之间需要协同工作,有了依赖关系之后。

这时候单纯的引用计数就会出问题,循环依赖的仓库/对象没办法回收,就像数据库的死锁一样让人讨厌,你没法让它自己变成 0。
这种情况在计算机中叫做"内存泄漏",该释放的没释放,该回收的没回收。
如果依赖关系更复杂,计算机的内存资源很可能用满,或者说不够用,内存不够用则称为"内存溢出"。
这样我们知道了引用计数法有一些缺陷,有没有办法解决呢?俗话说办法总比困难多,我找个人专门来排查循环计数行了吧,一个不够就两个......但如果仓库成千上万,或者上亿呢?还是能解决的,最多不就是慢点嘛。
像 Perl、Python 和 PHP 等平台/语言使用的就是引用计数法
标记-清除算法
下面我们一起来看看 JVM 中使用的垃圾收集方法。
这里首先强调一个概念,这对理解垃圾回收的过程非常有帮助,面试时也能很好地展示自己。
垃圾回收(GC) ,并不是找到不再使用的对象,然后将这些对象清除掉。它的过程正好相反,JVM 会找到正在使用的对象,对这些使用的对象进行标记和追溯,然后一股脑地把剩下的对象判定为垃圾,进行清理。这就是JVM 使用的标记---清除算法(Mark and Sweep algorithm)
-
Marking(标记):遍历所有的可达对象,并在本地内存(native)中分门别类记下。
-
Sweeping(清除):这一步保证了,不可达对象所占用的内存,在之后进行内存分配时可以重用。

标记清除算法最重要的优势,就是不再因为循环引用而导致内存泄露
由此,我们还可以引出以下几种别的算法
标记-整理算法
首先找出所有对象,将存活的对象进行标记,然后将存活对象整理到一端,把其他内存区域直接清理掉

复制
将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上,然后再把使用过的内存空间进行一次清理

GC Roots
了解了这个概念,我们就可以看下一些基本的衍生分析:
- GC(Garbage Collection) 的速度,和堆内存活对象的多少有关,与堆内所有对象的数量无关;
- GC 的速度与堆的大小无关,32GB 的堆和 4GB 的堆,只要存活对象是一样的,垃圾回收速度也会差不多;
- 垃圾回收不必每次都把垃圾清理得干干净净,最重要的是不要把正在使用的对象判定为垃圾。
那么,如何找到这些存活对象,也就是哪些对象是正在被使用的,就成了问题的核心。
**我们把这些正在使用的引用的入口,叫作GC Roots。**搜索过程所走过没得路径称为"引用链"
这种使用 tracing 方式寻找存活对象的方法,还有一个好听的名字,叫作可达性分析法。
GC Roots入口大约有三个:线程、静态变量和 JNI 引用。

如果某个对象到GC Roots间没有任何引用链相连,即GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。
除此之外,GC Roots 对象被回收后,就断开了个与待判定对象的连接。如果待判定对象最终与所有 GC ROOTS 都没有了关联,就会被回收。 就像图中灰色的。
Java中的引用
那么,通过 GC Roots 能够追溯到的对象,就一定不会被垃圾回收吗?这要看情况。
首先我们需要知道:Java中虚拟机HotSpot通过直接引用来访问Java对象。直接引用就是说指针是直接指向对象实例的,如果想要获取到对象的类型数据信息,则需要再调用对象里维护的类型数据指针
- 对象实例数据 就是 "这个对象具体长什么样,它有什么值"。每一个通过
new关键字创建出来的对象,都在堆中有一份独一无二的实例数据。- 对象的类型数据信息不是某个具体对象的信息。你可以把它理解为一个对象的"灵魂"或"物种信息"。它主要包括:类的完整有效名(如
java.lang.String)类的直接父类的完整有效名

Java 对象与对象之间的引用,存在着四种不同的引用级别,强度从高 到低依次是:强引用、软引用、弱引用、虚引用。
- 强应用 默认的对象关系是强引用,也就是我们默认的对象创建方式。这种引用属于最普通最强硬的一种存在,只有在和 GC Roots 断绝关系时,才会被消灭掉。(用途:对象的一般状态)
java
Object obj=new Object();
- 软引用 用于维护一些可有可无的对象。在内存足够的时候,软引用对象不会被回收;只有在内存不足时,系统则会回收软引用对象;如果回收了软引用对象之后,仍然没有足够的内存,才会抛出内存溢出异常。(用途:对象缓存)
java
public class Main {
public static void main(String[] args) throws InterruptedException {
//强
Obj obj = new Obj();
//软
SoftReference<Obj> sr = new SoftReference<Obj>(obj);
System.out.println(sr.get().getObjName());
}
}
class Obj {
String objName;
public Obj() {
objName = "test";
}
public String getObjName() {
return objName;
}
}
- 弱引用 级别就更低一些,当 JVM 进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。软引用和弱引用在堆内缓存系统中使用非常频繁,可以在内存紧张时优先被回收掉。(用途:对象缓存)
java
public class Main {
public static void main(String[] args) throws InterruptedException {
WeakReference<String> sr = new WeakReference<>(new String("hello"));
System.out.println(sr.get());
System.gc(); // 通知JVM的gc进行垃圾回收
System.out.println(sr.get());
}
}
// 输出
// hello
// null
- 虚引用 是一种形同虚设的引用,在现实场景中用得不是很多。这里有一个冷门的知识点:Java 9.0 以后新加入了 Cleaner 类,用来替代 Object 类的 finalizer 方法,这就是虚引用的一种应用场景。
分代垃圾回收
上面我们提到,垃圾回收的速度,是和存活的对象数量有关系的,如果这些对象太多,JVM 再做标记和追溯的时候,就会很慢。
一般情况下,JVM 在做这些事情的时候,都会停止业务线程的所有工作,进入 SafePoint 状态,这也就是我们通常说的 Stop the World。所以,现在的垃圾回收器,有一个主要目标,就是减少 STW 的时间。
其中一种有效的方式,就是采用分代垃圾回收,减少单次回收区域的大小。这是因为,大部分对象,可以分为两类:
- 大部分对象的生命周期都很短
- 其他对象则很可能会存活很长时间
这个假设我们称之为弱代假设(weak generational hypothesis)。
分代垃圾回收器会在逻辑上,把堆空间分为两部分:年轻代(Young generation)和老年代(Old generation)。

1.年轻代
年轻代中又分为一个新生代(Eden),两个幸存者空间(Survivor)。对象会首先在年轻代中的 Eden 区进行分配,通常会有多个线程同时创建多个对象,所以 Eden 区被划分为多个 线程本地分配缓冲区 (Thread Local Allocation Buffer,简称 TLAB)。通过这种缓冲区划分,大部分对象直接由 JVM 在对应线程的 TLAB 中分配,避免与其他线程的同步操作。如果 TLAB 中没有足够的内存空间,就会在共享 Eden 区(shared Eden space)之中分配。当 Eden 区分配满的时候,就会触发年轻代的 GC。
此时,存活的对象会被移动到其中一个 Survivor 分区(以下简称S0);年轻代再次发生垃圾回收,存活对象,包括S0区中的存活对象,会被移动到 S1 区。所以,S0和 S1两个区域,总有一个是空的。后面S0S1来回复制,直到对象存活次数达到晋升老年代的条件,就从survivor中移出,进入老年代
2.老年代
对垃圾回收的优化,就是要让对象尽快在年轻代就回收掉,减少到老年代的对象。那么对象是如何进入老年代的呢?它主要有以下四种方式。
- 正常提升(Promotion)
上面提到了年轻代的垃圾回收,如果对象能够熬过年轻代垃圾回收,它的年龄(age)就会加一,当对象的年龄达到一定阈值,就会被移动到老年代中。
- 分配担保
如果年轻代的空间不足,又有新的对象需要分配空间,就需要依赖其他内存(这里是老年代)进行分配担保,对象将直接在老年代创建。
- 大对象直接在老年代分配
超出某个阈值大小的对象,将直接在老年代分配,可以通过 -XX:PretenureSizeThreshold 配置这个阈值。
- 动态对象年龄判定
有的垃圾回收算法,并不要求 age 必须达到 15(现代 JVM 中这个阈值默认设置为 15 个 GC 周期。这也是 HotSpot JVM 中允许的最大值) 才能晋升到老年代,它会使用一些动态的计算方法。比如 G1,通过 TargetSurvivorRatio 这个参数,动态更改对象提升的阈值。
老年代的空间一般比较大,回收的时间更长,当老年代的空间被占满了,将发生老年代垃圾回收。
目前,被广泛使用的是 G1 垃圾回收器。G1 的目标是用来干掉 CMS 的,它同样有年轻代和老年代的概念。不过,G1 把整个堆切成了很多份,把每一份当作一个小目标,部分上目标很容易达成。

如上图,G1 也是有 Eden 区和 Survivor 区的概念的,只不过它们在内存上不是连续的,而是由一小份一小份组成的。G1 在进行垃圾回收的时候,将会根据最大停顿时间(MaxGCPauseMillis)设置的值,动态地选取部分小堆区进行垃圾回收。
G1 的配置非常简单,我们只需要配置三个参数,一般就可以获取优异的性能:
① MaxGCPauseMillis 设置最大停顿的预定目标,G1 垃圾回收器会自动调整,选取特定的小堆区;
② G1HeapRegionSize 设置小堆区的大小;
③ InitiatingHeapOccupancyPercent 当整个堆内存使用达到一定比例(默认是45%),并发标记阶段就会被启动。
上面我们讲了GC Roots, Java中的引用以及常见的垃圾回收算法(标记-清除算法,标记-整理算法,复制,分代收集),那我们接下来就来介绍垃圾收集器
垃圾收集器
HotSpot虚拟机中的七个垃圾收集器,图中有连线代表可以一起搭配使用

在学习各种垃圾收集器之前,需要清除大多数 JVM 都需要使用两种不同的 GC 算法------一种用来清理年轻代,另一种用来清理老年代。
串行GC(Serial收集器)

串行 GC 对年轻代使用 复制 算法。
单线程的垃圾收集器,不能进行并行处理,所以都会触发全线暂停(STW),停止所有的应用线程。
因此这种 GC 算法不能充分利用多核 CPU。
对于服务器端来说,因为一般是多个 CPU 内核,并不推荐使用,除非确实需要限制 JVM 所使用的资源。大多数服务器端应用部署在多核平台上,选择 串行 GC 就意味着人为地限制了系统资源的使用,会导致资源闲置,多余的 CPU 资源也不能用增加业务处理的吞吐量。
并行GC(ParNew收集器)
ParNew和串行GC在算法上相似,不一样的点在于并行GC的年轻代在执行"复制"阶段时都使用多个线程。通过并行执行,使得 GC 时间大幅减少。

总结:
- 垃圾收集时多线程并行
- ParNew是新生代的垃圾收集器
- 算法:复制算法
- 是 Server 模式下的虚拟机首选新生代收集器,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器(老年代)配合工作。
Parallel Scavenge收集器
该收集器与ParNew类似,都是多线程的垃圾收集器。其它收集器关注点可能是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,它被称为"吞吐量优先"收集器。这里的吞吐量指 CPU 用于运行用户代码的时间占总时间的比值。
吞吐量 =运行用户代码的时间/(运行用户代码的时间+运行垃圾收集的时间)
并行垃圾收集器适用于多核服务器,主要目标是增加吞吐量。因为对系统资源的有效使用,能达到更高的吞吐量:
- 在 GC 期间,所有 CPU 内核都在并行清理垃圾,所以总暂停时间更短;
- 在两次 GC 周期的间隔期,没有 GC 线程在运行,不会消耗任何系统资源。
另一方面,因为此 GC 的所有阶段都不能中断,所以并行 GC 很容易出现长时间的卡顿(注:这里说的长时间也很短,一般来说例如 minor GC 是毫秒级别,full GC 是几十几百毫秒级别)。如果系统的主要目标是最低的停顿时间/延迟,而不是整体的吞吐量最大,那么就应该选择其他垃圾收集器组合。
注:长时间卡顿的意思是,此 GC 启动之后,属于一次性完成所有操作,于是单次 暂停 的时间会较长。
总结:
- 吞吐量优先收集器
- 新生代垃圾收集器
- 算法:复制算法
- GC 自适应的调节策略开关:开启开关,就不需要手动指定新生代的大小(Xmn)、Eden 和 Survivor 区的比例、晋升老年代对象年龄等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。(此为与ParNew收集器最重要的一个区别)
Serial Old收集器
是 Serial 收集器的老年代版本,使用标记-整理算法,主要意义也是供客户端模式下的HotSpot虚拟机使用

总结:
- 老年代收集器
- 算法:标记-整理算法
- gc时暂停所有用户线程
- 主要作为客户端模式下的HotSpot虚拟机使用,另外也作为CMS收集器并发收集发生Concurrent Mode Failure时的后备预案使用。
Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,多线程并行收集。目前只能与新生代的Parallel Scavenge收集器搭配使用,可以说Parallel Old就是为Parallel Scavenge而生的。在这之前Parallel Scavenge收集器只能与老年代的Serial Old进行搭配,但是一个多线程,一个单线程,导致吞吐是并没有充分的提升,直到Parallel Old收集器出现。

总结:
- Parallel Old为Parallel Scavenge而生,只能搭配Parallel Scavenge.
- Parallel Old采用多线程
- 算法:标记-整理
- 在注重吞吐量以及处理器资源较为稀缺的场合,都可以优先考虑ParallelScavenge加Parallel Old收集器这个组合
CMS垃圾收集器

CMS垃圾收集器对年轻代采用并发复制算法,对老年代主要使用并发标记-清除算法。
CMS GC 的设计目标是避免在老年代垃圾收集时出现长时间的卡顿,主要通过两种手段来达成此目标:
- 第一,不对老年代进行整理,而是使用空闲列表(free-lists)来管理内存空间的回收。
- 第二,在 mark-and-sweep(标记---清除)阶段的大部分工作和应用线程一起并发执行。
也就是说,在这些阶段并没有明显的应用线程暂停。但值得注意的是,它仍然和应用线程争抢 CPU 时间。默认情况下,CMS 使用的并发线程数等于 CPU 核心数的 1/4。
如果服务器是多核 CPU,并且主要调优目标是降低 GC 停顿导致的系统延迟,那么使用 CMS 是个很明智的选择。通过减少每一次 GC 停顿的时间,很多时候会直接改善系统的用户体验。因为多数时候都有部分 CPU 资源被 GC 消耗,所以在 CPU 资源受限的情况下,CMS GC 会比并行 GC 的吞吐量差一些(对于绝大部分系统,这个吞吐和延迟的差别应该都不明显)。
下面我们来看一看 CMS GC 的几个阶段。
阶段 1:Initial Mark(初始标记)
这个阶段伴随着 STW 暂停。初始标记的目标是标记所有的根对象,包括根对象直接引用的对象,以及被年轻代中所有存活对象所引用的对象
初始标记阶段,为了标记老年代,需要处理年轻代对老年代的引用(通过卡表),但它本身不回收年轻代,所以说它'不管'年轻代的回收。

阶段 2:Concurrent Mark(并发标记)
在此阶段,CMS GC 遍历老年代,标记所有的存活对象,从前一阶段"Initial Mark"找到的根对象开始算起。"并发标记"阶段,就是与应用程序同时运行,不用暂停的阶段。请注意,并非所有老年代中存活的对象都在此阶段被标记,因为在标记过程中对象的引用关系还在发生变化。

阶段 3:Concurrent Preclean(并发预清理)
此阶段同样是与应用线程并发执行的,不需要停止应用线程。
因为前一阶段"并发标记"与程序并发运行,可能有一些引用关系已经发生了改变。如果在并发标记过程中引用关系发生了变化,JVM 会通过"Card(卡片)"的方式将发生了改变的区域标记为"脏"区,这就是所谓的"卡片标记(Card Marking)"。

在预清理阶段,这些脏对象会被统计出来,它们所引用的对象也会被标记。此阶段完成后,用以标记的 card 也就会被清空。

阶段 4:Concurrent Abortable Preclean(可取消的并发预清理)
此阶段也不停止应用线程。本阶段尝试在 STW 的 Final Remark 阶段 之前尽可能地多做一些工作。本阶段的具体时间取决于多种因素,因为它循环做同样的事情,直到满足某个退出条件(如迭代次数,有用工作量,消耗的系统时间等等)。
此阶段可能显著影响 STW 停顿的持续时间,并且有许多重要的配置选项和失败模式。
阶段 5:Final Remark(最终标记)
最终标记阶段是此次 GC 事件中的第二次(也是最后一次)STW 停顿。
本阶段的目标是完成老年代中所有存活对象的标记. 因为之前的预清理阶段是并发执行的,有可能 GC 线程跟不上应用程序的修改速度。所以需要一次 STW 暂停来处理各种复杂的情况。
通常 CMS 会尝试在年轻代尽可能空的情况下执行 Final Remark 阶段,以免连续触发多次 STW 事件。
在 5 个标记阶段完成之后,老年代中所有的存活对象都被标记了,然后 GC 将清除所有不使用的对象来回收老年代空间。
阶段 6:Concurrent Sweep(并发清除)
此阶段与应用程序并发执行,不需要 STW 停顿。JVM 在此阶段删除不再使用的对象,并回收它们占用的内存空间。

总之,CMS 垃圾收集器在减少停顿时间上做了很多复杂而有用的工作,用于垃圾回收的并发线程执行的同时,并不需要暂停应用线程。当然,CMS 也有一些缺点,其中最大的问题就是老年代内存碎片问题(因为不压缩),在某些情况下 GC 会造成不可预测的暂停时间,特别是堆内存较大的情况下。
优点: 并发收集、低停顿。
问题总结:
- 吞吐量低:CMS追求用户线程停顿时间少,停顿时间少就只能与用户线程并发执行部分阶段,导致整个垃圾回收需要执行的整体时间会更长(停顿之后专心垃圾收集肯定是最快的),所以吞吐是会降低
- "浮动垃圾"问题:"并发清除"阶段,由于gc线程是与用户线程并发的,这个期间用户还会产生新的垃圾,所以一般会预留出一部分内存,不能等到老年代快满的时候才去收集,如果预留的内存不足以存放这部分浮动垃圾的话就会出现Concurrent Mode Failure。 前面讲过,出现这个错误之后,虚拟机将临时启用 Serial Old 来替代 CMS
- 标记-清除算法:因为没有整理的过程,所以垃圾收集完之后,会有很多空间碎片,导致需要分配大块连续内存的时候,空间不足
G1垃圾收集器
G1 的全称是 Garbage-First,意为垃圾优先,哪一块的垃圾最多就优先清理它。
G1 GC 最主要的设计目标是:将 STW 停顿的时间和分布,变成可预期且可配置的。
事实上,G1 GC 是一款软实时垃圾收集器,可以为其设置某项特定的性能指标。例如可以指定:在任意 xx 毫秒时间范围内,STW 停顿不得超过 yy 毫秒。举例说明:任意 1 秒内暂停时间不超过 5 毫秒。G1 GC 会尽力达成这个目标(有很大概率会满足,但并不完全确定)。
G1 GC 的特点
为了达成可预期停顿时间的指标,G1 GC 有一些独特的实现。
首先,堆不再分成年轻代和老年代,而是划分为多个(通常是 2048 个)可以存放对象的 小块堆区域(smaller heap regions)。每个小块,可能一会被定义成 Eden 区,一会被指定为 Survivor 区或者 Old 区。所以Region成为了垃圾收集器的最小单位,每一次回收都会是Region的整数倍大小。在逻辑上,所有的 Eden 区和 Survivor 区合起来就是年轻代,所有的 Old 区拼在一起那就是老年代,如下图所示:

这样划分之后,使得 G1 不必每次都去收集整个堆空间,而是以增量的方式来进行处理:每次只处理一部分内存块,称为此次 GC 的回收集(collection set)。每次 GC 暂停都会收集所有年轻代的内存块,但一般只包含部分老年代的内存块,见下图带对号的部分:

G1 的另一项创新是,在并发阶段估算每个小堆块存活对象的总数。构建回收集的原则是:垃圾最多的小块会被优先收集。这也是 G1 名称的由来。
Region里面存在的跨Region引l用对象如何解决? 使用记忆集避免全堆作为GC Roots扫描,G1它的每个Region都维护有自己的记忆集这些记忆集会记录下别的Region 指向自己的指针,并标记这些指针分别在哪些卡页的范围之内。
那么在并发标记阶段如何保证收集线程与用户线程互不干扰地运行?
- 回收过程中改变对象引用关系: 必须保证其不能打破原本的对象图结构,导致标记结果出现错误。G1收集器则是通过原始快照(SATB)算法来实现的。
- 回收过程中新创建对象: G1为每一个Region设计了两个名为TAMS(Topat Mark Start)的指针,把Region中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置以上。

G1收集器运作的四个步骤:
- 初始标记:仅仅只是标记一下GC Roots能直接关联到的对象(需要停顿)
- 并发标记:从GC Roots开始进行可达性分析,完成对象图的扫描,判断存活对象和可回收对象。稍后再处理下SATB记录的有引用变动的对象(无需停顿)
- 最终标记:对用户线程做另一个短暂的停顿,用于处理并发阶段结束后仍然遗留下来的最后那少量的SATB记录;该阶段处理的,本身就是因为第二阶段和用户线程在并发,用户线程产生的新垃圾(需要停顿)
- 筛选回收:统计各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,筛选任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间(需要停顿)

介绍完垃圾回收算法和垃圾收集器后,我们接下来可以来看内存分配与回收策略的介绍
内存分配和回收策略
- 对象优先在Eden分配:大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次MinorGC(新生代GC)
- 大对象直接进入老年代:大对象就是指需要大量连续内存空间的]ava对象2.最典型的大对象便是那种很长的字符串,或者元素数量很庞大的数组,大对象会直接进入老年代,可以设想一下,如果大对象被分配在新生代,又因为新生代多采用复制算法,所以如果一个大对象能存活很久的话,那么复制开销将会是非常大的。
- 长期存活的对象将进入老年代:对象头里面存储了对象的分代年龄,新生带的对象每经历一次Minor GC 年龄就会增加一岁,当年龄达到一定程度(默认15,-XX:MaxTenuringThreshold可配),就会晋升为老年代。
- 动态对象年龄判定:并不是永远要求对象的年龄必须达到-XX:MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于 Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到-XX:MaxTenuringThreshold中要求的年龄。
- 空间分配担保:上面可以看出,Minor GC有可能会导致一大批对象从新生代进入老年代,那老年代如果放不下怎么办?
- 每次Minor GC之前都得检查老年代的空间是否能容纳所有新时代对象,如果可以那就安全;如果不可以,则虚拟机会先去查看-XX:Handle Promotion Failure参数的设置值是否允许担保失败;如果允许,(那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小;如果大于,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,就进行FuIl GC)。如果不允许,那这时就要改为进行一次FuII GC.