垃圾收集与内存分配策略
1. 垃圾收集
(1)引用计数法:
- 给对象维护一个引用计数器, 当对象被引用时, 计数器加一;
- 当引用失效时, 计数器减一, 当计数器归零时, 回收该对象;
- 该方法实现简单, 判定效率也高, 但是主流虚拟机并没有使用该方法, 因为它难以处理循环引用的问题;
(2)可达性分析法:

- 通过一系列的“GC Roots”对象作为起点进行搜索,如果在“GC Roots”和一个对象之间没有可达路径,则称该对象是不可达的,比如Object5, Object6, Object7处于不可达状态.
- 不过要注意的是被判定为不可达的对象不一定就会成为可回收对象。被判定为不可达的对象要成为可回收对象必须至少经历两次标记过程,如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,则基本上就真的成为可回收对象了.
其中可作为GC Roots的对象:
- 虚拟机栈(栈帧中的本地变量表)中的引用的对象.
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(即native方法)引用的对象
两次标记的过程:
可达性算法中不可达的对象, 并不一定非死不可, 他其实是被判缓刑. 可达性分析后发现一个对象没有与GC Roots相连的引用链, 它就会被打上第一次标记, 同时对它进行一次筛选, 判断是否有必要执行finalize(),该方法从Object继承, 如果该对象执行过一次 finalize()或者没有重写过finalize(),那它将被第二次标记, 二次标记后将被回收.如果该对象被判断为有必要执行finalize(),它将被放入F-Queue队列, 有一个低优先级的线程去执行这些对象的finalize(), 虚拟机承诺会触发这些finalize方法, 但不保证会等到它结束(finalize可能执行缓慢,甚至发生死循环),如果在finalize中该对象成功自救(把自己赋值给某个成员变量或静态变量), 他将不会被二次标记, 不会出现在即将被回收的对象集合.
**每一个对象最多只有一次finalize()自救机会, 尽量不要使用finalize(),它执行时不确定性大, 对象的执行顺序未知, 由于低优先级线程负责, 虚拟机也不会一直等它,它甚至都不一定能执行结束**
1.1 强引用, 软引用, 弱引用, 虚引用
引用: reference类型的数据中存储的数值代表另一处内存的起始地址.
这种定义下, 一个对象只存在引用或没有被引用两种状态.
JDK1.2之后, 引用的概念增强了;
- 强引用:
Object a=new Object();这样的引用就是强引用, 强引用只要存在,对象就不会被回收
- 软引用:
软引用引用的对象只有在内存即将不足时才会被回收, 回收后仍然空间不足才会发生内存溢出.(可以用来实现对内存敏感的高速缓存)
- 弱引用:
弱引用的对象只能存活到下一次GC之前, 无论是否内存空间不足, 一旦发生GC, 它都将被消灭
- 虚引用:
最弱的引用关系, 弱到你甚至不能通过虚引用获得对象实例, 虚引用的存在也不会影响对象的生存.它的存在是当这个对象被回收时收到一条通知(即如果你不想操作一个对象,但是关心它是否被回收的话, 可以使用).
1.2 方法区的回收
- 后续JDK版本的方法区移除(改为本地内存的元空间), 但是方法区本就是一种功能上的要求,可以使用不同手段实现, 元空间只是换了一种实现方式.
- 虚拟机并没有要求对方法区必须要有回收机制, 因为这部分回收效率较低, 不会有太多的可回收资源, 实现垃圾收集也会增加GC的复杂度.
- 该区域可回收的资源主要有, 废弃常量和无用的类信息, 常量的回收较简单, 当常量未被引用时回收(判断是否被引用可以参照堆的引用判定方法).
但是类的回收就很困难, 原因:
1. 类的所有实例都被回收
2. 类的classLoader被回收
3. 类的class对象未被任何地方引用, 没有被任何地方的反射使用.
由于第三方类的加载大部分通过系统类加载器AppClassLoader负责, 该加载器很显然不能被回收, 所以类的卸载只能考虑自己的classLoader加载的.如果大量使用动态代理等频繁自定义classLoader来加载大量的类时, 需要有类的卸载功能, 来确保不发生内存溢出.
1.3 垃圾收集算法
1.3.1 标记清除算法
标记需要回收的对象, 标记完成后再执行统一回收.
该算法作为基础回收算法, 有两个问题需要处理:
1. 效率问题: 标记和清除两个过程效率都不高
2. 空间问题: 清理完成后,会产生大量的空间碎片, 一旦之后的程序需要分配较大的空间,找不到合适大小的空间,就需要执行一次新的垃圾收集.
1.3.2 复制算法
解决效率问题, 把内存空间分为两块, 只使用其中一块, 当要进行回收时, 把这块空间中不需要回收的对象复制到另一块空间中, 然后格式化被使用的这一块.
这种算法的问题:
1. 如果没有很多对象回收, 复制的对象的过程会很缓慢, 因为存活的太多了.
2. 空间浪费很大, 只能使用空间的一半, 另一半准备用来进行复制存活者
上面的这两个问题, 在新生代中其实都不是问题, 根据IBM的调查, 新生代对象98%都是“朝生暮死”, 需要复制的只有剩下的2%;
由于只有少量对象存活, 内存区域划分时就没必要对半分了, 划分的使用区Eden, 复制存放区是两个Survivor, HotSpot整个新生代Eden和两个survivor默认比例8:1, 每次使用Eden和一个survivor, 把存活者放入另一个survivor, 然后清空Eden和刚用过的survivor.
但是survivor由于空间有限, 一旦发生极端情况, 比如Eden中对象全部存活, 只能依靠分配担保.
当survivor空间不足, 将把对象直接放入老年代, 老年代中的对象通常不是这么来的.
1.3.3 标记整理算法
老年代很显然不能使用复制法(每次回收存活大量对象, 没有其他空间可以用来担保), 在该区域回收时将所有保留的对象移动到空间一端顺序排列好, 然后清理端边界之外的对象.
1.3.4 分代收集算法
堆划分为新生代和老年代, 新生代对象存活时间短, 使用复制法, 付出少量的复制成本完成收集; 老年代对象存活数量多, 使用标记整理法, 不需要额外空间分配担保, 不需要像复制法一样复制所有存活对象;
1.4 垃圾收集算法需要解决的问题
1.4.1 枚举根节点
可达性算法中的根结点GC Roots是一个集合, 并不是说内存中所有的对象都指向同一个根节点, 而是有很多根节点, 而生产环境中的应用可能仅仅方法区就几百兆.
全局引用(例如常量和类静态变量等)和执行上下文(例如本地方法栈)都可以作为GC Root节点.
另外, 可达性分析的过程需要整个内存区域的引用之间的关系陷入停滞才能进行, 这需要所有的Java执行线程停顿
(sun将这种过程称之为stop the world) 来!大声和我念!杂瓦鲁多~~~
好在JVM使用准确式GC策略, 对于内存数据而言, 数据是否属于引用类型是明确可知的, 我们不需要在系统停顿之后搜索整个执行上下文和全局引用来确定对象是基本类型还是引用类型. JVM使用特定的数据结构记录引用类型的分布情况(类加载时, Hotspot在OopMap中记录这个类的对象的什么偏移量上是什么类型,类加载时对象并未创建,这些数据是计算得到的,JIT编译时也会在特定位置记录栈和寄存器中的引用的位置).
1.4.2 安全点
解决了如何分辨引用和寻找所有的GC Root, 但是OopMap中的引用关系由于代码的运行随时都可能变化, 我们需要实时的更新OopMap吗?
实际上不需要随时更新OopMap, 引发OopMap更新的代码指令非常多, 实时更新代价很大, 而且OopMap是为垃圾回收服务的, 我们不会一直进行回收, 没必要为每一条指令去维护OopMap. 只有在到达一些“安全点”时, 才记录引用信息, 并且程序也是在此处才能停顿. GC并不是在安全点才开始执行, 它是属于一个独立的线程, 只是执行时会去关注其它线程是否停顿.
安全点的数量太少会导致GC长时间等待, 太多时程序运行负担很大.
安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”为标准进行选定的--因为每一条指令执行的时间都非常短暂,程序不太可能因为指令流长度太长这个原因而过长时间运行, “长时间执行”的最明显的特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等, 所以具有这些功能的指令才会产生安全点.
安全点的位置: 循环末尾,方法返回之前,调用方法之后,抛异常的位置.
要理解为啥选这些位置, 需要结合前文提到的class文件javap反编译之后的指令.
我们的安全点选择时要防止两个安全点“离得太远”, 比如选择循环跳转(这里说的是循环跳转不是整个循环, 而是单次循环的末尾位置),在循环的末尾是goto到循环开始的位置这样一条指令, 就是为了防止我们用两个安全点把“循环整个包住”, 否则一个循环10万次, 那很遗憾, GC得一直等到循环结束才等到下一个安全点;
选择方法返回之前设置安全点的原因也同理, 我们不能让两个安全点把“整个方法包住”, 因为方法执行可能很久, 在方法返回之前, 就保证了一个方法至少含有一个安全点, 而不会发生一个安全点产生后, 进入一个方法, 要一直等待方法结束才会产生另一个安全点的情况.
方法调用之后可以产生, 原因也很简单, 如果说是递归1000次, 我们如果只是在方法返回之前加了一个安全点的话, 那一定会产生如下情况, 设想递归方法开始运行, 第一个安全点是递归运行到最内层(第1000层)产生, 在那之前的每一层其实都不会结束, 都不会返回, 都不会产生安全点, 这也是一种安全点距离过远的情况, 加上方法调用后产生就可以避免该问题了.
**如何到达安全点?**
1. 抢先式中断: 也叫被动中断, 这种方式不需要线程主动配合, 它们只需要听命令就行, GC发生时, 中断所有线程, 发现有不在安全点的, 恢复那个线程, 让它跑到安全点.
2. 主动式中断: 这里的主动指的是线程主动, 设置一个标识, 各个线程在执行时轮询该标识, 发现标识为真时, 主动将自己中断挂起, 而轮询标识的地方和安全点是重合的.
1.4.3 安全区
安全点只解决了程序运行时, 可以在GC时很快进入安全点这样的就绪位置.
而对于sleep和blocked的线程, 无法执行安全点机制的逻辑, JVM显然也不会一直等到这些线程运行起来.
安全区将会解决这个问题. 在安全区中引用关系不会发生变化, 此处可以随时进行GC. 线程进入安全区后, 会被打上标识, 此时如果发生GC, JVM可以不用管处于安全区的线程, 安全区中的线程如果要离开安全区需要先检查根节点枚举是否已经完成, 完成枚举, 则可以离开, 线程继续进行;否则必须等到可以离开的信号为止.
1.5 垃圾收集器

1.5.1 Serial
该收集器是一款单线程收集器, 如上图用于处理新生代垃圾收集(使用的是复制算法).
在进行收集时会暂停所有工作线程, 所以在Server模式下, 不要使用这个收集器, 巨大的新生代空间会有很明显的停顿, 但是Client模式下可以使用, 但运行线程都暂停时, 垃圾收集线程等于可以“集中火力”, 单个GC线程工作也不存在线程切换等额外消耗, 没有任何线程与其竞争任何资源了, Client模式下新生代空间并不大, 停顿时间可以控制在一个不容易被感知的范围.
1.5.2 ParNew
这也是一款新生代收集器, 是Serial的多线程版本(使用复制算法), 在工作时也必须暂停所有工作线程, 多线程处理垃圾收集工作, 在单核处理器上, 该收集器的性能不会比Serial强, 因为该做的工作一点没少, CPU还额外需要进行线程切换. 但在核心数量多时, 可以充分发挥多核优势, 多个核心共同处理收集工作, 可以提高收集速度, 压低停顿时间.
该收集器是CMS收集器(老年代收集器)默认使用的新生代收集器, Parallel Scavenge不能与CMS配合, 当老年代使用CMS, 新生代只能在Serial和Parnew中选一个. Parnew默认开启的收集线程等于CPU核心数.
1.5.3 Parallel Scavenge
多线程收集器, 新生代收集器, 使用复制算法, 不能与老年代收集器CMS联用, 其特殊之处是可以进行吞吐量的控制.
Parallel Scavenge不能与CMS配合也是因为这两个指标互相冲突. 停顿时间的缩短是牺牲了吞吐量和新生代空间获得的.(吞吐量等于业务代码运行时间/总时间,总共运行100分钟,其中1分钟处理垃圾,吞吐量就是99%).
新生代空间越小, 停顿时间越短, 但是这里指的是单次停顿时间, 新生代空间缩小, 将导致GC更加频繁, 总的GC时间会更大(吞吐量将降低),原来10秒一次GC, 每次100毫秒, 现在5秒一次, 而停顿时间一般都会大于50毫秒, 频繁的GC必然会导致定位垃圾时检索到很多不回收的空间, 这部分时间在每一次GC里都要重新检查一遍,所以频繁GC在总的GC运行时间上大概率是亏本买卖(这个问题就像是Mysql插入100万条数据, 关闭自动提交后总时间会下降很多一样的原理, 越频繁的折腾, 每一次的时间缩短弥补不了次数的增加导致的总时间的膨胀).
而吞吐量越大, 运行GC的总时间越短, 单次停顿的时间会更长.
提高吞吐量和停顿时间短各有优势, 停顿时间短用户交互更好, 吞吐量高能更有效的利用CPU时间.
控制吞吐量的参数:
-XX:MaxGCPauseMillis 最大停顿时间(大于0的毫秒数), 虚拟机将尽可能保证停顿时间不超过这个数.
-XX:GCTimeRatio GC时间占比(1/(1+X),X属于开区间(0,100)), 例如设置为19, GC占用时间就是1/(1+19)=5%, 默认99, 1/(1+99)=1%, 即1%的GC时间
Parallel Scavenge的另一个特点是: GC自适应调节.
使用参数: -XX:+UseAdaptiveSizePolicy 打开后,系统将自动配置新生代大小(-Xmn), Eden和Survivor区的比例(-XX:SurvivorRatio), 晋升老年代对象的年龄(-XX:PretenureSizeThreshold)等参数,以获得最合适的吞吐量.开启该功能在使用GCTimeRatio为虚拟机设立一个目标, 它将自动完成其他设置工作.
1.5.4 Serial Old
单线程的老年代收集器, 使用标记-整理算法, 适用于Client模式.

使用场景:
1. 与新生代收集器Parallel Scavenge配合
2. CMS收集器的备用收集器, 并发收集发生Concurrent Mode Failure时, 切换到Serail Old.
1.5.5 Parallel Old
多线程老年代收集器, 使用标记整理算法.
在1.6之后可用, 在此之前, 一旦新生代使用Parallel Scavenge, 那么老年但只能使用Serial Old, 而该收集器的单线程工作模式, 其性能上限很低, 最终导致即使使用Parallel Scavenge整体吞吐量也不高.
Parallel Scavenge + Parallel Old的组合在关注吞吐量以及CPU资源敏感时很强势.

1.5.6 CMS
并发收集, 老年代收集器, 使用标记-清除算法, 致力于降低停顿时间.

CMS收集过程:
1. 初始标记(需要停顿),检索GC Roots能直接关联到的对象, 即找到引用链的根结点, 这个根结点是有很多的, 并不是所有对象都会链接到同一个根上, 这些GC Root并没有一个容器来存储它们, GC Root直接关联的对象, 我的理解就是与根直接相连的对象, 如下图:

2. 并发标记, 长耗时, 与用户线程一起运行, 不需要停顿, 进行GC Roots tracing, 这是在按照引用链继续向下查找, 显然这需要遍历众多的引用树.因为是并发运行的,在运行期间会发生新生代的对象晋升到老年代、或者是直接在老年代分配对象、或者更新老年代对象的引用关系等等,对于这些对象,都是需要进行重新标记的,否则有些对象就会被遗漏,发生漏标的情况。为了提高重新标记的效率,该阶段会把上述对象所在的Card标识为Dirty,后续只需扫描这些Dirty Card的对象,避免扫描整个老年代,并发标记阶段只负责将引用发生改变的Card标记为Dirty状态,不负责处理;
3. 重新标记(需要停顿), 在并发收集过程中发生变动的对象重新修正它们的标记记录.
4. 并发清除, 长耗时, 清理时用户线程可运行.
CMS使用标记清除算法, 而它与其他收集器在标记这一步区别很大, 其他收集器无论单线程还是多线程并发, 它们在标记时都是全程停顿. 要压低停顿时间就要把“标记”这个长过程, 进一步细分, 要想在妈妈打扫房间时可以继续扔纸屑就要考虑在打扫完后把刚扔的纸屑再补扫(重新标记), 而补上的这次打扫是必须停顿的, 即在新的打扫时是不准再扔了的.
然而, CMS也存在它的问题:
1. 对CPU资源敏感, 任何多线程并发的程序都有这个问题, 在并发阶段, 虽然用户线程没有停顿, 但是GC线程和用户线程都在运行(CPU不再像平时一样只处理业务代码了), 吞吐量下降.虚拟机提供了一种应对方法, “增量式并发收集”这是一种如同单核CPU时代抢占式的模拟多任务的策略, 让GC线程和用户线程交替执行, 互相竞争CPU资源, GC争夺到的资源越少, 用户线程越是可以正常运行, 但吞吐量会越低, 实际证明该策略并不好用, GC要做的工作一点也没少, 而且在竞争过程中又引入了更多的线程切换.
2. 无法处理浮动垃圾, 在并发阶段, 用户线程也会继续产生垃圾, 而此时标记过程不能把这部分垃圾统计进去, 只能等下次GC时处理, 这部分就是浮动垃圾. 由于并发过程用户线程的运行, CMS必须更早的开始进行GC, 而不能像其他的收集器一样在老年代快被填满时进行. 如果并发运行时, 发生空间不足, 就会出现Concurrent Mode Fail, 此时会切换到Serial Old收集器.
3. 标记清除算法, 会产生空间碎片, 这些碎片化的空间很难使用, 特别是要分配大对象时. 空间不足时, 会提前进行一次Full GC.
-XX:+UseCMSCompactAtFullCollection开关参数, 开启时,CMS在即将进行Full GC时, 会执行空间压缩进行内存整理, 该过程不能并发, 所以停顿时间会变长, 默认开启
-XX:CMSFullGCsBeforeCompaction设置多少次不带整理的Full GC之后来一次带整理的, 默认0, 每次Full GC后都整理
1.5.7 G1
并发收集, 同时适用于新生代和老年代, 使用标记整理算法, 可预测的停顿.
这种标记整理算法是从宏观角度看的, G1把新生代和老年代进行了更加的细分, 划为很多区域, GC发生在这些区域时, 其实是使用复制算法.
G1收集器对新生代和老年代进行细化, 每一个区域都是一个回收区, 每一个回收区自带一个集合Remembered Set, 这个集合用来保存非本区域的对象, 而且本区域的对象一定被Remembered Set中的对象引用, 即系统会在引用类型的数据发生写操作时, 先中断该操作, 检查被引用的对象是否是跨区的对象, 如果是就通过CardTable把相关引用的信息写到它引用的那个对象的区域的Remembered Set中, 这样就可以在对一个区域进行GC时, 把Remembered Set加入GC Roots的枚举中即可保证不发生遗漏.

G1回收过程:
1. 初始标记:除了检索GC Roots直接关联的对象之外, 还需要修改TAMS(Next Top at Mark Start)的值, 在下一个阶段并发标记时, 用户线程能够使用正确的Region来创建新对象, 该过程需要停顿.
2. 并发标记:可达性分析阶段, 不需要停顿, 与用户线程并发执行.
3. 最终标记:与CMS一样, 并发标记阶段会产生引用关系变动, 引用关系的变动将导致标记变动, 这部分的标记记录将被记录到Remembered Set Logs中, 最终标记阶段将把Remembered Set Logs中的数据合并到Remembered Set中, 该部分一定需要停顿, 可以并发执行, 我在这里的理解是Remembered Set已经进行过标记处理了, 其中的元素在初始标记进行GC Roots关联对象的遍历时就连带着这部分一起检查过了, 甚至这部分的元素也会被作为GC Roots(我猜的),并发过程中的变动会不会也包括这部分的变动呢, 变动发生, 最终标记阶段对变动部分进行重标.
4. 筛选回收:在这个阶段将各个Region按照回收价值和成本进行排序, 我们前面说过G1可以进行可预测的停顿, 系统会按照设置的停顿时间组织相应的区域优先处理甚至只处理一部分区域, 力图达到该目标. 该阶段可以与用户线程一起执行, 但是由于只回收一部分区域, 只进行GC可以提高回收效率.
G1的问题:
1. 浮动垃圾的问题依然存在.
2. 该收集器也是以压制停顿时间为目标, 如果关注吞吐量这一指标, 它并不比Parallel强.
1.6 三色标记法
CMS的可达性分析实现就是把对象标为三种颜色(黑,灰,白):
黑色:不回收, 本身和其下所有引用(这里说的是直接引用)都已被扫描.
灰色:本身被扫描但至少还有一个直接引用未被扫描
白色:没有被扫描, 或者不可达
过程:
1. 所有对象一开始都是白的
2. GC Roots其直接引用的对象设置为灰色
3. 遍历灰色的所有引用, 遍历完成后设置为黑色, 其引用设置为灰色
4. 重复3, 直至没有灰色
5. 回收所有白色
在STW时, 该过程可以保证没有问题.
但如果是有用户线程在运行, 则可能发生漏标和错标:
漏标: 本来该标识为黑色, 没有标为黑色, (在并发时, 对象重新被某个线程使用到),将导致错删被使用的对象

错标: 本来不该是黑色, 结果被涂成黑色, (并发时, 对象的引用彻底消失了),会少删了该对象.

1.7 GC日志
-XX:PrintGCDetails 打印GC日志
日志格式如下:

不同的收集器, 年轻代老年代的名字会有区别, 上面的PSYoungGen是因为使用的Parallel Scavenge.
GC: 就是新生代或老年代发生一次GC
Full GC: 全内存区都发生, 可以看到后面有各个代的收集数据, 如果是调用System.gc()会显示[Full GC(System)]
后面是各个区域的使用空间的大小变化, A->B(C), 从使用A那么多变成使用B那么多, 总空间还有C那么多. 通常后面还会有一个汇总, 总的内存使用, 如上面, 第三行6491K->6235K(163328K)就是汇总了前面的年轻代和老年代, 由于使用jdk1.8, 元空间属于直接内存的一部分, 独立出jvm运行时数据区了.
[Times:]: 表示使用的时间, 依次是: 用户态消耗的CPU时间, 内核态消耗的CPU时间, 墙钟时间(实际用时).
墙钟时间除了包括CPU消耗的, 还有磁盘IO、线程阻塞等等耗时, 当CPU是多核时, CPU耗时是每个核心的相加, 所以它超过墙钟时间是完全正常的.
1.8 垃圾收集常用参数

2. 内存分配与回收策略
2.1 对象优先在Eden分配
-Xms20M 初始大小20M
-Xmx20M 最大20M
-Xmn10M 年轻代10M (Eden+survivor)
-XX:+PrintGCDetails 打印GC日志
-XX:SurvivorRatio=8 (Eden:一个survivor=8:1)
-XX:+UseSerialGC 使用Serial+Serial Old
-Xloggc:gc.log GC日志文件输出路径

设置堆空间初始大小20M, 最大20M, 不能扩展, 年轻代10M, Eden:8M, 每个Survivor:1M, 分配对象大小2M,2M,2M,4M.

运行代码, 日志显示一次GC:Allocation Failure, 这是Minor GC, 只回收了DefNew即新生代, 因为新生代空间10M, 其中可以使用的是8+1, 一个survivor要作为GC时的复制区, 分配第四个对象时, 新生代空间不足, 做了一次Minor GC后, 分配到老年代.
2.2 大对象直接在老年代
还有什么比一个大对象更让人崩溃的吗? 一群“朝生夕灭”的“短命大对象”.
-XX:PretenureSizeThreshold=10240 超过该值直接老年代, 单位不能写(单位B), 只支持Serial 和 Parnew
2.3 长期存活的对象进入老年代
分代收集, 需要分辨哪些对象放在老年代, 哪些放在新生代, 每个对象维护一个年龄计数器 ( 对象的对象头的mark word中 ), 当经过一次MinorGC后, 对象还存活, 并且survivor还有空间容纳, 将该对象移动到survivor, 对象年龄加一, 当年龄增加到一定阈值(默认15), 将晋升到老年代.
-XX:MaxTenuringThreshold 设置年龄阈值
2.4 动态年龄判定
为适应不同程序的内存状况, 虚拟机并不要求严格遵守年龄阈值才能晋升, 如果survivor中某个年龄的对象大小之和超过survivor的一半, 那么年龄大于等于该年龄的对象就可以直接晋升.
2.5 空间分配担保
老年代的连续空间大于新生代对象的总大小或者历次晋升的平均水平就进行Minor GC, 否则进行Full GC.