文章目录
对象已死?
引用计数算法
给对象中添加一个引用计数器,每当有一个地方引用它,计数器值就加 1;当引用失效时,计数器值就减 1;任何时刻计数器为 0 的对象就是不可能再被使用的。
引用计数算法实现原理简单,判定效率高,但目前主流的虚拟机中都没有选择这个算法来管理内存,其主要原因是有很多例外情况要考虑,比如它很难解决对象之间相互循环引用的问题。所谓对象之间的相互引用问题,即对象 objA 和 objB 都有字段 instance ,且令 objA.instance = objB 及 objB.instance = objA ,除此之外,这两个对象之间再无任何引用。然后将 objA 与 objB 都置为 null,实际上着两个对象已经不可能再被访问,但是他们因为互相引用对方,导致它们的引用计数器都不为 0,于是引用计数算法无法通知 GC 回收器回收他们。
可达性分析算法
这个算法的基本思路就是通过一系列称为 “GC Roots” 的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为 “引用链” ,如果某个对象到 GC Roots 间没有任何引用链相连,或者用图论的话来说就是从 GC Roots 到这个对象不可达时,则证明此对象是不可能再被使用的。
可作为 GC Roots 的对象包括以下几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如当前正在运行方法所使用到的参数、局部变量等。
- 方法区中类静态属性引用的对象,譬如 Java 类的引用类型静态变量。
- 方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
- 本地方法栈中 JNI(即Native 方法)引用的对象
- 所有被同步锁(synchronized关键字)持有的对象
再谈引用
无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否引用链可达,判定对象是否存活都和 “引用” 有关。
引用分为强引用、软引用、弱引用、虚引用 4 种,引用强度依次减弱。
强引用:
指在代码之中普遍存在的引用赋值,即使用 new
对象创建强引用。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会对被引用的对象进行回收。
Object obj = new Object();
软引用:
用来描述一些还有用但非必须的对象,即只有在内存不够的情况下才会被回收。可使用 SoftReference
类来创建软引用。
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<>(obj);
obj = null; // 使对象只被软引用关联
弱引用:
也是用来描述那些非必须对象,但被弱引用关联的对象不管内存是否足够都一定会被回收,也就是说它只能存活到下一次垃圾回收发生为止,比起软引用,只具有弱引用的对象拥有更短暂的生命周期。可以使用 WeakReference
类来创建弱引用。
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<>(obj);
obj = null;
软引用和弱引用都可以和一个引用队列(ReferenceQueue)联合使用,如果软/弱引用所引用的对象被垃圾回收,JAVA 虚拟机就会把这个软/弱引用加入到与之关联的引用队列中。
虚引用必须和引用队列(ReferenceQueue)联合使用
虚引用:
又称为“幽灵引用”或者“幻影引用”,一个对象是否有虚引用的存在,完全不会对其生存时间造成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用的唯一目的是能在这个对象被回收时收到一个系统通知。可以使用 PhantomReference
来创建虚引用。
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<>(obj, null);
obj = null;
在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。
回收方法区
《 Java虚拟机规范 》中并没有强制要求虚拟机必须实现方法区(HotSpot中的元空间或永久代)的垃圾收集,而且方法区中进行垃圾收集的“性价比”比较低。
方法区的垃圾收集主要回收两部分内容:废弃常量和不再使用的类(即无用的类)。回收废弃常量与回收 Java 堆中的对象非常类似。如果没有任何地方引用这些常量便会被系统清理。
判断一个常量是否 “废弃” 相对简单,而要判定一个类是否属于 “不再被使用的类” 的条件就比较苛刻。需要同时满足下面三个条件:
- 该类的所有实例都已经被回收,也就是 Java 堆中不存在该类及派生子类的任何实例。
- 加载该类的类加载器已经被回收了。
- 该类对应的 java.lang.Class 对象没有在被任何地方被引用,无法在任何地方通过反射访问该类的方法。
垃圾收集算法
标记 - 清除算法
首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象(也可以反过来)。标记过程就是对象是否属于垃圾的判定过程(即引用计数与可达性分析算法)。
缺点:
- 执行效率不稳定:如果 Java 堆中包含大量可回收对象,这时必须进行大量标记和清除动作,导致标记和清除两个过程的效率都不高。同时需要扫描两次。
- 空间问题:标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后再程序运行过程中需要分配较大的对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
通过可达性分析算法从Root根节点出发递归遍历可达对象,进行标记。然后一一遍历内存,将可回收的对象进行清除。
标记 - 复制算法
为了解决标记 - 清除算法面对大量可回收对象时执行效率低问题而诞生。将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑空间碎片的复杂情况,只要移动堆顶指针,按顺序分配内存即可,且只需要扫描一次,实现简单,运行高效。
缺点是:
- 将可用内存缩小为了原来的一半,空间浪费未免太多了一点。
- 移动时,需要复制对象,并调整对象引用。
为什么说执行效率更高?
从Root根节点递归遍历可达对象时,遍历到时不进行标记,直接复制到另一块内存上。而且也不需要一一遍历进行清除,直接整个清理掉就行。
现在的商业虚拟机都采用这种收集算法来回收新生代,IBM公司的专门研究表明,新生代中的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分新生代的内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor。
发生垃圾收集时,将Eden和Survivor中仍然存活的对象一次性地复制到另外一块Survivor空间上,然后直接清理掉Eden和刚才用过的那块Survivor空间。
HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(Eden的80% + 一个Survivor的10%),只有10%的新生代内存会被“浪费”。
当然,98%的对象可被回收仅仅是普通场景下测得的数据,任何人都没办法保证每次回收都只有不多于10%的对象存活,因此当Survivor空间不足以容纳依次Minor GC之后存活的对象时,就需要依赖其他内存(指老年代)进行分配担保(Handle Promotion)。
标记 - 整理算法
复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。
针对老年代对象的存亡特征,提出了标记 - 整理算法。标记过程仍然与 “标记 - 清除” 算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。
-
优点:不会产生内存碎片
-
不足:需要扫描两次;需要移动大量对象,处理效率比较低。
标记 - 清除算法内存不连续,只能使用空闲列表来为对象分配内存;复制和整理算法都可用使用指针碰撞。
分代收集理论
弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。
强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。
跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极少数。
分代收集算法
当前虚拟机的垃圾收集都采用分代收集算法,这种算法只是根据对象存活周期的不同将内存分为几块。一般将 Java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
比如在新生代中,每次收集都会有大量对象死去,所以可以选择 ”标记-复制“ 算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择 “标记-清除” 或 “标记-整理” 算法进行垃圾收集。
GC目标
-
部分收集( Partial GC ):指目标不是完整收集整个Java堆的垃圾收集,其中分为:
- 新生代收集( Minor GC / Young GC ):指目标只是新生代的垃圾收集。因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。
- 老年代收集( Major GC / Old GC ):指目标只是老年代的垃圾收集(目前只有CMS收集器有单独收集老年代的行为)。
- 混合收集( Mixed GC ):指目标是收集整个新生代以及部分老年代的垃圾收集(目前只有C1收集器)。
-
整堆收集( Full GC ):收集整个 Java 堆和方法区的垃圾收集。回收老年代和新生代,老年代对象其存活时间长,因此 Full GC 很少执行,执行速度会比 Minor GC 慢很多。
堆的内存细分
Java 的自动内存管理主要是针对对象内存的回收和对象内存的分配。
垃圾收集主要是针对堆和方法区进行。程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后就会消失,因此不需要对这三个区域进行垃圾回收。
Java 堆是垃圾收集器管理的主要区域,因此也被称作GC 堆(Garbage Collected Heap)。从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代:再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。
- 堆空间的基本结构:
其中Eden 区、From Survivor0(“From”) 区、To Survivor1(“To”) 区都属于新生代,Old Memory 区属于老年代
大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后**(当Eden区满时触发YGC/Minor GC),如果对象还存活,则会进入 s0 或者 s1,即将Eden区以及From区还存活的对象转移至To,并且对象的年龄还会加 1 (Eden 区转移至Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为大于 15 岁),就会被晋升到老年代中**。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold
来设置默认值,这个值会在虚拟机运行过程中进行调整,可以通过-XX:+PrintTenuringDistribution
来打印出当次GC后的Threshold。
经过这次 GC 后,Eden 区和"From"区已经被清空。这个时候,“From"和"To"会交换他们的角色,也就是新的"To"就是上次 GC 前的“From”,新的"From"就是上次 GC 前的"To”。不管怎样,都会保证名为 To 的 Survivor 区域是空的。Minor GC 会一直重复这样的过程,在这个过程中,有可能当次Minor GC后,Survivor 的"From"区域空间不够用,有一些还达不到进入老年代条件的实例放不下,则放不下的部分会提前进入老年代。
垃圾收集器
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
以上是 HotSpot 虚拟机中的 7 种垃圾收集器,连线表示垃圾收集器之间可以搭配使用。
在谈论垃圾收集器的上下文语境中,可用理解为:
- 并行(Parallel):并行描述的是多个垃圾收集器线程之间的关系,说明同一时间有多个这样的线程在协同工作,通常默认此时用户线程是处于等待状态。
- 并发(Concurrent):并发描述的是垃圾收集器线程与用户线程之间的关系,说明同一时间垃圾收集器线程与用户线程都在运行。
Serial 收集器
Serial(串行)收集器是一个单线程工作的收集器,它的 “单线程” 的意义不仅仅说明它只会使用一个处理器或一条垃圾收集线程去完成垃圾收集工作,更重要的是强调在它进行垃圾收集时,必须暂停其他所有的工作线程( “Stop The World” ),直到它收集结束。是虚拟机运行在客户端模式下的默认新生代收集器。
- 作用域:新生代
- 垃圾收集算法:复制算法
- 线程数:单线程
- 特点:进行垃圾收集时,由于是单线程,需要Stop The World,即将其他所有用户线程停止,直到收集结束。但简单高效(避免线程切换的开销),内存消耗小。
ParNew 收集器
ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。
- 作用域:新生代
- 垃圾收集算法:复制算法
- 线程数:多线程
- 特点:多线程,在多核CPU情况下垃圾收集效率较高,Stop The World的时间较短。
Parallel Scavenge 收集器
Parallel Scavenge 收集器也是使用标记-复制算法的多线程收集器,它看上去几乎和 ParNew 都一样。 那么它有什么特别之处呢?
CMS 等垃圾收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而 Parallel Scavenge 收集器的目标是达到一个可控制的吞吐量。所谓吞吐量就是 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值。停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验;而高吞吐量则可以高效率地利用 CPU 资源,尽快完成程序的运算任务,适合在后台运算而不需要太多交互的分析任务。
垃圾收集停顿时间缩短是以牺牲吞吐量和新生代空间为代价换取的:新生代空间变小,垃圾回收变得频繁,停顿事件下降,但也导致吞吐量下降。
- 作用域:新生代
- 垃圾收集算法:复制算法
- 线程数:多线程
- 特点:目标是提高吞吐量,能够在较短的时间内完成指定任务,适合不需太多交互的后台运算
Parallel Scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量:
-XX:MaxGCPauseMillis
用于控制最大垃圾收集停顿时间
-XX:GCTimeRatio
用于直接设置吞吐量的大小
如果对于收集器运作不太了解,手工优化存在困难的时候,可以通过一个开关参数打开 GC 自适应的调节策略(GC Ergonomics),就不需要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例、晋升老年代对象年龄等细节参数了。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。
JDK1.8 默认使用的是 Parallel Scavenge + Parallel Old,如果指定了-XX:+UseParallelGC 参数,则默认指定了-XX:+UseParallelOldGC,可以使用-XX:-UseParallelOldGC 来禁用该功能
Serial Old 收集器
Serial 收集器的老年代版本,它同样是一个单线程收集器。
- 作用域:老年代
- 垃圾收集算法:标记 - 整理算法
- 线程数:单线程
- 特点:与 Serial 收集器相同
它主要有两大用途:
- 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用。
- 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。
Parallel Old 收集器
Parallel Old 收集器是 Parallel Scavenge 收集器的老年代版本,支持多线程并行收集,基于标记 - 整理算法。
- 作用域:老年代
- 垃圾收集算法:标记 - 整理算法
- 线程数:多线程
- 特点:目标是提高吞吐量
CMS 收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户交互体验的应用上使用。CMS 收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
- 作用域:老年代
- 垃圾收集算法:标记 - 清除算法
- 线程数:并发线程
- 特点:以缩短停顿时间为目标
从名字中的Mark Sweep可以看出,CMS 收集器是基于 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:
- 初始标记: 仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快。
- 并发标记:并发标记阶段从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。
- 重新标记: 重新标记阶段是为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。这个阶段的停顿时间一般会比初始标记阶段的时间稍长,但也远比并发标记阶段的时间短。
- 并发清除: 清理删除掉标记阶段判断的已经死亡的对象,对未标记的区域做清扫。由于不需要移动存活对象,所以这个阶段也是可以与用户线程并发执行的。
其中初始标记、重新标记这两个步骤仍然需要 “ Stop The World ”,但速度很快。由于在整个过程中耗时最长的并发标记和并发清除阶段中,垃圾收集器线程都可以与用户线程一起工作,所以从总体上 CMS 收集器的内存回收过程是与用户线程一起并发执行的。
G1 收集器
G1 (Garbage-First) 是一款面向服务端应用的垃圾收集器,主要针对配备多核CPU及大容量内存的机器,以极高概率满足 GC 停顿时间要求的同时,还兼具高吞吐量的性能特征。被视为 JDK1.7 中 HotSpot 虚拟机的一个重要进化特征,在 JDK 9 时正式取代 Parallel Scavenge + Parallel Old 组合成为默认垃圾收集器。
- 作用域:新生代与老年代
- 垃圾收集算法:标记 - 复制算法(局部上看)、标记 - 整理算法(整体上看)
- 线程数:并发线程
- 特点:以缩短停顿时间为目标,同时兼顾高吞吐量
G1出现之前的垃圾收集器,包括CMS在内,垃圾收集的目标范围要么是整个新生代(Minor GC),要么就是整个老年代(Major GC),再要么就是整个 Java 堆(Full GC)。而 G1 跳出了这个框架,它可以面向堆内存的任何部分来组成回收集(Collection Set,CSet),衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是 G1 收集器的 Mixed GC 模式。
G1 是基于 Region 的堆内存布局来进行回收的,G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的 Java 堆划分为多个大小相等的独立区域 Region,每一个 Region 都可以根据需要,扮演新生代的 Eden 空间、Survivor 空间或者老年代空间,收集器能够对不同角色的 Region 采用不同的策略去处理。
Region 中还有一类特殊的 Humongous 区域,专门用来存储大对象。G1 认为只要大小超过了一个 Region 容量一半的对象即可判定为大对象。对于超过了整个 Region 容量的超级大对象,将会存放在 N 个连续的 Humongous Region 中,G1 大多数行为都会把 Humongous Region 作为老年代的一部分来看待。
运行过程
G1收集器的运作过程大致可划分为以下四个步骤:
- 初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,并修改 TAMS 指针的值,让下一阶段用户线程并发运行时,能够在可用的 Region 中分配对象。这个阶段需要暂停用户线程,但是时间很短。而且这个停顿是借用 Minor GC 的时候同步完成的,所以在这个阶段实际没有额外的停顿。
- 并发标记:从 GC Root 开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象。当对象图扫描完成后,还要重新处理 SATB 记录下的在并发时有引用变动的对象。
- 最终标记:对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的 SATB 记录(原始快照,用来记录并发标记中某些对象)。
- 筛选回收:负责更新 Region 的统计数据,对各个 Region 的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个 Region 构成回收集,然后把决定要回收的那一部分 Region 的存活对象复制到空的 Region 中,再清理掉整个旧 Region 的全部空间。这里的操作涉及存活对象的移动,所以必须要暂停用户线程,由多条收集器线程并行完成。
内存分配策略
1、对象优先在 Eden 分配
大多数情况下,对象在新生代 Eden 区分配,当 Eden 区没有足够空间进行分配时,虚拟机发起一次 Minor GC。
2、大对象直接进入老年代
大对象是指需要大量连续内存空间的对象(最典型的大对象是那种很长的字符串以及元素数量大的数组)。
3、长期存活的对象将进入老年代
虚拟机给每个对象定义了一个对象年龄计数器,存储在对象头中。如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1 岁。对象在 Survivor 区中每熬过一次 MinorGC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 ),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold
设置。
4、动态对象年龄判定
虚拟机并不是永远要求对象的年龄必须达到 -XX:MaxTenuringThreshold
才能晋升老年代,如果在 Survivor 空间中低于或等于某年龄的所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到 -XX:MaxTenuringThreshold
中要求的年龄。
- 动态年龄计算策略:
Hotspot 遍历所有对象时,按照相同年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了 survivor 区的一半时,取这个年龄和 MaxTenuringThreshold 中更小的一个值,作为新的晋升年龄阈值。
- 动态年龄计算的代码如下:
uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) {
//survivor_capacity是survivor空间的大小
size_t desired_survivor_size = (size_t)((((double) survivor_capacity)*TargetSurvivorRatio)/100);
size_t total = 0;
uint age = 1;
while (age < table_size) {
total += sizes[age];//sizes数组是每个年龄段对象大小
if (total > desired_survivor_size) break;
age++;
}
uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold;
...
}
5、空间分配担保
在发生 Minor GC 之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那这一次 Minor GC 可以确保是安全的。
为什么说这次 Minor GC 是确保安全的呢?因为我们知道新生代使用复制收集算法,且只使用其中一个 Survivor 空间作为轮换备份,因此当出现大量对象在 Minor GC 后仍然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),可能出现 Survivor 空间不够存下这些对象,需要老年代进行分配担保,把 Survivor 无法容纳的对象直接送入老年代。而老年代担保的前提是本身还有容纳这些对象的剩余空间,但有多少对象会再这次回收中活下来在实际完成内存回收之前是无法明确知道的,只能取之前的平均大小作为经验值。
即空间分配担保是为了确保在 Minor GC 之前老年代本身还有容纳新生代所有对象的剩余空间。
如果不成立的话,虚拟机会先查看 HandlePromotionFailure 参数的值是否允许担保失败。如果允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC,尽管这次 Minor GC 是有风险的;如果小于,或者 HandlePromotionFailure 设置不允许冒险,那么就要改为进行一次 Full GC。
在 JDK 6 Update 24 之后,-XX:HandlePromotionFailure 参数不会再影响到虚拟机的空间分配担保策略,实际虚拟机中已经不会再使用它。JDK 6 Update 24 之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小,就会进行 Minor GC,否则将进行 Full GC。
参考资料
《深入理解Java虚拟机 第3版》——周志明