深入理解Java虚拟机——第三章——垃圾收集器与内存分配策略
概述
GC需要完成的3件事情:
- 哪些内存需要回收?
- 什么时候回收?
- 如何回收?
程序计数器、虚拟机栈、本地方法栈3个区域生命周期与线程相同。栈中的栈帧随着方法的进入和退出对应着入栈和出栈的操作,每一栈帧中分配多少内存基本上是在类结构确定下来时就已知(忽略运行时JIT编译器的优化)。因此这几个区域的内存分配和回收都具备确定性,不过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟着回收了。
Java堆和方法区则不一样,一个接口的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样(方法分支?)。只有在程序处于运行期间才能知道会创建哪些对象,这部分的内存分配和回收都是动态的,垃圾收集器所关注的是这部分内存,后序讨论的内存分配与回收也仅针对这一部分内存。
对象存活判定算法
堆里存放着几乎所有对象实例(这里应该只的是Java堆,不包括方法区,因为方法区是Non-heap),垃圾收集器进行回收前就是要确定这些对象之中哪些还“存活”,哪些已经“死去”(即不可能再被任何途径使用的对象)。
引用计数算法
引用计数算法:给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时候计数器值为0的对象就是不可能再被使用的。
引用计数算法实现简单,判定效率也很高,但很难解决对象间互相循环引用的问题。如
public class ReferenceCountingGC { public Object instance = null; public static void main(String[] args) { ReferenceCountingGC objA = new ReferenceCountingGC(); ReferenceCountingGC objB = new ReferenceCountingGC(); objA.instance = objB; objB.instance = objA; objA = null; objB = null; } }
当执行到objB.instance = objA时,两个对象的引用计数均为2,将objA和objB置为null,则引用计数变为1。如要满足objA的垃圾回收条件,需要清除对它的引用,即清除objB.instance = objA,而清除这个引用的前提是objB被回收,但objA.instance的引用指向了objB。以此类推,陷入了死循环中。(当然如果把两个instance都指向null就可以回收,但就不是自动回收的,大部分人只是认为把两个对象置为null就能被回收了)。
可达性分析算法
基本思路是通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。
Java中,可作为GC Roots的对象包括:
- 虚拟机栈(栈帧中的本地变量表?这里应该是作者写错,栈帧中是局部变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
引用
无论是什么算法,判断对象的存活都与“引用”有关
JDK1.2前,引用的定义:如果reference类型的数据中存储的数值代表的是另外一块内存区域的起始地址,就称为这块内存代表着一个引用。这种对象只有引用和被引用状态。
JD1.2后,将引用分为强引用、软引用、弱引用、虚引用四种,引用强度依次逐渐减弱。(只要引用为0就会被回收,引用类型对应的是强制回收,即不管引用为多少)
- 强引用:程序代码中普遍存在,类似“Object obj = new Object()”这类的引用。只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
- 软引用:描述一些还有用但并非必须的对象。在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果回收后还没有足够内存才会抛出内存溢出异常。SoftReference类实现软引用。
- 弱引用:同样描述非必须对象,但强度比软引用更弱一些。被弱引用关联的对象只能生存到下一次垃圾收集发生之前,当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只会被弱引用关联的对象(即活不过一回合?)。WeakReference类实现弱引用。
- 虚引用:又被称为幽灵引用或幻影引用。一个对象是否有虚引用的存在,不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的是能在这个对象被收集器回收时收到一个系统通知。PhantomReference类实现虚引用。
生存还是死亡
可达性分析算法中不可达的对象,也并非是“非死不可”的,这时它们暂时处于“缓刑”阶段。要真正宣告一个对象死亡,至少要经历两次标记过程:
- 可达性分析后发现没有与GC Roots相连接的引用链,则会被第一次标记并进行筛选。筛选的条件是对象是否有必要执行finalize()方法,当对象没有覆盖finalize()方法或者finalize()方法已被虚拟机调用后,虚拟机将这两种情况都视为“没有必要执行”(没有必要执行是否就是直接进行第二次标记直接回收?)。如果对象被判定有必要执行finalize()方法,那么会被放在F-Queue队列中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程执行它。但并不承诺会等待它执行结束,因为对象在finalize方法()中可能执行缓慢或者发生死循环,将可能导致F-Queue队列中其它对象永久处于等待,甚至导致整个内存回收系统崩溃。
- finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模标记。如果对象要在finalize()中拯救自己——只要重新与引用链上的任何一个对象建立关系即可(例如将自己即this赋值给某个类变量或者对象的成员变量),在第二次标记时会被移除“即将回收”的集合,如果对象这时候还没有逃脱,那么就真的被回收。这种自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次。
public class FinalizeEscapeGC { public static FinalizeEscapeGC SAVE_HOOK = null; public void isAlive() { System.out.println("yes, i am still alive :)"); } @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("finalize method executed!"); System.out.println(this.name); FinalizeEscapeGC.SAVE_HOOK = this; } public static void main(String[] args) throws Throwable { SAVE_HOOK = new FinalizeEscapeGC(); // 对象第一次成功拯救自己 SAVE_HOOK =null; System.gc(); // 因为finalize()方法的优先级很低,所以暂停0.5秒后等它 Thread.sleep(500); if(SAVE_HOOK != null) { SAVE_HOOK.isAlive(); } else { System.out.println("no, i am dead"); } // 第二次拯救自己失败,因为任何一个对象的finalize()方法都只会被系统自动调用一次 SAVE_HOOK =null; System.gc(); Thread.sleep(500); if(SAVE_HOOK != null) { SAVE_HOOK.isAlive(); } else { System.out.println("no, i am dead"); } } }
运行结果:
finalize method executed!
yes, i am still alive :)
no, i am dead
运行结果为一次逃脱成功,一次失败。因为任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会再被执行,因此第二次自救失败。
尽量避免使用finalize(),其不是C/C++中的析构函数,而是Java刚诞生时为了使C/C++程序员更容易接受它做出的一个妥协。它的运行代价高,不确定性大,无法保证各个对象的调用顺序。finalize()能做的所有工作,使用try-finally或者其他方式都可以做的更好、更及时。因此可以忘掉该方法的存在。
回收方法区
方法区(或者Hotsopt虚拟机中的永久代)可以不实现垃圾收集。其进行垃圾收集的性价比一般比较低。新生代进行一次垃圾收集回收70%~95%,永久代远低于此。
永久代垃圾收集主要回收两部分内容:废弃常量和无用的类。
回收废弃常量与回收Java堆中的对象非常类似。例如一个字符串“abc”已经放入了常量池,但是当前系统没有任何一个String对象引用常量池中“abc”常量,如果此时发生内存回收,这个“abc”常量就会被系统清理出常量池。常量池中的其它类(接口)、方法、字段的符号引用也与此类似。判定一个常量是否是“废弃常量”比较简单,而要判定一个类时否是“无用的类”则要满足下面3个条件:
- 该类的所有实例都已经被回收,也就是Java堆中不存在该类的任何实例
- 加载类的ClassLoader已经被回收
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法再任何地方通过反射访问该类的方法
虚拟机可以对满足上述3个条件的无用类进行回收,仅仅是可以,而并不是跟对象一样,不使用了就必然会回收。
在大量用反射、动态代理、动态生成JSP等频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。
垃圾收集算法
标记-清除算法
首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
不足之处:
- 效率问题:标记和清除两个过程的效率都不高
- 空间问题:标记清除之后会产生大量不连续的内存碎片。空间碎片太多可能会导致后序需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
复制算法
解决了效率问题:将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。这样每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可(清理后第二次应该是从剩下的内存里平分)。代价是将内存缩小到原来的一半,未免太高了。
现在商业虚拟机都采用这种收集算法来回收新生代。IMB公司研究发生新生代中的对象98%都是“朝生夕死”,因此并不需要按1:1的比例来划分内存空间,而是划分成一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一个Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。Hotspot虚拟机默认Eden和Survivor的大小比例是8:1。即每次新生代可用内存空间为整个新生代容量的90%,只有10%的内存会被浪费。但不能保证每次都只有不多余10%的对象存活,当Survivor空间不够时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。
标记-整理算法
复制算法在对象存活率较高时就要进行较多的复制操作,效率会变低。
标记-整理算法针对老年代,标记过程与标记-清除算法一样,但后续不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。即存活对象和可回收对象会进行位置的替换和移动,直到存活对象被整理到最左端,然后再清除掉存活对象边界以外的可回收对象,这样就不会有内存碎片了。
分代收集算法
当前商业虚拟机的垃圾收集都采用“分代收集”(Generational Collection)算法:没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。
一般将Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。新生代中,每次垃圾收集都有大量对象死去,只有少量存活,因此选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。老年代中因为对象存活率高,没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。
HosSpot的算法实现
枚举根节点
可达性分析来标记出将来可能要宣告死亡的对象。
从可达性分析中从GC Roots节点找引用链这个操作为例,可作为GC Roots的节点主要在全局性的引用(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表)。现在很多应用仅方法区就数百兆,如果逐个检查这里面的引用,必然会消耗很多时间。
可达性分析对执行时间的敏感还体现在GC停顿上,因为这项分析必须在一个能确保一致性的快照中进行,即分析过程中不能出现对象引用关系还在不断变化,否则分析结果准确性无法得到保证。这点是导致GC进行时必须停顿所有Java执行线程(Sun称为“Stop The World”)中的一个重要原因。
目前主流Java虚拟机使用的都是准确式GC,因此当执行系统停顿下来后,并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机应当是有办法直接得知哪些地方存放着这些对象引用。HotSpot是使用一组称为OopMap的数据结构来达到这个目的,在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用(?没懂,寄存器是CPU的一部分,栈是内存的一种结构)。这样GC在扫描时就可以直接得知这些信息了。
补充:
- 准确式(precise或exact)GC指GC能够知道一块内存区域是引用还是非引用,如一个32位的区域可以是一个对象引用也可以是int数据类型。准确式GC能准确的识别和回收每一个无用对象,为了准确识别每一个对象的引用,需要存储一些额外的数据。
- 保守式GC不能准确的识别每一个无用对象,但是能保证在不会错误的回收存活的对象的情况下回收一部分无用对象。保守式GC并不需要额外的数据来支持查找对对象的引用,它将所有内存数据假定为指针,通过一些条件来判定这个指针是否是一个合法的对象的引用。
可以把oopMap简单理解成是调试信息。在源代码里面每个变量都是有类型的,但是编译之后的代码就只有变量在栈上的位置了。OopMap就是一个附加的信息,告诉你栈上哪个位置本来是个什么东西。
安全点
在OopMap的协助下,HotSpot可以快速且准确地完成GC Roots枚举。但OopMap的引用关系变化可能非常频繁,如果对每一条指令都生成对应的OopMap,那将会需要大量的额外空间。实际上,HotSpot没有为每条指令生成OopMap,只是在特定的位置记录下这些信息(偏移量对应的类型,哪些位置是引用),这些位置称为安全点,即程序执行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停。安全点的选定是以程序“是否具有让程序长时间执行的特征”为标准选定的,“长时间执行”最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等。具备这些功能的指令才会产生安全点。总的来说,安全点就是指,当线程运行到这类位置时,堆对象状态是确定一致的,JVM可以安全地进行操作,如GC,偏向锁解除等。
在GC中断时要让所有线程都“跑”到最近的安全点上再停顿下来,有两种方式:
- 抢先式中断:不需要线程的执行代码去主动配合,在GC发生时,首先把所有线程全部中断,如果线程中断的地方不在安全点上,就恢复线程,让它“跑到”安全点
- 主动式中断:当GC需要线程中断的时候,不直接对线程进行操作,而是设置一个标志,各个线程执行时主动去轮询这个标志,当标志为真时线程就自己中断挂起。这些轮询标志和安全点是重合的
安全区域
安全点只能保证程序执行时,在不太长的时间内就遇到可进入GC的安全点。但程序不执行时就不保证,比如线程处于Sleep或者Blocked状态。这时候线程无法响应JVM中断请求,走到安全的地方中断挂起。
安全区域指在一段代码片段中,引用关系不会发生变化。这个区域中的任意位置开始区域都是安全的。安全区域可以看做是扩展了的安全点。
在线程执行到安全区域中的代码,就标识自己已经进入了安全区域。在线程要离开安全区域时,检查系统是否已经完成了根节点枚举(或是整个GC过程),完成就继续执行,未完成就等到接收到可以离开安全区域的信号。
垃圾收集器
Serial收集器
Serial收集器是一个单线程,新生代收集器,采用复制算法。暂停其它所有工作线程,即Stop The World,直到它收集结束。
ParNew收集器
ParNew收集器其实就是Serial收集器的多线程版本,也是新生代收集器,除了使用多线程进行垃圾收集外,其余行为都与Serial收集器完全一样。如控制参数、收集算法、Stop The World、对象分配规则、回收策略等。
补充:
在垃圾收集器上下文语境中并发和并行的含义:
- 并行:多条垃圾收集线程并行工作,但用户线程是处于等待状态
- 并发:用户线程与垃圾收集线程同时执行(但并一定是并行的,可能会交替执行)
Parallel Scavenge收集器
是一个新生代收集器。也是使用复制算法,又是并行的多线程收集器。
目标是达到一个可控制的吞吐量。吞吐量就是CPU用于运行用于代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 / (运行用于代码时间 + 垃圾收集时间)。但并不是说就不关注停顿时间。
-XX:MaxGCPauseMills控制最大垃圾收集停顿时间,-XX:GCTimeRatio设置吞吐量大小。
- -XX:MaxGCPauseMills:大于0的毫秒数。GC停顿时间缩短是以牺牲吞吐量和新生代空间换取的。新生代小则收集快,比如收集500m速度大于收集300m。这也导致了收集更频繁,一直在收集则垃圾收集时间长,吞吐量就小。
- -XX:GCTimeRatio:0-100的整数。垃圾收集时间占总收集时间的比率,相当于是1-吞吐量。计算为1/(1+time)。time为19则允许最大GC时间就占总时间的5%。
吞吐量优先的收集器。
-XX:+UseAdaptiveSizePolicy:开关参数,打开后就不要手工指定新生代的大小、Eden与Survivor比例、晋升老年代对象大小的参数了。虚拟机会根据当前系统的运行情况动态调整以提供最合适的停顿时间或最大的吞吐量。
Serial Old收集器
Serial收集器的老年代版本。单线程和“标记-整理”算法。用于客户端。两种用途:JDK1.5之前与Parallel Scavenge搭配使用。另一种是作为CMS(Concurrent Mark Sweep)收集器的后备预案。
Parallel Old收集器
Parallel Scavenge收集器的老年代版本。多线程和“标记-整理”算法。
CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以最短回收停顿时间为目标的收集器。用于老年代。用于服务端。
Mark Sweep可以看出是基于“标记-清除”的算法实现的,运作过程包含四个步骤:
- 初始标记
- 并发标记
- 重新标记
- 并发清除
其中初始标记、重新标记仍然需要“Stop The Wolrd”。初始标记仅标记GC Roots能直接关联到的对象,速度很快,并发标记就是进行GC Roots Tracing,而重新标记就是为了修正并发标记期间用户程序继续运作导致的标记产生变动的那一部分对象的标记记录。这个阶段的停顿时间一般比初始标记的停顿时间长,但远比并发标记的时间短。
整个过程耗时最长的并发标记和并发清除都可以与用户线程一起工作。
优点是并发收集、低停顿。
缺点:
- 对CPU资源非常敏感。默认启动回收线程数为(CPU数量+3)/ 4,即CPU为4时并发回收线程数不少于25%。随着CPU数量下降其占用资源就高,对用户程序的影响就变大。为了应付这种情况曾经提供了一种“增量式并发收集器”(Incremental Concurrent Mark Sweep),在并发标记、清理的时候让GC线程、用户线程交替运行,尽量减少GC线程的独占时间。但实践证明效果一般,被deprecated。
- 无法处理浮动垃圾。可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。由于并发清理阶段用户线程还在运行,自然就会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只能等下一次GC,这一部分垃圾就称为“浮动垃圾”。即垃圾收集阶段用户程序还要运行,也就需要预留足够的空间给用户线程用。因此无法像其他收集器一样等老年代几乎被填满再进行收集,需要预留一部分空间提供并发收集时用户程序使用。JDK1.5老年代使用68%就被激活,可通过-XX:CMSInitiatingOccupancyFraction来提高触发百分比。JDK1.6默认92%。如果CMS运行期间预留内存无法满足需要就会出现“Concurrent Mode Failure”,这时候启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。
- “标记-清除”算法带来不连续内存碎片问题。CMS收集器提供-XX:UseCMSCompactAtFullCollection开关参数(默认开启),用于在CMS收集器要进行Full GC时开启内存碎片的合并整理过程,内存整理过程是无法并发的,停顿时间变长。虚拟机设计者来提供了-XX:CMSFullGCSBeforeCompaction,用于设置执行多次不压缩Full GC后,跟着来一次带压缩的(默认为0,即每次Full GC都要进行碎片整理)。
G1收集器
面向服务端应用的垃圾收集器。有以下特点:
- 并行与并发:使用多个CPU(或CPU核心)来缩短Stop The World的时间。可以通过并发的方式让Java程序与GC动作同时运行。
- 分代收集
- 空间整合:从整体上来看是基于“标记-整理”算法,从局部(两个Region)来看是基于“复制”算法实现(即整体是标记Region来整理,但是实际整理是两个Region之间的复制)。
- 可预测的停顿:降低停顿是G1与CMS共同的关注点,但G1还能建立可预测的停顿时间模型,能让使用者指定在长度为M毫秒内消耗在垃圾收集上的时间不超过N毫秒。
G1将Java堆划分成多个大小相等的独立区域(Region),仍保留新生代和老年代的概念,但不是物理隔离的,都是一部分Region(不需要连续)的集合。G1跟踪各个Region里垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许收集时间,优先回收价值最大的Region。
虚拟机使用Remembered Set来避免全堆扫描。G1中每个Region都有一个与之相应的Remembered Set,在对Reference类型进行写操作会中断并检查Reference引用对象是否处于不同的Region之中(分代的例子中就是老年代对象是否引用新生代对象),如果是则通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。
G1收集器的步骤(不算Remembered Set):
- 初始标记:标记GC Roots能直接关联到的对象
- 并发标记:从GC Roots开始对堆中对象进行可达性分析。可与用户程序并发执行。
- 最终标记:修正在并发标记时用户程序运行导致标记产生变动的那一部分标记。把变化记录在Remembered Set Logs里,然后把Remembered Set Logs的数据合并到Remembered Set中。需要停顿线程但可以并发执行。
- 筛选回收:根据用户期望的GC停顿时间来指定回收计划。可停顿并发,也可不停顿和用户线程一起。
理解GC日志
33.125: [GC [DefNew: 3324K->152K(3712K), 0.0025925 scsc] 3324K->152K(11904K), 0.0031680 secs]
33.125表GC发生时间,从虚拟机启动开始计数,以秒为单位。
“[GC”和“[Full GC”表示垃圾收集的停顿类型,而不是区分新生代还是老年代GC。有“Full”表示发生Stop The World。
接下去“[DefNew”表示GC发生的区域,区域名称与收集器相关。例如DefNew是Serial收集器新生代名称“Default New Generation”,Parallel Scavenge的新生代为“PSYoungGen”。老年代和永久代同理。
“3324K->152K(3712K)”含义是“GC前该内存区域已使用容量->GC后该内存区域已使用容量(该内存区域总容量)”。方括号外“3324K->152K(11904K)”含义是“GC前Java堆已使用容量->GC后Java堆已使用容量(Java堆总容量)”。
0.0025925表该内存区域GC所占用时间。
内存分配与回收策略
自动内存管理解决两个问题:给对象分配内存以及回收分配给对象的内存。
对象主要分配在新生代的Eden区上,分配规则不是固定的,取决于垃圾收集器组合,虚拟机中内存相关参数设置。
对象优先在Eden分配
当Eden没有足够空间,则虚拟机发起一次Minor GC。即把Eden和From Survivor中的存活对象放到To Survivor中去,如果To Survivor放不下,只能通过分配担保机制提前转移到老年代去。
补充:
- 新生代GC(Minor GC):从新生代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC。因为Java大多对象都朝生夕灭,所以Minor GC非常频繁,一般回收速度也快。
- 老年代GC(Major GC/Full GC):指发生在老年代的GC。经常会伴随至少一次的Minor GC,但并非绝对。一般比Minor GC慢10倍以上。
大对象直接进入老年代
大对象指需要大量连续内存空间的Java对象,典型的就是长字符串和数组。避免写“朝生夕灭”的“短命大对象”,容易导致内存还有不少空间就会提前触发垃圾收集来获得足够的连续空间来放置它们。
-XX:PretenureSizeThreshold进行参数设置,大于设置值就直接在老年代分配。只对Serial和ParNew收集器有效。Parallel Scavenge不认识该参数。
长期存活的对象将进入老年代
虚拟机给每个对象定义一个对象年龄计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并被移动To Survivor,其年龄为1。每“熬过”一次Minor GC 年龄就加1。超过默认的15岁就会被晋升到老年代。年龄阈值通过-XX:MaxTenuringThreshold。
动态对象年龄判定
虚拟机并不永远要求对象年龄必须达到设定值才晋升到老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。
空间分配担保
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果大于,那么Minor GC是安全的。小于的话会查看HandlePromotionFailure设置值是否允许担保失败。允许那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于将尝试进行一次Minor GC,尽管有风险;如果小于或者HandlePromotionFailure设置不允许冒险,就改成进行一次Full GC。
冒险:新生代是复制收集算法,用To Survivor来轮换备份,因此可能出现大量对象GC后仍存活,这时需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代。但有多少对象会存活下来是不知道的,所以只要取之前每次回收晋升到老年代对象容量的平均大小作为经验值,与老年代剩余空间比较,决定是否Full GC。如果Minor GC后存活对象高于平均值则担保失败,则失败后重新发起一次Full GC。虽然担保失败绕圈大,但是为了避免Full GC频繁还是打开HandlePromotionFailure开关。
(即老年代最大连续空间大于新生代所有对象,那么即使Minor GC完所有对象都存活要放过来,也放的下。如果小于的话就得担保,担保条件就是最大连续空间大于以前晋升到老年代的平均对象的大小,那么真正GC完后,可能过来的大小跟以前的一样,就担保成功了,也就是放的下。如果担保失败,即要过来的对象总和大于连续空间,那么久不得不Full FC。担保是GC前做的,冒险是GC时做的)。