一.概述
GC需要完成的三件事:1.哪些内存需要回收? 2.什么时候回收? 3.如何回收
Java内存运行时区域的各个部分:
a.其中程序计数器、虚拟机栈、本地方法栈三个区域随线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作,每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的(在运行期会由JIT编译器进行一些优化,本章默认是编译器可知的),因此这几个区域的内存分配和回收都具备确定性,在这几个区域内就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。
b.Java堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的是这部分内存。
二.堆内存的回收
1.判断对象是否还有引用
在堆里面存放着 Java 世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还"存活"着,哪些已经"死去"(即不可能再被任何途径使用的对象)。
1.1 引用计数算法
a. 给对象中添加一个引用计数法,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
b. 引用计数算法的实现简单,判定效率也很高,在大部分情况下它都是一个不错的算法,也有一些比较著名的应用案例,例如微软公司的COM计数,FlashPlayer,Python语言和在游戏脚本领域被广泛应用的Squirrel中都使用了引用计数算法进行内存管理。
c. 但是主流的Java虚拟机里面没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间相互循环引用的问题,例如两个对象A和B都有字段instance,赋值令A.instance = B和B.instance = A,除此之外,这两个对象再无任何引用,实际上这两个已经不可能再被访问,但是它们因为互相引用着对方,导致它们的引用计数都不为0,于是引用计数法无法通知GC收集器回收它们。
1.2 可达性分析算法
a.在主流的商用程序语言(Java、C#)的主流实现中,都是通过可达性分析来判定对象是否是存活的。
b.这个算法的基本思路是通过一系列的称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连,则证明此对象是不可用的。
c.在Java语言中,可作为GC Roots的对象包括下面几种:
1 虚拟机栈(栈帧中的本地变量表)中引用的对象
2 方法区中类静态属性引用的对象
3 方法区中常量引用的对象
4 本地方法栈中JNI(即一般说的Native方法)引用的对象
2.java中的引用类型
a. 无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与"引用"有关。
b. 在JDK 1.2以前,Java中的引用的定义很传统,如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用,这种定义很纯粹,,是太过狭隘,我们希望能描述这样一类对象,当内存空间还足够时,则能保留在内存之中,如果内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象,很多系统的缓存功能都符合这样的应用场景。
c. 在JDK 1.2之后,Java对引用的概念进行了扩充,将引用分为强引用,软引用,弱引用,虚引用 4 中,这4种引用强度依次逐渐减弱。
四种引用类型:
1.强引用:在程序代码之中普遍存在的,类似"Object obj = new Object()"这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
2 软引用:用来描述一些还有用但并非必须的对象,对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常,在JDK 1.2 之后,提供了SoftReference类来实现软引用。
3 弱引用:也是用来描述非必须对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前,当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象,在JDK 1.2之后,提供了WeakReference类来实现弱引用。
4 虚引用:也称为幻影引用,它是最弱的一种引用关系,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例,为一个对象设置虚引用关联的唯一目的就是能在这个对象被垃圾收集器回收时收到一个系统通知,在JDK1.2之后,提供了PhantomReference类来实现虚引用。
3.回收前的标记
即使在可达性分析算法中不可达的对象,也并非是"非死不可",这时候它们暂时处于"缓刑"阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:
1 如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。
2 当对象没有覆盖finalize()方法,或者 finalize() 方法已经被虚拟机调用过,虚拟机将这两种情况都视为"没有必要执行"。
3 如果这个对象被判定为有必要执行 finalize() 方法,那么这个对象将会放置在一个叫做 F-Queue 的队列之中,并在稍后由一个由虚拟机自动建立的低优先级的 Finalizer 线程去执行它,虚拟机会触发这个方法,但并不会承诺等待它运行结束,这样做的原因是,如果一个对象在 finalize() 方法中执行缓慢,或者发生了死循环,将很可能导致 F-Queue 队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。
4 finalize() 方法是对象逃脱死亡命运的最后一次机会,稍后GC将对 F-Queue 中的对象进行第二次小规模的标记,如果对象要在 finalize() 中成功拯救自己,只要重新与引用链上的任何一个对象建立关联即可,那在第二次标记时它将被移除出"即将回收"的集合,如果这时候还没有逃脱,那基本上他就真的被回收了。
5 最好不要用 finalize() 方法拯救对象,它的运行代价高昂,不确定性大,无法保证各个对象的调用顺序,finalize()能做的工作使用try-finally或者其他方式可以做的更好。
三.回收方法区
很多人认为方法区(或者HotSpot虚拟机中的永久代)是没有垃圾收集的,Java虚拟机规范中确实说过可以不要求虚拟机在方法区实现垃圾收集,而且在方法区中进行垃圾收集的"性价比"一般比较低:在堆中,尤其是在新生代中,常规应用进行一次垃圾收集一般可以回收70%~95%的空间,而永久代的垃圾收集效率远低于此。
永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类
1. 回收废弃常量与回收Java堆中的对象非常类似,以常量池中字面量的回收为例,例如一个字符串"abc"已经进入了常量池中,但是当前系统没有任何一个String对象是叫做"abc"的,换句话说,就是没有任何String对象引用常量池中的"abc"常量,也没有其他地方引用了这个字面量,如果这时发生内存回收,而且必要的话,这个"abc"常量就会被系统清理出常量池,常量池中的其他类(接口)、方法、字段的符号引用也与此类似。
2.无用的类:
a.该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
b.加载该类的ClassLoader已经被回收。
c.该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。虚拟机可以对满足上述3个条件的无用类进行回收,这里说的仅仅是"可以",而并不是和对象一样不使用了就必然会回收,在大量使用反射、动态代理、CGLib等ByteCode框架,动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。
注:本章主要以java1.7举例,这里有必要记录下方法方法区的演变:
方法区的实现的演变:
jdk1.7之前:hotspot虚拟机对方法区的实现为永久代 。
jdk1.8及之后:hotspot移除了永久代用元空间代替。运行时常量池存和字符串常量池的变化:
jdk1.7之前:运行时常量池(包含字符串常量池)存放在方法区,此时 hotspot 虚拟机对方法区的实现为永久代。jdk1.7:字符串常量池从方法区移动到了堆中。运行时常量池剩下的东西还在方法区,也就是hotspot中的永久代。
jdk1.8:hotspot移除了永久代,用元空间(Metaspace) 取代。字符串常量池还在堆中,运行时常量池还在方法区, 只不过方法区的实现从永久代变成元空间(Metaspace)。
四.垃圾收集算法
1.基础算法:标记-清除算法
1.该算法分为"标记"和"清除"两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象,它是最基础的收集算法,后续的收集算法都是基于这种思路并对其布局进行改进而得到的。
2.它主要有两个不足:
a.效率问题:标记和清除两个过程的效率都不高。
b.空间问题:标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
2.新生代算法:复制算法
它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清除掉,这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效,只是这种算法的代价是将内存缩小为了原来的一半,代价很高。
1.现代商业虚拟机都采用这种收集算法来回收新生代,新生代中的对象98%是"朝生夕死"的,所以不需要按照 1:1 的比例来划分内存空间而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块Survivor,当回收时,将 Eden 和 Survivor 中还存活着的对象一次性复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间,HotSpot虚拟机默认 Eden 和 Survivor 的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%,只有10%的内存会被浪费,当 Survivor 空间不够时,需要依赖其他内存(这里指老年代)进行分配担保。
2.内存的分配担保:如果另外一块 Survivor 空间没有足够的空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代。
3.老年代算法:标记-整理算法
1.复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低,更重要的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。
2.根据老年代的特点,产生了一种"标记-整理"算法,标记过程仍然与 "标记-清除" 算法一样,但后续步骤不是直接对可回收对象进行整理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
4.总结:分代收集算法
1.当前商业虚拟机的垃圾收集都采用"分代收集"算法,这种算法根据对象存活周期的不同将内存划分为几块,一般是把Java堆分为新生代和老年代,这样根据各个年代的特点采用最适当的收集算法。
2.新生代:在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
3.老年代:在老年代中因为对象存活率高,没有额外空间对它进行分配担保,就必须使用"标记-整理"或者"标记-整理"算法来进行回收。
五.HotSpot的算法实现
1.枚举根节点
1.在可达性分析中从 GC Roots 节点找引用链这个操作为例,可作为 GC Roots 的节点主要在全局性的引用(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表)中,现在很多应用仅仅方法区就有数百兆,如果要逐个检查这里面的引用,那么必然会消耗很多时间。
2.另外,可达性分析对执行时间的敏感还体现在GC停顿上,因为这项分析工作必须在一个能确保一致性的快照中进行------这里"一致性"的意思是指在整个分析期间整个执行系统看起来就像被冻结在某个时间点上,不可以出现分析过程中对象引用关系还在不断变化的情况,该点不满足的话分析结果准确性就无法得到保证,这点是导致GC进行时必须停顿所有Java执行线程的其中一个重要原因,即使是在号称(几乎)不会停顿的CMS收集器中,枚举根节点时也是必须要停顿的。
3.由于目前的主流Java虚拟机使用的都是准确式GC,所以当执行系统停顿下来后,并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机应当是有办法直接得知哪些地方存放着对象引用,在HotSpot的实现中,是使用一组称为OopMap的数据结构来达到这个目的得,在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特点的位置记录下来栈和寄存器中哪些位置是引用,这样GC在扫描时就可以直接得知这些信息了。
2.安全点
1.在OopMap的协助下,HotSpot可以快速且准确地完成 GC Roots 枚举,但一个很现实的问题随之而来:可能导致引用关系变化,或者说OopMap内容变化的指令非常多,如果为每一条指令都生成对应的OopMap,那将会需要大量的额外空间,这样GC的空间成本将会变得很高。
2.实际上,OopMap 也的确没有为每条指令都生成 OopMap,前面已经提到,只是在"特定的位置"记录了这些信息,这些位置称为安全点,即程序执行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停,安全点的选定既不能太少以至于让GC等待时间太长,也不能过于频繁以至于过分增大运行时的负荷,所以安全点的选定基本上是以程序"是否具有让程序长时间执行的特征"为标准进行选定的-----因为每条指令执行的时间是非常短暂,程序不太可能因为指令流太长这个原因而过长时间运行,"长时间执行"的最明显特性就是指令序列复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生安全点。
3.对于安全点,另一个需要考虑的问题是如何在GC发生时让所有线程(这里不包括执行JNI调用的线程)都"跑"到最近的安全点上再停顿下来。这里有两种方案可供选择:
a.抢先式中断:不需要线程的执行代码主动去配合,在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它跑到安全点上,现在几乎没有虚拟机实现采用抢先式中断来暂停线程从而影响GC事件。
b.主动式中断:当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起,轮询标志的地方和安全点时重合的,另外再加上创建对象需要分配内存的地方。
例如:下面的test指令是HotSpot生成的轮询指令,当需要暂停线程时,虚拟机把 0x160100 的内存页设置为不可读,线程执行到test指令时就会产生一个自陷异常信号,在预先注册的异常处理器中暂停线程实现等待,这样一条汇编指令便完成安全点轮询和触发线程中断。
3.安全区域
1.安全点机制保证了程序执行时,在不太长的时间内就会遇到可进入 GC 的安全点,但是在程序不执行的时候,就是在程序没有分配 CPU 时间,典型的例子就是线程处于 sleep 状态或者 blocked 状态,这时候线程无法响应 JVM 的中断请求,JVM 也显然并不可能等待线程重新被分配 CPU 时间,对于这种情况,就需要安全区域来解决。
2.当线程执行到安全区中的代码时,首先表示自己已经进入了安全区,那样,当在这段时间里 JVM 要发起 GC 时就不用管表示自己为安全区状态的线程了,当线程要离开安全区域时,它要检查系统是否已经完成了根节点枚举(或者是整个 GC 过程),如果完成了,那线程就继续执行,否则它就必须等待直到收到可以安全离开安全区域的信号为止。
六.垃圾收集器
我们主要看下HotSpot 中的垃圾回收器
1.Serial收集器
1.单线程收集器,只会使用一个 CPU 或一条收集线程去完成垃圾收集工作(多条垃圾收集并行工作,但用户线程仍处于等待状态),而且在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束,这种停顿就是典型的"stop the world"。
2.虽然如此,但它依然是虚拟机运行在 Client 模式下的默认新生代收集器,它有着优于其他收集器的地方:简单而高效,没有线程交互的开销,可以获得最高的单线程收集效率。
2.PerNew收集器
1. ParNew 收集器是 Serial 收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括 Serial 可用的所有控制参数,ParNew 收集器除了多线程收集之外,与其他 Serial 收集器相比并没有太多创新之处,但它却是许多运行在 Server 模式下的虚拟机中首选的新生代收集器,其中一个与性能无关但很重要的原因是:除了 Serial 收集器外,目前只有它能与 CMS 收集器配合工作。
2. ParNew 随着使用的 CPU 的数量的增加,它对于 GC 时系统资源的有效利用还是很有好处的。
3.Parallel Scavenge收集器
1.Parallel Scavenge 收集器是一个新生代收集器,它使用复制算法,又是并行的多线程收集器(多条垃圾收集并行工作,但用户线程仍处于等待状态)。
2.Parallel Scavenge 收集器的特点是它的关注点是达到一个可控制的吞吐量(CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间))。
3.吞吐量高则可以高效率地利用 CPU 时间,适合在后台运算而不需要太多交互的任务。
4.Parallel Scavenge 收集器也经常称为"吞吐量优先"收集器,通过参数 -XX:+UseAdaptiveSizePolicy 参数开启 GC 自适应调节策略,虚拟户会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。
4.Serial Old收集器
1.Serial Old 是 Serial 收集器的老年代版本,它同样是一个单线程收集器,使用"标记-整理算法"。
2.这个收集器的主要意义在于给 Client 模式下的虚拟机使用。
5.Parallel Old收集器
1.Parallel Old 收集器是 Parallel Scavenge 收集器的老年代版本,使用多线程和"标记-整理"算法。
2.在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。
6.CMS收集器
1.CMS 收集器是一款以获取最短停顿时间为目标的收集器,且为老年代收集器,目前很大一部分的 Java 应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验,CMS 收集器就非常符合这类应用的需求。
2.CMS 收集器基于"标记-清除"算法实现的,它的运作过程相对于前面几种收集器更复杂一些,整个过程分为四个步骤: 初始并发、并发标记、重新标记、并发清除。
3.CMS 收集器的内存回收过程与用户线程一起并发执行的,它的主要优点:并发收集、低停顿。
4.CMS 收集器缺点:
a. CMS 收集器对 CPU 资源非常敏感,在并发阶段,它虽然不会导致用户线程停顿,但会因为占用了一部分线程(或者说 CPU 资源)而导致应用程序变慢,吞吐量会降低,随着 CPU 数量越低,CMS 垃圾收集线程占用的 CPU 资源比例就越大。
b. CMS 收集器无法处理浮动垃圾,可能出现 "Concurrent Mode Failure" 失败而导致另一次 Full GC 产生(由于 CMS 并发清理阶段用户线程还在运行,伴随程序运行自然就会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次 GC 时再清理掉,这一部分垃圾称为"浮动垃圾"),因此 CMS 收集器不能像其他收集器等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。
c. GMS 基于"标记-清除"算法实现的收集器,这意味着收集结束时会有大量空间碎片产生,空间碎片太多会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但无法找到足够大的连续空间来分配当前对象,不得不提前触发一次 Full GC,CMS 收集器提供了一个 -XX:+UseCMSCompactAtFullCollection 开关参数(默认开启的),用于在 CMS 收集器顶不住要进行 Full GC 时开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间会变长,CMS 收集器还提供了另一个参数 -XX:CMSFullGCBeforeCompaction,这个参数用于设置执行多少次不压缩的 Full GC 后,跟着来一次带压缩的(默认为0,表示每次进入 Full GC 时都进行碎片整理)。
7.G1收集器
G1 是一款面向服务端应用的垃圾收集器,HotSpot 开发团队赋予它的使命是未来可以替换掉 JDK1.5 中的 CMS 收集器,与其他GC收集器相比,G1具备如下特点:
1.并行与并发:G1 能充分利用多 CPU,多核环境下的硬件优势,使用多个 CPU 来缩短 "stop The world" 停顿的时间,部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1收集器仍然可以通过并发的方式让 Java 程序继续执行。
2.分代收集:与其他收集器一样,分代概念在 G1 中依然得以保留,虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次 GC 的旧对象以获取更好的收集效果。
3.空间整合:与 CMS 的"标记-清理"算法不同,G1 从整体来看是基于"标记-整理"算法实现的收集器,从局部(两个 Region 之间)上来看是基于"复制算法"实现的,在 G1 运行期间是不会产生内存碎片的,收集后能提供规整的可用内存,有利于程序的长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
4.可预测的停顿:G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过M毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。
5.使用 G1 收集器时,Java 堆内存布局就与其他收集器有很大差别,它将整个 Java 堆划分为多个大小相等的独立区域,虽然还保留了新生代和老年代的概念,但新生代和老年代不再是物理隔离了,他们都是一部分 Region(不需要连续)的集合。
七.内存分配与回收策略
1.新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为 Java 对象大多都具备朝生夕灭的特性。所以 Minor GC 非常频繁,一般回收速度也比较快。
2.老年代GC(Major GC/Full GC):指发生在老年代的 GC,出现了 Major GC 经常会伴随至少一次的 Minor GC(但并非绝对,Parallel Scavenge 收集器的收集策略里就有直接进行 Major GC 的策略选择过程),Major GC 的速度一般会比Minor GC 慢10倍以上。
1.对象在Eden分配
大多数情况下,对象在新生代 Eden 区中分配,当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC。
2.大对象直接进入老年代
所谓的大对象是指需要大量连续内存空间的 Java 对象,最典型的大对象就是那种很长的字符串以及数组,大对象对虚拟机的内存分配来说是一个坏消息,经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来"安置"它们。
3.长期存活的对象将进入老年代
虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应该放在新生代,哪些对象放在老年代,为了做到这点,虚拟机给每个对象定义了一个对象年龄计数器,如果对象在 Eden 出生并经过第一次 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并且对象年龄设为1,对象在 Survivor 区中每熬过一次,年龄就增加1,当它的年龄增加到一定程度(默认为15岁)就将被晋升到老年代中,对象晋升老年代的年龄阈值,可以通过参数: -XX:MaxTenuringThreshold 设置。
4.动态对象年龄判定
为了能更好的适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。
5.空间分配担保
1.在发生 Minor GC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么 Minor GC 可以确保是安全的,如果不成立,则虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败,如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC,尽管这次 Minor GC 是有风险的,如果小于或者 HandlePromotionFailure 设置不允许冒险,那这时也要改为进行一次 Full GC。
2.新生代使用复制收集算法,但为了内存利用率,只使用其中一个 Survivor 空间来作为轮换备份,因此当出现大量对象在 Minor GC 后仍然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,把 Survivor 无法容纳的对象直接进入老年代,与生活中的贷款担保类似,老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,有多少对象会活下来在实际完成内存回收之前是无法明确知道的,所以只好取之前每一次回收晋升到老年代对象容量的平均值的大小值作为经验值,与老年代的剩余空间进行比较,觉得是否进行 Full GC 来让老年代腾出更多空间。
3.取平均值进行比较仍然是一种动态概率的手段,如果某次 Minor GC 存活后的对象突增,远远高于平均值的话,依然会导致担保失败,那就只好在失败后重新发起一次 Full GC,虽然担保失败时绕的圈子更大,但大部分情况下还是会将 HandlePromotionFailure 开关打开,避免 Full GC 过于频繁。