Java和C++之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,墙外面的人想进去,墙里面的人却想出来。
对Java程序员来说,JVM有自动内存管理机制,不需要自己手动进行内存分配和回收,也不容易出现内存泄露和内存溢出的问题。那么JVM是怎么对内存进行管理的呢?JVM制定了一些内存分配策略对内存进行合理的分配,使用垃圾收集器对回收内存。Java程序员不需要关心内存的分配和回收是怎么进行的,极大地简化了编程。那我们为什么还需要去了解垃圾收集(GC)和内存分配呢?和了解JVM内存结构是一样的,当需要排查内存溢出、内存泄露问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,就需要对这些“自动化”的技术实施必要的监控和调节。
下面讲解JVM的垃圾收集器和内存分配策略,以及垃圾收集机制为了避免内存溢出异常的出现都做了哪些努力。
1.概述
在使用垃圾收集器对垃圾进行回收时,需要明确3件事情:
<1>什么样的内存才能算得上是垃圾,需要被回收?
<2>什么时候对内存进行回收?
<3>如何回收?
带着这些问题进行学习。
在JVM内存结构时有讲过Java运行时内存区域分为程序计数器,虚拟机栈,本地方法栈,堆区,方法区等区域。其中程序计数器,虚拟机栈,本地方法栈这三个区域是线程私有的,随线程而生,随线程而灭。当方法结束或线程结束时,内存自然就跟着回收了。因此这几个区域的内存分配和回收具有确定性,在这几个区域就不需要过多的考虑内存回收问题。
Java堆区和方法区是线程共享的,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,只有在程序运行期间才能知道会创建哪些对象,这部分的内存分配和回收是动态的,GC所关注的也是这部分内存。因此通常的内存分配和回收中的“内存”都是指的堆区和方法区的内存。
2.对哪些内存进行回收
在堆中存放着几乎所有的对象实例,垃圾收集器在对堆内存进行回收前,需要先判断哪些对象才是需要被回收的。如果对象还“存活”(这个对象还正在被使用),就不用被回收;如果对象已经“死去”(这个对象已经没有任何途径来使用它),这个对象就需要被回收。那么问题就转化成怎样判断对象是否存活的问题。
(1)引用计数法
该方法就是给对象添加一个引用计数器,每当有一个地方引用它,计数器的值就加1;引用失效时,计数器的值就减1。任何时刻计数器为0的对象就是不再被使用的。
引用计数法(Reference Counting)的实现简单,判定效率也很高,大部分情况下是个不错的算法。但是,主流的Java虚拟机却没有选用引用计数法来管理内存,最主要的原因是:虽然任何时刻计数器为0的对象就不再被使用的判定是对的,但是计数器不为0的对象就一定是正在使用的对象吗?不一定。这种情况就是对象之间相互循环引用的情况。因此,使用引用计数法判定对象是否存活,很难解决对象之间互相循环引用的问题。
对象相互引用,举个例子:
假设对象objA和objB都有一个Object类型的变量instance,赋值令objA.instance = objB,objB.instance = objA。除此之外,这两个对象再无任何引用,objA = null,objB = null。那么此时这两个对象已经不可能再被访问,但是他们因为互相引用着对方,导致他们的引用计数器都不为0。在最后调用System.gc( )触发垃圾收集时,引用计数算法也无法通知GC收集器回收它们。
实际上,如果调用System.gc( )触发垃圾收集,objA和objB都是可以被回收的。虚拟机并不因为这两个对象互相引用就不回收他们,这也从侧面说明虚拟机并不是通过引用计数算法来判断对象是否是存活,从而进行垃圾收集的。
那虚拟机是使用什么方法判断对象的存活呢?
(2)可达性分析算法
主流的商用程序语言中都是通过可达性分析(Reachability Analysis)来判定对象是否存活的。该算法的基本思路:通过一系列的称为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(就是GC Roots到这个对象不可达)时,就证明此对象是不可用的。如图,对象object5,object6,object7虽然互相有关联,但是他们到GC Roots是不可达的,所以他们就被判定为是可回收的对象。
那么在Java语言中,什么样的对象可以作为GC Roots呢?可作为GC Roots的对象主要有以下几种。
可作为GC Roots的节点主要在全局性的引用(例如常量和类静态属性)和执行上下文(例如栈帧中的本地变量表)中。GC Roots如:
<1>虚拟机栈(栈帧中的本地变量表)中引用的对象。
<2>方法区中静态类属性引用的对象。
<3>方法区中常量引用的对象。
<4>本地方法栈中JNI(即一般说的Native方法)引用的对象。
(3)引用类型
上面提到的引用计数算法和可达性分析算法,都是通过有没有引用对象判断该对象是否会被垃圾收集器回收。早些年代的引用关系只能定义对象被引用或者不被引用两种状态。如果我们希望有这样一种对象:当内存空间还足够时,就把它保存在内存中;当进行完垃圾收集内存空间还是不足时,就可以抛弃这些对象。那么只有两种状态的“被引用”或“不被引用”的引用状态,就无法描述这类对象。
为此,在Java的后续发展过程中,又对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)四种类型,这四种引用的强度依次递减。
强引用是对对象实例的引用,只要强引用还在,垃圾收集器就都不会回收掉被引用的对象。
软引用用来描述一些还有用但并非必须的对象。如果这一次发生垃圾回收,当垃圾收集之后如果有足够的内存,软引用就不会被回收。如果这次回收没有足够的内存,会把对象放入回收范围中,这次不回收,等待第二次回收。
如果第二次回收没有足够的内存,就会回收;有足够的内存就不会回收。就是进行回收时会有两次机会可以不被回收。内存的二次回收具体见下面(4)生存还是死亡。
弱引用也是用来描述非必须对象的。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉被弱引用关联的对象。被弱引用关联的对象只能存活到下一次垃圾收集之前。也就是如果这一次发生垃圾回收,弱引用对象肯定会被回收。
虚引用也成为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,不会对其生存时间构成影响,也无法通过虚引用取得对象实例。一个对象设置虚引用的唯一目的就是能在这个对象被垃圾收集时收到一个系统通知。
(4)生存还是死亡
即使在可达性分析算法中的不可达对象,也并非是“非死不可”的。要宣告一个对象真正死亡,需要经历两次标记的过程:如果对象在进行可达性分析后发现没有与GC Roots相关联的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize( )方法。当对象没有覆盖finalize( )方法,或者finalize( )方法已经被虚拟机调用过,虚拟机将这两种情况都视为没有必要执行。如果一个对象没有必要执行finalize( )方法,那么在对象第一次进行GC时就会被回收。
如果对象被判定为有必要执行finalize( )方法,该对象就会被放在一个就做F-Queue的队列中,并在稍后由一个由虚拟机自动建立的、低优先级的终结器(Finalizer)线程去执行它。finalize( )方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象在finalize( )方法中成功拯救自己:重新和引用链的任何一个对象建立关联,比如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移出“即将回收”的集合;如果这个时候对象还没有逃脱,还没有被其他的对象引用,那就只能被回收了。从这里我们也可以看出,如果一个对象执行了finalize( )方法,那它也有可能仍然是存活的。
因此对象在被GC时可以使用finalize( )方法自我拯救,但是这种自救的机会只有一次,因为一个对象的finalize( )方法最多只能被系统自动调用一次。
但是不建议使用finalize( )方法来拯救对象,因为它的运行代价高昂,不确定性大。finalize( )能做的所有工作,使用try-catch或其他方式都可以做的更好,更及时。因此建议在Java语言中忽略这个方法的存在。
(5)方法区中的内存回收
Java虚拟机规范可以不要求虚拟机在方法区实现垃圾收集,而且在方法区进行垃圾收集的效率很低。因此很多人认为方法区是没有垃圾收集的。
方法区的垃圾收集主要分为两部分:废弃常量和无用的类。回收废弃常量和回收Java堆中的对象类似。如果没有任何对象引用常量池中的一个字符串常量,那么有必要的话,这个字符串常量就会被清除出常量池,在进行垃圾收集时会被回收。量池中的其他类(接口)、方法、字段的符号引用也与此类似。
回收无用的类,需要先判断要回收的类是否是无用的,然后再对满足条件的无用类进行回收。
对无用的类的判断依据:
<1>Java堆中不存在该类的任何对象实例,该类的所有实例都已经被回收。
<2>加载该类的ClassLoader已经被回收。
<3>该类的对应的Class对象没有被引用,无法在任何地方通过反射访问该类的方法。
3.垃圾收集算法
垃圾收集有几种对应的垃圾收集算法,下面介绍几种常用的算法。
(1)标记-清除算法
标记-清除(Mark-Sweep)算法的思想:该算法对内存的回收分为“标记”和“清除”两个阶段:首先标记出要回收的对象,标记完成后统一回收被标记的对象,该算法的执行过程如图:
该算法是最基础的内存收集算法,后续的收集算法都是基于这种思路并对其不足进行改进而得到的。该算法的不足:
<1>效率问题:标记和清除两个过程的效率都不高。
<2>空间问题:标记清除之后会产生大量不连续的内存碎片,不利于以后大对象的分配。
(2)复制算法
复制(Coping)算法是为了解决标记-清除算法的效率问题而提出的。复制算法的思想:复制算法把可用内存按容量分为大小相等的两块,每次只使用其中的一块。当其中的一块内存使用完毕,将存活的对象复制到另一块没有使用的内存区域,然后再把已使用过的内存一次清理掉。复制算法的执行过程如图:
该算法的优点是:每次对内存的半个区域进行回收,内存分配时不用考虑内存碎片等复杂情况。缺点是可用内存减少到了原来的一半,内存回收的代价高昂。
(3)标记-整理算法
复制算法在对象存活率较高时就要进行较多的复制操作,效率就会变低。如果要回收的内存区域对象的存活率不是很低,可以使用标记-整理(Mark-Compact)方法进行内存回收。
标记-整理算法思想:内存回收分为两步,标记和整理。标记过程和标记-清除算法的标记过程一样,标记出需要被回收的对象。但后续过程不是直接对可回收对象进行清理,而是让所有的存活对象都向一端移动,然后直接清理边界以外的内存。标记-整理算法的执行过程如图:
(4)分代收集算法
现在商业虚拟机的垃圾收集都采用“分代收集”(Generational Collection)算法。JVM中,内存是按照分代进行组织的。该算法的思想:把Java堆根据对象存活周期的不同划分为新生代和老年代,这样就可以根据各个年代的特点对不同的年代采用最恰当的收集算法。
在新生代中,每次垃圾收集都有大量对象死去,只有少量对象存活,那就选择复制算法。采用复制算法只需付出少量存活对象的复制成本就可以完成收集。
研究表明,新生代中的对象98%都是“朝生夕死”的,所以采用复制算法对新生代的对象进行收集,并不需要按照1:1的比例来划分内存空间,可以把内存分为一块较大的Eden空间和两块较小的Survivor空间,他们之间的比例为8:1:1,每次使用Eden和其中的一块Survivor。当回收时,将Eden和Survivor中还存活的对象一次性复制到另外一块还没使用的Survivor空间上,最后清理掉Eden和刚才使用过的Survivor空间。这样每次新生代中可用内存空间为整个新生代容量和90%(80%+10%),只有10%的内存没有被使用。
但是,98%的对象可回收只是通常的情况,我们没有办法保证每次回收都有不多于10%的对象存活。那如果内存回收的时候有超过10%的对象存活,我们该怎么办呢?
当内存回收之后,有较多的对象存活,导致Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行空间分配担保。
内存的空间分配担保就是:如果空闲的Survivor空间不够存放在新生代的内存回收留下来的存活对象时,这些对象将直接通过分配担保机制进入老年代(前提是老年代也要有足够的内存)。关于对新生代进行分配担保的更多内容,在下面的“空间分配担保”中会有所提及。
而老年代中因为对象存活率高,没有额外空间对它进行空间分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。
在堆中,尤其是在新生代中,常规应用进行一次垃圾收集一般可以回收70%到95%的空间,而永久代的垃圾收集效率远低于此。
需要注意的是,垃圾收集是根据根节点到其他节点的可达性分析判断对象是否是要被回收的。那在进行垃圾收集的过程中,不能存在对象引用的关系还在不断变化的情况。如果这点不满足的话,根节点引用关系的准确性就无法得到保证。我们必须在分析根节点的引用关系的时候确保整个执行系统的引用关系是不变的。因此执行GC时必须停顿所有的Java执行线程(GC时停顿执行线程的事件被称为“Stop The World”)。即使需要的停顿时间很少或停顿的频率很低,在GC时也必须要进行停顿。
那JVM是如何对内存进行回收的呢?JVM使用垃圾收集器实现了具体的内存回收。
4.垃圾收集器
虚拟机中有多种垃圾收集器,HotSpot虚拟机的垃圾收集器如下图。如果垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现,垃圾收集器中使用了垃圾收集算法。
上图展示了7种作用于新生代和老年代的收集器。如果两个收集器之间存在连线,说明他们可以搭配使用。上面的收集器是针对新生代的收集器,下面的收集器是针对老年代的收集器。下面分别介绍这些收集器的特性、基本原理和使用场景。
另外,对这些收集器来说,没有最好,只有哪个收集器放在哪些场景下最适合使用。
(1)Serial收集器
Serial收集器是一个单线程的新生代收集器,采用复制算法。它在进行垃圾收集时,必须暂停其他的工作线程,直到收集结束。
它的特点是简单高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,收集器由于没有线程交互的开销,可以获得最高的单线程收集效率。Serial收集器对于运行在Client模式下的虚拟机来说是一个很好的选择。
但是在进行垃圾收集时需要暂停其他所有的工作线程,由JVM自动发起垃圾收集,在用户不可见的情况下把用户正常工作的线程停掉,给用户带来了不好的用户体验。
垃圾收集和打扫房间是一个性质的,你妈妈一边在帮你打扫房间的时候不可能让你一边往地上乱扔纸屑。不过实际上垃圾收集比打扫房间要复杂得多。
(2)ParNew收集器
ParNew收集器是Serial收集器的多线程版本,使用多条线程进行垃圾收集,采用复制算法。它是运行在Server模式下的虚拟机中首选的新生代收集器,其中一个原因是,除了Serial收集器,目前只有它能和CMS收集器配合使用。
CMS收集器是HotSpot虚拟机上第一款真正意义上的并发(Concurrent)收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。用上面的例子来说,就是做到了你妈妈在帮你打扫房间的时候还能让你一边往地上扔纸屑。
但是ParNew收集器在单CPU环境中没有展现比Serial收集器更好的效果。
(3)Parallel Scavenge收集器
Paraller Scavenge收集器也是一个新生代收集器,使用复制算法,是一个并行的多线程收集器。
Parellel Scavenge收集器的特点是它的关注点和其它收集器不同,CMS等收集器尽可能的缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。吞吐量是CPU用于运行用户代码的时间与CPU总消耗时间的比值。吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。比如虚拟机运行了100分钟,垃圾收集花掉1分钟,那吞吐量就是99%
停顿时间越短越适合需要与用户交互的程序,良好的响应速度能提升用户的体验;而高吞吐量可以更高效的利用CPU时间,尽快完成用户的任务,主要适合在后台做运算而不需要太多交互的任务。
由于Parellel Scavenge收集器与吞吐量关系密切,该收集器也经常被成为“吞吐量优先”收集器。
Parallel Scavenge收集器有一种称为GC自适应的调节策略:虚拟机根据当前系统的运行情况收集性能监控信息,动态调整和垃圾收集器有关的参数以提供最合适的停顿时间或最大的吞吐量。自适应策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别。
(4)Serial Old收集器
Serial Old收集器是Serial收集器的老年版本,是一个单线程收集器,采用标记-整理算法。这个收集器主要作用是给Client模式下的虚拟机使用。
(5)Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法。
在注重吞吐量以及CPU资源敏感的场合,可以优先考虑Parallel Scavenge加Parallel Old收集器。
(6)CMS收集器
CMS(Concurrent Mark Sweep ) 收集器是一种以获取最短回收停顿时间为目标的收集器。对重视服务的响应速度,希望系统停顿时间最短,给用户带来较好的体验的一些应用,CMS收集器就非常符合这类应用的需求。
CMS(Current Mark Sweep ) 收集器采用标记-清除算法。它的垃圾收集过程分为四个步骤:
<1>初始标记(CMS initial mark)
<2>并发标记(CMS concurrent mark)
<3>重新标记(CMS remark)
<4>并发清除(CMS concurrent sweep)
其中,初始标记和重新标记两个步骤仍然需要暂停其它线程(Stop The World)。虽然在CMS收集器中几乎不会发生停顿,但是在进行GC时初始标记和重新标记阶段也是必须要停顿的。
初始标记仅仅是标记一下GC Roots能直接关联到的对象,速度很快。并发标记阶段就是进行GC Roots Tracing的过程。而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段一般比初始标记阶段稍长一些,但远比并发标记的时间短。
由于整个过程中耗时最长的并发标记和并发清理过程收集器都可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用于线程一起并发执行的。CMS收集器的运行过程如下图:
CMS收集器的优点:
并发收集,低停顿,因此CMS收集器也叫并发低停顿收集器(Concurrent Low Pause Collector)。
CMS收集器的缺点:
<1>对cpu资源敏感,也就是会占用较多的cpu资源。面向并发设计的程序都对CPU资源比较敏感,虽然并发阶段不会导致应用线程停顿,但是会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。
<2>无法处理浮动垃圾。由于垃圾收集是和用户线程并发执行,所以并发时用户线程产生的的新垃圾(浮动垃圾)无法收集。
<3>CMS是基于标记-清除算法,收集结束的时候会产生大量空间碎片,空间碎片过多时,将会给大对象的分配带来很大的麻烦。往往会出现老年代有很大剩余空间,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。
(7)G1收集器
G1(Garbage First)收集器是收集器理论进一步发展的产物,是当今收集器技术发展的前沿成果之一。从实验版本到商用版本,不断从成熟走向完善。它是一款面向服务端应用的垃圾收集器。
<1>G1收集器可以实现在基本不牺牲吞吐量的前提下完成低停顿的内存回收,避免全区域的垃圾收集。
<2>G1收集器充分利用多CPU、多核环境下的硬件优势,使用多个cpu来缩短STW(Stop The World )的停顿时间。
分代收集。
另外,G1收集器与CMS收集器相比还有两个重要的改进:
<1>G1收集器基于标记-整理算法,不会产生内存空间碎片,这对长时间运行的应用程序来说非常重要。分配大对象时不会因为无法找到足够大的连续空间而提前触发一次Full GC。
<2>可以非常精确的控制停顿,建立可预测的停顿时间模型。能够让使用者指定在一个长度为M毫秒时间段内,消耗在垃圾收集上的时间不会超过N毫秒。
G1收集器的执行流程如下所示:
它的垃圾收集过程分为四个步骤:
<1>初始标记(Initial Marking)
<2>并发标记(Concurrent marking)
<3>最终标记(Final Marking)
<4>筛选回收(Live Data Counting and Evacuation)
其中,初始标记,并发标记和CMS收集器的初始标记和并发标记类似。初始标记用于标记GC Roots能直接关联到的对象,初始标记需要暂停其它线程(Stop The World)。并发标记就是从GC Roots开始对堆对象进行可达性分析,找出存活的对象,这个阶段比较耗时,因此可以和用户线程并发执行。
而最终标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段需要停顿线程,但是所有的最终标记线程可并行执行。
筛选回收对内存的回收价值和成本进行排序,根据用户期望的GC停顿时间定制回收计划。
5.内存分配和回收策略
Java技术体系中所提倡的自动内存管理最终可以归结为自动化地解决了两个问题:给对象分配内存以及回收分配给对象的内存。
对象的内存分配,就是在堆上分配,对象主要分配在新生代的Eden区上,少数情况下也可能会直接分配在老年代中。
接下来讲解几条最普遍的内存分配原则。
GC种类
Minor GC:发生在新生代的垃圾收集动作,Java对象大多都具有朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快,回收的对象也很多。
Major GC:发生在老年代的GC,用于回收老年代,出现Major GC通常会出现至少一次Minor GC(并非绝对)。Major GC的回收速度比较慢,一般会比Minor GC慢10倍以上。
Full GC:会收集整个堆,包括yong gen(新生代),old gen(老年代)和perm gen(永久代:方法区),是针对整个新生代、老年代、元空间(metaspace,java8以上版本取代perm gen)的全局范围的GC。Full GC不等于Major GC,也不等于Minor GC+Major GC,发生Full GC需要看使用了什么垃圾收集器组合,才能解释是什么样的垃圾回收。
(1)对象优先在Eden分配
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC。
(2)大对象直接进入老年代
所谓大对象就是指,需要大量连续内存空间的Java对象,最典型的就是那种很长的字符串及数组。
经常出现大对象,由于大对象需要连续的空间,容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来“安置”他们。
通过-XX:PretenureSizeThreshold参数,令大于这个参数值的对象直接分配在老年代分配。
(3)长期存活的对象将进入老年代
虚拟机给每个对象定义了一个对象(Age)年龄计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,对象年龄设为1。对象在Survivor去每“熬过”一次Minor GC,还能继续在存活的话,年龄就增加一岁。当年龄增加到一定程度时(默认为15),就会被晋升到老年代中。
对象晋升老年代的阈值,可以通过参数-XX:MaxTenuringThreshold设置。比如当MaxTenuringThreshold = 1时,对象在第二次GC发生时就进入老年代。
(4)对象动态年龄判定
为了能更好的适应不同程序的运行状况,虚拟机并不是永远的要求对象的年龄必须到达MaxTenuringThreshold才能晋升老年代。如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间一半,年龄大于或等于该年龄的对象可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。
(5)空间分配担保
在发生Minor GC之前,虚拟机检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。
如果条件不成立,则查看是否允许担保失败(Handle Promotion Failure)。如果允许,那么会继续检查老年代的最大可用的连续空间是否大于历次晋升老年代对象的平均大小(将之前的作为经验值),如果大于,将尝试着进行一次Minor GC,这里执行Minor GC是有风险的。如果小于,就进行Full GC。条件不允许的话进行Full GC。
为什么条件不成立,允许担保失败时执行Minor GC是有风险的呢?
新生代使用复制收集算法。为了新生代的内存利用率,只使用其中一个Survivor空间来作为轮换备份。当出现大量对象在Minor GC之后仍然存活的情况(极端的情况就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代,与生活中的贷款担保类似。老年区要进行这样的担保,前提条件是老年代本身有容纳这些对象的剩余空间,如果有的话就让对象直接进入到老年区,担保成功,只执行一次Minor GC。如果老年代没有足够的空间,仍然会出现担保失败的情况,还需要执行一次Full GC。如果允许担保失败,那么就有可能出现担保失败的情况,因此这种情况下执行Minor GC是有风险的。
(6)总结:GC触发条件
Minor GC(ygc):
<1>当Eden空间不足的时候会触发ygc。
Full GC:
<1>创建大对象时,Eden区空间不足,会尝试分配到老年代,如果老年代空间也不足就会触发Full GC。
<2>触发yong gc的时候,会检查老年代的最大连续空间是否大于新生代所有对象总空间。如果不大于并且不允许担保失败,进行Full GC。如果允许担保失败并且老年代的最大可用连续空间小于晋升对象的平均大小,进行Full GC。如果允许担保失败并且老年代的最大可用连续空间大于晋升对象的平均大小,进行Minor GC。
<3>当系统要加载的类,反射的类和调用的方法较多,并且永久代空间不足的话,进行Full GC。
<4>当To Survivor区的空间不足的时候,会把对象复制到老年代,如果老年代空间不足会进行Full GC。