1. 对象已死?
1.1 引用计数法
给对象添加一个引用计数器,被引用时加1,反之减1。存在对象相互循环引用。
public class ReferenceCountingGC { public Object instance = null; private static final int SIZE = 1024 * 1024; private byte[] bigSize = new byte[SIZE]; public static void testGC() { ReferenceCountingGC objA = new ReferenceCountingGC(); ReferenceCountingGC objB = new ReferenceCountingGC(); objA.instance = objB; objB.instance = objA; objA = null; objB = null; System.gc(); } }
至少主流的Java虚拟机没有选用引用计数法来管理内存。
1.2 可达性分析
Java语言中GC Roots的对象包括以下几种:
1)虚拟机栈中(栈帧中的本地变量表)的引用变量。栈帧就是Java在每个方法执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
2)方法区中类的静态属性引用的对象。方法区用于存放已经被虚拟机加载的类信息、常量、静态变量、即时编译其编译后的代码等数据。
3)方法区中常量引用的对象
4)本地方法栈中JNI(即一般说的Native方法)引用的对象
1.3 引用
JDK1.2之后,java对引用的概念进行了扩充,将引用分为了强引用、软引用、弱引用、虚引用四种,引用强度依次逐渐减弱。
1)强引用(Strong Reference):在程序代码中普遍存在的,类似于“Object o = new Object();”这类引用,只要强引用还在,垃圾收集器就不会回收被引用的对象。
2)软引用(Soft Reference):用来描述一些有用但非必需的对象。系统在发生内存溢出异常前,将会把这些对象列进回收范围之中进行的二次回收,SoftReference类。
3)弱引用(Weak Reference):也是用来描述非必需的对象,被引用关联的对象只能生存到下一次垃圾收集发生之前,垃圾回收时,无论内存是否足够,都会回收,WeakReference类。
4)虚引用(Phantom Reference):最弱的之中引用关系,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用的唯一目的就是能在这个对象被收集器回收时收到一个系统通知,通过PhantomReference类来实现。
1.4 生存还是死亡
如果由于一个对象被判定为有必要实行finalize()方法,那么对象将会放置在一个F-Queue的队列中,然后虚拟机会自动建立一个优先级低的Finalizer线程去执行,但虚拟机不会等待它运行结束,任何一个对象的finalize方法都只会被系统自动调用一次。
finalize()方法可以逃脱一次GC,但是并不推荐使用,因为它运行代价高,不确定性大,无法保证各个对象的调用顺序。
2.垃圾回收算法
2.1 标记-清除算法(百度搜出来的图片):
思想就是先标记需要回收的,然后回收
缺点就是标记和清除的效率不高,清除后还有很多碎片,分配较大对象时无法找到足够的连续内存而不得不提前出发一次垃圾回收动作。
2.2 复制算法
思想就是把内存分为两块容量一样的内存空间,每次只使用一块,当某块内存使用完了就把还存活的对象复制到另一块,再把原先那块清理掉。
优点是实现简单,运行高效,分配内存时不用考虑内存碎片,只需要移动堆顶指针,按顺序分配即可。
缺点就是内存利用率低,每次就只用一半,所幸通过IBM的研究表明,新生代中对象基本都是’朝生夕死‘,所以不用按照1:1来,通常把新生代分为一个Eden : From Survivor : To Survivor空间,比例默认8:1:1(可以通过-XX:SurvivorRatio=8设置Eden可Survivor空间的比率),每次只是用Eden+一个Survivor,当Survivor空间不够时需要老年代分配担保。
例子:
测试新生代垃圾回收
private static final int _1MB = 1024 * 1024; /** * VM Properties * -XX:+UseSerialGC -verbose:gc -Xms20M -Xmx20M -Xmn10M * -XX:+PrintGCDetails -XX:SurvivorRatio=8 * */ public static void main(String[] args) { byte[] allocation1, allocation2, allocation3, allocation4; allocation1 = new byte[2 * _1MB]; allocation2 = new byte[2 * _1MB]; allocation3 = new byte[2 * _1MB]; allocation4 = new byte[4 * _1MB]; }
设置VM配置我的IDE时IDEA在VM options里输入虚拟机的配置
上述配置的意义:
-XX:+UseSerialGC是指选择了Serial(新生代) + SeialOld(MSC年老代)垃圾收集器组合,具体在后面
-verbose:gc 表示输出虚拟机中GC的详细情况
-Xms 是指设定程序启动时占用内存大小。一般来讲,大点,程序会启动的快一点,但是也可能会导致机器暂时间变慢。
-Xmx 是指设定程序运行期间最大可占用的内存大小。如果程序运行需要占用更多的内存,超出了这个设置值,就会抛出OutOfMemory异常,所以说上面程序用20M的内存。
-Xmn 是指年轻代的大小
-XX:+PrintGCDetails 是指打印GC细节,直译
-XX:SurvivorRatio=8 是指Eden : Survivor大小为8 : 1
上面程序的结果:
新生代和年老带都是10M,Eden为8M,两个Survivor都为1M,所以新生代总的可用的空间为9M。
按照代码中的分配,先分配了3个2M的空间,然后分配4M空间的时候,由于内存空间不足了,新生代发起了一次Minor GC,要把3个2M的放到1M的Survivor空间里,但是显然不行,然后就通过担保机制放到了年老代中。
通过打印的日志可以看到新生代空间被回收了但是总的使用空间没有太大的变化,是因为那6M的对象还没有’死‘。
这次GC结束后,4M的allocation4被安排到了Eden里,年老代占用6M。
[GC (Allocation Failure) [DefNew: 7976K->643K(9216K), 0.0042953 secs] 7976K->6787K(19456K), 0.0043279 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 4905K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 52% used [0x00000000fec00000, 0x00000000ff0297e8, 0x00000000ff400000)
from space 1024K, 62% used [0x00000000ff500000, 0x00000000ff5a0cb0, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
tenured generation total 10240K, used 6144K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 60% used [0x00000000ff600000, 0x00000000ffc00030, 0x00000000ffc00200, 0x0000000100000000)
Metaspace used 3137K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 343K, capacity 388K, committed 512K, reserved 1048576K
新生代GC(Minor GC):指发生在新生代的垃圾收集动作,非常频繁,回收速度快。
老年代GC(Major GC / Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次Minor GC(但非绝对的,在Parallel Scavenge收集器收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。
2.3 标记-整理算法
上面的复制算法在对象存活率比较高(年老代的对象存活时间都比较长)的时候需要进行多次复制,效率会降低。
如果不想浪费50%的空间,就需要由额外的空间担保,以应对所有对象都存活的极端情况,所以年老代一般不能直接选用复制算法。
根据年老代的特点,有人提出了“标记-整理”(Mark-Compact)算法,标记过程和“标记-清除”一样,在回收过程中是让所有活着的对象都向一段移动,然后清理掉端边界以外的内存。
2.4 分代收集算法
把Java堆分为新生代和老年代,这样就可以根据他们各自的特点来选择合适的收集算法。
3. HotSpot的算法实现
3.1 枚举根节点
从可达性分析从GC ROOT节点找引用链这个操作为例,GC ROOT主要是方法区中的类静态属性和常量引用的对象,虚拟机栈引用的对象和本地方法栈JNI引用的对象。
方法区假如有几百兆,如果逐个检查这里面的引用,那么效率肯定很低。
另外,可达性分析对执行时间敏感还体现在GC停顿(Stop the world),因为你不停顿分析的结果可能不准,比如分析完了,某个被判定为可回收的对象又和GC ROOT有链路了。
即使是在号称不会发生停顿的CMS垃圾收集器,在枚举根节点时也会停顿。
在HotSpot中,是使用一组称为OopMap的数据结构来让虚拟机知道那里存放着对象的引用,在类加载完成时,hotspot就把对象什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中那些位置是引用。
3.2 安全点
在OopMap的帮助下,HotSpot可以快速且准确地完成GC Roots枚举,HotSpot不会为每条指令都生成OopMap,只是在特定的位置记录这些信息,这些位置被称为安全点,只有达到安全点才可以暂停。Safepoint的选定既不能太少以至于让GC等待时间过长,也不能太多,以至于频繁的GC加大运行时的负担。所以,安全点的选定基本上是以程序”是否具有让程序长时间执行的特征”为标准进行选定的--因为每条指令实行的时间都非常短,程序不太可能因为指令流长度过长这个原因而执行时间过长,“长时间执行”的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生安全点。
上面是如何选择安全点的问题,下面是如何在发生GC时让所有线程(这里不包括JNI调用的线程)都跑到最近的安全点停下来。
1. 抢先式中断:在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程让他跑到安全点上,几乎没有虚拟机这么做。
2. 主动式中断:当GC需要中断线程时,不直接对线程操作,而是简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。
3.3 安全区域
SafePoint在线程处于Sleep或者Blocked状态时,无法响应JVM的中断请求,而JVM也不太可能等待线程重新分配CPU时间,对于这种情况就需要安全区了。
安全区域是指一段代码片段中,引用关系不会发生变化。在这个区域的任意位置开始GC显然是安全的。
当线程执行到Safe Region中的代码时,首先标识自己已经进入了Safe Region,当这段时间里JVM要发起GC时,就不用管标识自己为Safe Region状态的线程了。在线程要离开安全区域时,它要检查系统是否已经完成了根节点的枚举,如果完成了那线程就继续执行,不然就要等待可以安全离开Safe Region的信号。
4. 垃圾收集器
线条代表可以搭配使用,选择垃圾收集器主要看你的应用,选择合适的很重要。
1. Serial收集器(单线程的收集器)
它在进行垃圾回收时,必须暂停其他所有的工作线程,直到它收集结束。它依然是Client模式下新生代默认的收集器。
缺点:STW时间长
优点:简单高效,对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程手机效率。
2. ParNew收集器
其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数、收集算法、STW、回收策略等都与Serial收集器一样,在实现上,这两种收集器也共用了很多的代码。在上面的图中可以看出只有Serial和ParNew可以和CMS一起工作。
-XX:+UseConcMarkSweepGC默认使用ParNew作为新生代的垃圾收集器
-XX:+UseParNewGC来强制使用ParNew收集器
-XX:ParallelGCThreads限制垃圾收集的线程数
3. Parallel Scavenge收集器
他是一个新生代的收集器,也使用了复制算法,又是并行的多线程收集器,它的特点是它的关注点与别的收集器不同,CMS等收集器关注的是尽可能缩短垃圾收集式用户线程停顿的时间,而Parallel Scavenge收集器则关注达到一个可控制的吞吐量。
吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
低停顿时间的收集器适合用户交互的程序,而高吞吐量适合后台运算且不没有太多交互的任务,这样cpu的利用率高,处理的快。
最大垃圾收集停顿时间:-XX:MaxGCPauseMillis,一个大于0的毫秒数,尽可能保证回收时间不超过它,不是越小越好,因为他是通过牺牲吞吐量和新生代空间来换取的,新生代越小,回收越快,GC频繁的话可能最后的吞吐量降低了。
设置吞吐量大小:-XX:GCTimeRatio,大于0且小于100,如果为n,那么允许最大1 / (1 + n)比率的时间作为垃圾回收的时间。
使用-XX:+UseAdaptiveSizePolicy参数的话,那么就不需要手动设置新生代大小(-Xmn),Eden与Survivor的利率,晋升老年代对象大小等细节了,虚拟机会更具当前系统运行情况收集性能监控信息,动态调整来提供最合适的停顿时间或者最大吞吐量,这被称为GC自适应调节策略。
4. Serial Old收集器
单线程的老年代收集器,使用标记整理算法。
用处:
1. 主要的用处就是给Client模式下的虚拟机使用
2. server模式下:1)jdk1.5及以前,与Parallel Scavenge搭配使用 2)CMS收集器的后备方案(当发生Concurrent Mode Failure时)
5. Parallel Old收集器
Parallel Scavenge收集器的老年代版本,使用多线程和‘标记整理算法 ’,jdk1.6开始提供,在这之前Parallel Scavenge只能和Serial Old搭配使用,没有别的选择。
6. CMS(Concurrent Mark Sweep)收集器
它是一种以获得最短停顿时间为目标的收集器,采用标记清除,目前很大一部分的Java应用集中在互联网站或者B/S系统的服务器上,这类应用尤为重视响应速度,希望系统停顿时间少。
整个过程分为以下四个步骤:
1. 初始标记(CMS initial mark):仍然需要STW,初始标记仅仅是标记一下GC ROOT能直接关联到的对象,速度很快
2. 并发标记(CMS concurrent mark):就是进行GC Roots Tracing的过程
3. 重新标记(CMS remark):仍然需要STW,修正并发标记期间因为用户程序继续运行而导致标记变动的那一部分对象的标记记录,停顿时间会比初始标记稍长,但是远比并发标记的时间要短
4. 并发清除(CMS concurrent sweep):
缺点:
1. CMS收集器对CPU资源非常敏感:虽然在并发标记和并发清除时,收集器不停止其他线程的工作,但是会占用到CPU,CMS默认启动的回收线程数是(CPU数量+3)/4,那么CPU资源少的时候就会有很大的影响。
2. CMS收集器无法处理浮动垃圾:可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。浮动垃圾就是在标记后出来的新的垃圾,收集器无法在本次收集中回收这部分的垃圾。由于垃圾收集阶段用户线程还需要运行,所以不能等到老年代快满的时候Full GC,jdk1.5的默认是当老年代使用68%(可以通过-XX:CMSInitiatingOccupancyFraction来设置),jdk1.6为92%是触发GC。要是CMS运行期间的预留空间不足,就会出现Concurrent Mode Failure,虚拟机将启动后备预案,临时启用Serial Old,这样停顿的时间就很长了。
3. 基于“标记-清除”算法实现的收集器,会有大量空间碎片,这对于分配大对象时是致命的,可能老年代还有很多可用空间,但是因为碎片的原因导致对象不能分配,然后就来了一次Full GC。为了解决这个问题,CMS收集器提供了-XX:+UseCMSCompactAtFullCollection开关参数(默认是开启的),在顶不住要Full GC时开启内存碎片整理,内存整理的过程是无法并发的,所以停顿时间会变长。-XX:CMSFullGCBeforeCompaction,这个参数用于设置执行多少次不压缩的Full GC后来一次带压缩的,默认为0表示每次Full GC都进行碎片整理。
7. G1收集器
G1是一款面向服务端应用的垃圾收集器。
在使用G1做垃圾收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的Region,虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔离的,他们都是一部分Region(不需要连续)的集合。
在G1收集器中,Region之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,虚拟机都是使用Remembered Set来避免全堆扫描的。G1中每个Region都与一个Remembered Set对应,虚拟机啊发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region中,如果有便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set既保证不对全堆扫描也不会有遗漏。
运作步骤:
1. 初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,并修改TAMS的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建对象。
2. 并发标记(Concurrent Marking):可达性分析,找出存活的对象。
3. 最终标记(Final Marking):为了修正并发标记期间因为程序继续运转而导致标记产生变动的那一部分标记记录。虚拟机将这段时间对象的变化记录到Remembered Set Log里,最终标记需要把log里的数据合并到Remembered Set中。
4. 筛选回收(Live Data Counting And Evacuation):堆region的回收价值和成本进行排序,根据用户希望的回收时间制定回收计划。
G1与相较于其他垃圾收集器的热点:
1. 并行与并发:充分利用多核,降低STW的时间,部分其他收集器需要停顿的地方可以并发。
2. 分代收集:分代收集概念在G1中仍然保留,虽然G1可以不需要其他收集器就可以管理整个GC堆,但他能够采取不同的方式去处理新创建的对象、存活了一段时间的对象、熬过多次GC的就对象以获得更好的收集效果。
3. 空间整合:从整体来看是基于“标记-整理”,局部(两个Region)来看是基于“复制”算法实现的。
4. 可预测的停顿:G1除了追求低停顿时间外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个m毫秒的时间片段内,消耗在垃圾收集上的时间不超过n毫秒。G1之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集,它在后台维护一个优先列表,每次在允许的时间内回收价值最大(回收的空间大小以及回收所需要的时间)的Region,这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
8. 了解GC日志
有的收集器的GC占用时间可能是这样的"[Times: user=0.01 sys=0.00 real=0.02 secs]",分别代表了用户态消耗的CPU时间、内核态消耗的CPU时间和操作从开始到结束所经过的墙钟时间(Wall Clock Time)。CPU时间和墙钟时间区别是,墙钟时间包括各种非运算的等待时间,比如等待IO,等待线程阻塞,而CPU时间不包括这些,当系统有多核或多CPU时,多线程操作会叠加这些CPU时间,所以user和sys时间大于real时正常的。
9. 垃圾收集器参数总结
参数 | 描述 |
UseSerialGC | 虚拟机运行在Client模式下的默认值,使用Serial + Serial Old组合和 |
UseParNewGC | ParNew + Serial Old |
UseConcMarkSweepGC | ParNew + CMS + Serial Old(CMS发生Concurrent Model Failure时的后备收集器) |
UseParallelGC | Server模式下的默认值,Parallel Scavenge + Serial Old |
UseParallelOldGC | Parallel Scavenge + Parallel Old |
SurvivorRatio | Eden和Survivor空间的比,默认8:1 |
PretenureSizeThreshold | 直接晋升老年代的大小 |
MaxTenuringThreshold | 晋升到老年代的年龄 |
UseAdaptiveSizePolicy | 动态调整Java堆中各个区域的大小以及进入老年代的年龄 |
HandlePeomotionFailure | 是否允许分配担保失败 |
ParallelGCThreads | 并行GC时进行内存回收的线程数 |
GCTimeRatio | 默认99,即允许1%的时间做GC,仅在Parallel Scavenge是有效 |
MaxGCPauseMillis | GC的最大停顿时间,仅在Parallel Scavenge时有效 |
CMSInitiatingOccupationFraction | CMS收集器在老年代用掉多少时出发垃圾收集,默认68% |
UseCMSCompactAtFullCollection | 设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片整理 |
CMSFullGCsBeforeCompaction | 设置CMS收集器多少次不压缩的Full GC后来一次带压缩的,默认0,表示每次FullGC都会进行碎片整理 |
5. 内存分配与回收策略
自动化内存管理主要时自动化分配内存和自动化回收内存。
书中代码实在Client模式下运行的,所以垃圾收集器的组合时Serial + Serial Old。-XX:+UseSerialGC
5.1 对象优先分配在Eden
5.2 大对象直接进入老年代
所谓的大对象就是需要大量连续内存空间的Java对象,比如上面的byte数组。
/** * VM Properties: * -verbose:gc -Xms20m -Xmx20m -Xmn10m -XX:+PrintGCDetails -XX:SurvivorRatio=8 * -XX:PretenureSizeThreshold=3145728 这个参数只对Serial和ParNew收集器有效 * -XX:+UseSerialGC * */ public class PretenureSizeThresholdTest { private static final int _1MB = 1024 * 1024; public static void main(String[] args) { // 直接分配到老年代里 byte[] allocation = new byte[4 * _1MB]; } }
Heap
def new generation total 9216K, used 2185K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 26% used [0x00000000fec00000, 0x00000000fee22780, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation total 10240K, used 4096K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 40% used [0x00000000ff600000, 0x00000000ffa00010, 0x00000000ffa00200, 0x0000000100000000)
Metaspace used 3221K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 350K, capacity 388K, committed 512K, reserved 1048576K
5.3 长期存活的对象将进入老年代
可以通过-XX:MaxTenuringThreshold这只进入老年代的年龄,Eden空间出生并经过一次Minor GC后任然存活的,并且能被Survivor空间容纳的下的,将被移动到Survivor空间,年龄为1,此后熬过一次Minor GC年纪加1。
/** * -verbose:gc -Xms20m -Xmx20m -Xmn10m -XX:+PrintGCDetails -XX:SurvivorRatio=8 * -XX:MaxTenuringThreshold=1 -XX:+UseSerialGC -XX:+PrintTenuringDistribution * */ public class MaxTenuringThresholdTest { private static final int _1MB = 1024 * 1024; public static void main(String[] args) { byte[] allocation1, allocation2, allocation3; allocation1 = new byte[_1MB / 4]; // 什么时候进入老年代取决于XX:MaxTenuringThreshold设置 allocation2 = new byte[4 * _1MB]; allocation3 = new byte[4 * _1MB]; allocation3 = null; allocation3 = new byte[4 * _1MB]; } }
因为参设设置的是1,所以在第二次GC时,那个1MB/4的空间的年龄到了,被移动到了老年代。
当XX:MaxTenuringThreshold=15:
通过GC日志可以看出allocation1还是被送到老年代里了,我猜测是因为动态年龄判定导致的,因为我没有主动创建对象和数组时,eden空间有占用了几百KB,所以我猜测发生GC时,这几百KB和那个1/4 MB的总大小超过了Survivor的一半导致动态对象年龄判定,直接进入老年代而不管它是否到了年纪。
经过上面的猜测,我重新设置了VM的参数,让Eden空间大小不变,survivor大小变大一点,结果如第二张截图所示。
第一张截图(allocation1被送到老年代了):
第二张截图(allocation1在survivor空间):
新的参数:
-verbose:gc -Xms22m -Xmx22m -Xmn12m -XX:+UseSerialGC -XX:+PrintGCDetails
-XX:SurvivorRatio=4 -XX:MaxTenuringThreshold=15 -XX:+PrintTenuringDistribution
结果证明我的猜测是对的,应该:)
5.4 动态对象年龄判定
如果在Survivor空间中相同年龄所有对象的大小总和大于Survivor的一半,年龄大于或者等于该年龄的对象就直接进入老年代。
通过上面的例子,我们可以看到年龄为2的对象大小已经超过了原先的Survivor空间的一半,所以才会出现最初的结果和书上的不一样。所以书上那个例子就不写了。
5.5 空间分配担保
在发生Minor GC前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure(在Jdk 6u24之前有效,在此之后只要老年代的最大可用连续空间大于新生代的对象总大小或者大于历次晋升的平均大小,就会进行Minor GC,如果失败再用Full GC)设置值是否允许担保失败。如果允许,则会继续检查老年代最大可用连续空间是否大于历次晋升老年代的对象的平均大小,如果大于则尝试一次Minor GC,尽管这次Minor GC是有风险的;如果小于或者HandlePromotionFailure设置不允许冒险,那这时要改为Full GC了。