程序计数器,虚拟机栈,本地方法栈这三个区域随线程而生,随线程而灭,栈中的栈帧随着方法的进入和退出而有条不紊的执行入栈和出栈操作,每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的。因此这几个区域不需要过多考虑回收的问题。而java堆和方法区的内存的分配都是动态的,故需要关注这一部分的内存回收。

垃圾收集需要考虑的三件事:

  • 哪些内存需要回收
  • 什么时候回收
  • 如何回收

如何判断对象已经死去

引用计数器法

概述:给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;引用失效,计数器就减1;当一个对象的引用计数器为0时,表示对象已死,可回收

优点:实现简单,并且高效

缺点:无法解决对象之间的循环引用问题

可达性分析算法

概述:解决引用计算器法的无法判断循环引用的问题,基本思路是,以一系列称为“GC Roots”的对象作为起点,从这些节点开始通过引用链(Reference Chain)的方式向下搜索。当对象A引用了对象B,那么对象A和对象B之间便有了引用链,如果对象A是“GC Roots”,那么对象B就被称为可达。当对象B引用了对象C,因为B是可达的,所以A通过B也能到达C,对象C也被称作可达。引用链:A -> B -> C

哪些对象可作为GC Roots的对象?

  • 虚拟机栈(栈帧中的本地变量)中引用的对象
  • 方法区(注意:方法区这个概念,在JDK1.8便移除了,改为了元空间)中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中引用的对象

优点:全面的分析对象是否存活

缺点:实现复杂,效率低下

引用

判断对象是否存活,离不开引用。JDK1.2以前,定义引用的方法很纯粹:如果reference类型的数据中存储的数值代表的是另一块内存的起始地址,就称这块内存代表着一个引用。这就导致了引用只有两种状态,有或者无。两个对象之间,存在着引用就是有,不存在引用就是没有,没有就被垃圾收集器给回收。但是我们对于一些“食之无味,弃之可惜”的对象就显得无能为力了。当我们的内存足够时,我们并不想毁灭它,以此来避免后面的重复创建,浪费时间。所以有了以下四种定义

  • 强引用:程序代码中普遍存在,类似“Objec obj = new Object()”,个人理解为,就是GC Roots能可达的对象,这些对象之间的引用链就是强引用,只要强引用在,就永远不会被回收掉。
  • 软引用:SoftReference,描述一些还可能有用,但是非必需的对象。在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。当然,如果这次回收还没有收集足够的空间供新对象使用,才会抛出内存溢出的异常,导致OOM。
  • 弱引用:WeakReference,也是用来描述一些非必需的对象,但是它比软引用更加的弱一些。每一次垃圾回收,不管内存够不够都要回收它们。只能生存在下一次垃圾回收之前。
  • 虚引用:PhantomReference,最弱的引用,一个对象是否有虚引用,完全不会对其生存时间构成影响,唯一的目的是:能在这个对象被收集器回收时收到一个系统通知

回收方法区

即JDK1.8后的元空间,回收对象主要有两部分内容:废弃的常量和无用的类。

判断常量是否废弃比较简单

满足三个条件才算是无用的类:

  • 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例
  • 加载该类的ClassLoader已经被回收
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的对象

垃圾收集算法

标记-清除算法

image-20210604210757546

  • 概述:算法分为两个阶段,首先标记所有需要回收的对象,在标记完成后统一回收所有标记的对象。最基础的收集算法。
  • 缺点:效率不高,空间上会产生大量的空间碎片。如果程序遇到大对象需要分配时,无法找到足够的连续内存而不得不提前触发另一次的垃圾收集动作。

标记-复制算法

image-20210604211847364

  • 概述:把内存分为大小相等的两块,每次只使用其中的一块。当这一块的内存快用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次性的清理掉。现代的商业机都是采用的这种收集算法,但是IBM研究表明,新生代中的对象98%都是“朝生夕死”的,并不需要按1:1来分配内存空间,而是将内存分为一块较大的Eden和两块较小的Survivor1空间,每次使用Eden和其中的一块Survivor空间。当回收时,将Eden和Survivor中还存活的对象一次性的复制到另外一块Survivor上去,最后在清理掉Eden和刚才使用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1。但是可能会出现复制到Survivor过程中,存活的对象太多了,装不下,Survivor空间不够用。所以还需要进行分配担保(Handle Promotion),依赖其他内存来复制(这里指老年代)。
  • 优点:简单高效,不用考虑内存碎片等复杂情况。
  • 缺点:内存利用会减少一半(1:1分配内存空间时)

标记-整理算法

image-20210604211827014

适用于老年代,存活率高的对象。让所有存活的对象向一端移动,然后清理掉端边界以外的内存。

分代收集算法

当前商业虚拟机都是采用的“分代收集”算法,把java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。新生代用复制算法,因为有大量对象死去,老年代使用“标记-清理”或者“标记整理”,因为老年代存活率高。

HotSpot如何实现垃圾回收

枚举根节点

从GC Roots出发,也就是根节点,进行可达性分析,这个过程非常消耗时间。而且可达性分析的话,需要保持“一致性”,一致性是指整个可达性分析期间的执行过程应该看起来是要停留在某个时间点上。不可以出现分析过程中,对象之间的引用还在发生变化。要保证可达性分析结果的正确性,准确性。所以GC会导致系统的“Stop The World”

当然由于枚举根节点消耗大量的时间,所以引入了OopMap这种数据结构。GC时,当执行系统停顿下来时,并不需要一个不漏的把所有执行上下文和全局的引用位置,虚拟机而是通过OopMap直接得知哪些地方存放着对象的引用

安全点

为了配合OopMap,引入了安全点。HotSpot通过OopMap确实可以快速且准确地完成GC Roots的枚举,但是OopMap内容的变化的指令非常的多,如果位每一条指令都生成对应的OopMap,那将会需要大量的额外空间。所以虚拟机只在特定的时间位置,记录了这些引用信息,并没有为每条指令都生成OopMap,这些位置便称作安全(SafePoint),即程序执行时并非能在所有的地方停下来,而是只能在达到安全点时才能暂停。

如何在GC发生时,让所有的线程都“跑”到最近的安全点去?

  • 抢先式中断:强制把所有的线程中断,如果有线程不在安全点上,就恢复线程,让它跑到安全点上,几乎没有虚拟机使用这种方法,
  • 主动式中断:当线程需要中断时,不直接对线程操作,而是通过一个标志,各个线程执行时,主动的去轮询这个标志,需要中断时,改变这个标志。线程在轮询的时候发现中断标志为真,就自己中断挂起。轮询标志的地方和安全点是重合的。

SafePoint很完美吗?不见得,safePoint关注的点是正在运行的线程,那么有些线程处于Sleep状态或者Blocked状态,这时候线程无法响应中断请求。所以引入了安全区域(Safe Region)

安全区域是指:在一段代码中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的。具体描述:在线程执行到safe region中的代码时,首先标识自己已经进入了safe region,那样,当在这段时间里JVM要发起GC时,就不用管标识自己为safe region状态的线程了。在线程要离开safe region时,它要检查系统是否已经完成了根节点枚举(即整个GC过程),如果完成了,那线程就继续执行,否则他就必须等待直到收到可以安全离开safe region的信号为止

垃圾收集器

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。

**并行(Parallel)**:指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待阶段。

**并发(Concurrent)**:指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行在另一个CPU上。

Serial收集器

新生代收集器,单线程收集器。最基本,发展历史最悠久的收集器。特点:在他进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束,Stop The World。优点:简洁又高效。在运行在client模式下的虚拟机来说是一个很好的选择。

ParNew收集器

是serial收集器的多线程版本。它是许多运行在Server模式下的虚拟机中首选的新生代收集器。原因很简单,目前只有它与serial收集器能与CMS收集器配合工作。

Parallel Scavenge收集器

目标是达到一个可控的吞吐量。所谓吞吐量用于运行用户代码的时间与CPU总消耗时间的比值。停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。它也被称为吞吐量优先的收集器。可以设置一个参数,让虚拟机GC自适应的调节。即虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最适合的停顿时间或最大吞吐量。这是它与parnew收集器的一个重要区别。

Serial Old收集器

是serial收集器的老年代版本,同样是一个单线程收集器,使用标记_整理算法。

Parallel Old收集器

是Parallel Scavenge收集器的老年代版本,使用多线程的标记_整理算法。

CMS(Concurrent Mark Sweep)

image-20210604210400117

是一种以获取最短回收停顿时间为目标的收集器。重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。是基于标记-清除算法实现的,它的运作过程:初始标记,并发标记,重新标记,并发清除。第一步和第三步仍然需要Stop The World。

  • 初始标记:仅仅标记一下GC Roots能直接关联到的对象,速度很快。需要STW
  • 并发标记:进行GC Roots Tracing的过程,时间较长
  • 重新标记:则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那些对象的标记记录(增量更新),比初始标记稍微长一些,但远比并发标记的时间短。需STW
  • 并发清除:执行sweep,时间较长

优点:并发收集,低停顿,又称之为并发低停顿收集器。

缺点

  1. CMS对CPU资源非常的敏感,在并发阶段虽然不会导致用户线程停顿,但是会因为占用CPU资源而导致应用程序变慢,总吞吐量会降低。多CPU的时候,可以用回收默认线程数(CPU数量+3)/4。使并发回收垃圾线程所占的CPU资源使用百分比降低。CPU越多,降的多。但是CPU很少的情况下,问题就很大,由此虚拟机提供了i-CMS增量式并发收集器,就是让GC线程,用户线程交替运行,尽量减少GC线程独占CPU的时间,这样会是整个垃圾回收时间增长,但对用户程序的影响就少些。但是效果一般,不提倡使用。
  2. CMS无法处理浮动垃圾,由于在并发清理阶段用户线程还在运行,可能产生新的垃圾,这一部分垃圾出现在重新标记阶段之后。因此需要预留提供一部分空间给并发时的用户线程使用,以供用户程序正常运行,而不能像其他收集器一样,等到老年代几乎被填满了再进行收集。
  3. 是标记-清除算法的缺点,会有大量的空间碎片产生,碎片过多时,将会给大对象带来很大的麻烦,如果没有空间给大对象,就不得不提前触发一次fullGC。为了解决这个问题,可以设置一些参数来优化,如当FullGC执行了多少次的时候,来一次空间碎片压缩,开启内存碎片的整理过程。但是这样使得停顿时间变长了。

G1(Garbage-First)收集器

image-20210604211735296

G1是一款面向服务端应用的垃圾收集器,它的使命是替换掉CMS,弥补CMS空间碎片等缺点。不同于其他的分代回收算法、G1将堆空间划分成了互相独立的区块。每块区域既有可能属于O区、也有可能是Y区,且每类区域空间可以是不连续的(对比CMS的O区和Y区都必须是连续的)。这种将O区划分成多块的理念源于:当并发后台线程寻找可回收的对象时、有些区块包含可回收的对象要比其他区块多很多。虽然在清理这些区块时G1仍然需要暂停应用线程、但可以用相对较少的时间优先回收包含垃圾最多区块。这也是为什么G1命名为Garbage First的原因:第一时间处理垃圾最多的区块。

  • 并行与并发:G1能充分利用多CPU、多核环境的硬件优势,使用多个CPU来减少STW的停顿时间
  • 分代收集:还是与其他收集器一样采用分代收集,不过older与young不再是连续的空间了
  • 空间整合:整体是感觉采用“标记-整理”;局部之间是靠两个region基于“复制算法”;这两种策略所以不会产生空间碎片
  • 可预测停顿:建立可预测的停顿时间模型,能让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集器上的时间不得超过N毫秒,这几乎是实时Java(RTSJ)的垃圾收集器的特征了

平时工作中大多数系统都使用CMS、即使静默升级到JDK7默认仍然采用CMS、那么G1相对于CMS的区别在:

  • G1在压缩空间方面有优势
  • G1通过将内存空间分成区域(Region)的方式避免内存碎片问题
  • Eden, Survivor, Old区不再固定、在内存使用效率上来说更灵活
  • G1可以通过设置预期停顿时间(Pause Time)来控制垃圾收集时间避免应用雪崩现象
  • G1在回收内存后会马上同时做合并空闲内存的工作、而CMS默认是在STW(stop the world)的时候做
  • G1会在Young GC中使用、而CMS只能在O区使用

就目前而言、CMS还是默认首选的GC策略、可能在以下场景下G1更适合:

  • 服务端多核CPU、JVM内存占用较大的应用(至少大于4G)
  • 应用在运行过程中会产生大量内存碎片、需要经常压缩空间
  • 想要更可控、可预期的GC停顿周期;防止高并发下应用雪崩现象

GC模式:G1中提供了三种模式垃圾回收模式,young gc、mixed gc 和 full gc,在不同的条件下被触发。

  • young gc:发生在年轻代的GC算法,一般对象(除了巨型对象)都是在eden region中分配内存,当所有eden region被耗尽无法申请内存时,就会触发一次young gc,这种触发机制和之前的young gc差不多,执行完一次young gc,活跃对象会被拷贝到survivor region或者晋升到old region中,空闲的region会被放入空闲列表中,等待下次被使用。

    参数 含义
    -XX:MaxGCPauseMillis 设置G1收集过程目标时间,默认值200ms
    -XX:G1NewSizePercent 新生代最小值,默认值5%
    -XX:G1MaxNewSizePercent 新生代最大值,默认值60%
  • mixed gc:当越来越多的对象晋升到老年代old region时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即mixed gc,该算法并不是一个old gc,除了回收整个young region,还会回收一部分的old region,这里需要注意:是一部分老年代,而不是全部老年代,可以选择哪些old region进行收集,从而可以对垃圾回收的耗时时间进行控制。

    那么mixed gc什么时候被触发?

    先回顾一下cms的触发机制,如果添加了以下参数:

    1
    `-XX:CMSInitiatingOccupancyFraction=``80` `-XX:+UseCMSInitiatingOccupancyOnly`

    当老年代的使用率达到80%时,就会触发一次cms gc。相对的,mixed gc中也有一个阈值参数 -XX:InitiatingHeapOccupancyPercent,当老年代大小占整个堆大小百分比达到该阈值时,会触发一次mixed gc.

    mixed gc的执行过程有点类似cms,主要分为以下几个步骤:

    1. initial mark: 初始标记过程,整个过程STW,标记了从GC Root可达的对象
    2. concurrent marking: 并发标记过程,整个过程gc collector线程与应用线程可以并行执行,标记出GC Root可达对象衍生出去的存活对象,并收集各个Region的存活对象信息
    3. remark: 最终标记过程,整个过程STW(使用原始快照SATB),标记出那些在并发标记过程中遗漏的,或者内部引用发生变化的对象
    4. clean up: 垃圾清除过程,如果发现一个Region中没有存活对象,则把该Region加入到空闲列表中
  • full gc:如果对象内存分配速度过快,mixed gc来不及回收,导致老年代被填满,就会触发一次full gc,G1的full gc算法就是单线程执行的serial old gc,会导致异常长时间的暂停时间,需要进行不断的调优,尽可能的避免full gc.

低延迟垃圾收集器

  垃圾收集器三个重要的指标:内存占用、吞吐量和延迟。内存占用和吞吐量随着硬件性能的提升,帮助了软件不少,不需要那么关注这两点,随着硬件的提升这两项指标也会随着提升。但是延迟不一样,延迟也就是STW的时间,随着内存条的容量越来越大,Java堆可用的内存也越来越大,意味着需要回收的空间也越来越大,那么STW也就越久。

Shenandoah收集器

是一款非官方的垃圾收集器,是由RedHat公司开发的项目,受到来自Sun公司的排斥,所以在正式商用版的JDK中是不支持这个收集器的,只有在OpenJDK才有。虽然没有拥有正统血脉,但是在代码上它相较于ZGC更像是G1的继承者,在很多阶段与G1高度一致,甚至共用了一部分源码,但相较于G1又有一些改进。最主要有三个改进:

  1. 支持并发标记-整理算法。
  2. 默认不适用分代收集,Shennandoah和G1一样使用Region分区,但是在Shennandoah中并没有Region会去扮演新生代或者老年代
  3. G1中存储引用关系的记忆集占用了大量的内存空间,在Shennandoah改用为连接矩阵,具体可以看P107。

Shennandoah收集工作过程大概可以分为9个步骤:

  1. 初识标记:与G1一样,标记处与GC Roots直接关联的对象,STW。
  2. 并发标记:与G1相同,根据上一步的对象,完整标记出可达对象。
  3. 最终标记:也与G1一样,利用原始快照的方法标记出上个阶段变动的对象,还会在这个阶段统计出回收价值最高的Region,组成一个回收集。
  4. 并发清理:这个阶段会清理整个Region区一个存活对象都没有的区域,所以可以并发进行。
  5. 并发回收:将回收集中存活的对象复制一份到其他未被使用的Region区中。
  6. 初始引用更新:并发回收阶段复制后,还需修正到复制后的新地址,但这个阶段并未做什么具体操作,只是相当于一个集合点,确保并发回收阶段所有线程都完成了自己的复制工作。
  7. 并发引用更新:这个阶段才是真正修正引用的阶段。
  8. 最终引用更新:上一步只是修正了Java堆中对象的引用,还要修正存在于GC Roots的引用,最后一次短暂的暂停,只与GC Roots数量有关。
  9. 并发清理:经过了并发回收的复制和引用修正,会收集中的Region就可以完全清理了。

  再说说Shennandoah的一个特点,也就是前面说到的并发标记-整理算法。整理阶段可以细分为5,6,7,8四个步骤。其最大的一个问题就是,在复制或者在修正引用的时候用户线程可能正在使用这个对象。原来有个解决类似问题的方案,就是保护陷阱,大概过程就是当用户线程访问到对象就地址后,会进入一个异常处理器中,由该处理器转发到新的地址。而在Shennandoah中用的是一种相对更好的方案:转发指针,就是在每个对象前面加个新的引用字段,当不处于并发移动的情况下,该引用指向自己,并发移动了的话就指向新地址。

ZGC收集器

ZGC的目标和Shennandoah相似,都希望在不影响吞吐量的情况下,将停顿时间限制在10毫秒以内。ZGC也是基于Region布局的,还并未支持分代收集,但其Region有大中小三个类型

  1. 小型Region容量固定为2MB,用于放置小于256KB的小对象。
  2. 中型Region固定容量为32MB,用于放置大于等于256KB,小于4MB的对象。
  3. 大型Region容量不固定,但一定是2的整倍数,用于存放大于4MB的对象。

  ZGC在实现并发整理时用到了染色指针,之前的的收集器如果想在对象中额外存储一些信息,大多会在对象头里存储,比如转发指针。再就是之前说到的可达性分析中的三色标记,其只是表达了对象引用的情况,跟对象本身的数据没任何关系,所以染色指针就是把这些标记信息记录在引用对象的指针上。指针为什么还能存储信息呢?这就要说到系统架构了,具体看P114,染色指针只支持64位系统,而AMD64架构中只支持到了52位,而各大操作系统又有自己的限制,染色指针在Linux支持的46位指针宽度中,拿出4位存储这些标记信息,所以使用了ZGC进一步压缩了原本46位的地址空间,从而导致了ZGC能管理的内存不能超过4TB,在今天看来,4TB的内存依旧非常充足。

染色指针的三大优势:

  1. 一旦某个Region的存活对象被移走后,这个Region立即就能被回收重新利用,而Shennandoah需要一个初始引用更新,等待所有线程复制完毕。
  2. 染色指针可以大幅度减少在垃圾收集过程中内存屏障的使用数量(后面过程中的第五步提到),一部分功能就是因为染色指针把信息存储在指针上了,还有一部分原因就是ZGC还并未支持分代收集,所以也不存在跨代引用。
  3. 染色指针在未来可以拓展,记录更多信息,前面说到在64位系统中,Linux只用到了46位,还要18位未被开发。还有一个问题就是染色指针重新定义指针中的几位,操作系统 是否支持,虚拟机也只是一个进程而已,这里就用到了虚拟内存映射,具体看P116。

ZGC工作过程大概可以分为以下几步:

  1. 初始标记:与之前几个收集器一样,找到GC Roots的直接关联对象。
  2. 并发标记:标记出完整的可达对象,与G1和Shennandoah不同的是,它是在指针上做更新而不是对象头。
  3. 最终标记:和Shennandoah一样。
  4. 并发预备重分配:这个阶段需要根据特定的查询条件统计出本次收集过程要清理哪些Region。这里的分配集不是像G1那样按收益优先的回收集,分配集只是决定了里面的对象会被复制到新的Region,这里的Region要被释放了。
  5. 并发重分配:这个过程要把分配集中的对象复制到新的Region中,并为分配集中的每个Region维护一个转发表,得益于染色指针的帮助,可以仅从引用上就可以得知某个对象是否在分配集上,如果在复制时,一个用户线程访问了分配集中的对象就会被内存屏障截获,然后根据转发表将访问转发到新的对象上,并修正这个线程访问该对象的引用,这个过程称为指针的自愈。
  6. 并发重映射:这个阶段要修正整个堆中指向重分配集中旧对象的所有引用。这个阶段比较特殊,因为它不是迫切需要去执行的,上个阶段的自愈过程就是针对某一对象的引用修正,所以即使没有这一步也不会出现问题,只是第一次自愈有个转发过程会稍慢一点,后面也都正常了。正因为这种不迫切,ZGC巧妙的把这步工作合并到了并发标记过程当中,因为并发标记也需要遍历所有对象,这一步也需要修正所有旧对象的引用。

  ZGC的一大问题就是其暂时还没有分代收集,这限制了它能承受的对象分配速率不会太高(ZGC每次都要对一个很大的堆做一次完整的并发收集,在这段收集时间内会创建大量对象,而这些对象大部分都是朝生夕灭的,导致产生大量浮动垃圾)。如果长时间的回收速率比不上分配速率,产生的浮动垃圾越来越多,可分配的空间也越来越小了。所以要从根本上解决这个问题还是要引入分代收集,让新生代专门去存储这些频繁回收创建的对象。

内存分配与回收策略

主要是以下五种策略,目的都是在结合JVM实际情况下,尽可能的提高效率,且做到安全可靠

  • 对象优先在Eden分配:大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC(新生代GC)。
  • 大对象直接进入老年代:大对象对虚拟机来说是一个很坏的消息,经常出现大对象会容易导致内存还有不少空间,但是满足不了这个大对象,所以提前触发垃圾收集以获取足够的空间来容纳安置它们。可以开启一个参数,是大对象直接进入老年代,在老年代分配,这样做的目的是避免在Eden区以及两个Survivor区之间大量的内存复制(新生代采用的是复制算法)。
  • 长期存活的对象将进入老年代:给对象加一个年龄计数器,每熬过一次Minor GC后仍然存活,并且能够被Survivor容纳的话,对象年龄就加一。可以设置一个参数,当年龄足够就晋升老年代之中去。
  • 动态对象年龄判定:并不一定要求,所有对象的年龄必须要达到某一个年龄值,如果Survivor空间中相同年龄所有对象的大小的总和大于Survivor空间的一半,年龄大于等于该年龄对象就可以直接进入老年代,无须一定要达到某个年龄才能进去。
  • 空间分配担保:发生在Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,那么会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么就会继续检查老年代的最大可连续空间是否大于历次晋升到老年代对象的平均大小,如果大于就尝试进行一次Minor GC,虽然有风险。如果小于就进行Full GC,来回收老年代的空间。圈子虽然绕的大,但是可以避免频繁的FULL GC。