深入理解JVM总结-垃圾收集器与内存分配策略

来源:互联网 发布:2站域名www.tt69.com 编辑:程序博客网 时间:2024/05/16 06:39

在面试时,总是会问到JVM的GC机制等问题,这一块算是十分重要的了。

GC(garbage collection),垃圾收集器。

对于GC,我们需要考虑的是以下三点:

①哪些内存需要回收;②什么时候回收;③如何回收。

为何要考虑垃圾回收机制呢?

当需要排查各种内存溢出、内存泄漏时,当垃圾收集成为系统达到高并发量的瓶颈时,就需要对这些“自动化”技术实施必要的监控和调节。

哪些内存需要回收

从写的另一篇博文 深入理解JVM总结-JDK各版本、JVC内存分配及溢出异常已知,内存运行时,程序计数器、本地方法栈和虚拟机栈是随着线程的产生而产生,随着线程的消亡而消亡的。栈中的栈帧是随着方法的进出而入栈出栈的,它们的内存分配是在类结构确定下来时就已经分配好了的。因此这几个地方的分配和回收是确定好了的,不必考虑其回收的问题,因为方法结束或线程结束时,内存就紧跟着回收了

Java堆和方法区不一样。一个接口中多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在运行期间才知道会创建哪些对象。这部分的内存分配是动态的,JVM的GC机制主要针对的就是这部分动态内存分配区域的垃圾回收

什么时候回收

---->回收Java堆

由于Java堆中存放的是Java的对象实例,因此,在回收之前,必须得确定哪些对象是“存活的”,哪些对象是“死亡的”。而我们要回收的,就是那些“死亡的”对象。

判断的方法有两种:引用计数算法,可达性分析算法。

①引用计数算法

给对象一个引用计数器,引用一次计数器加1,失效时,计数器减1;任何时刻引用计数器的值为0,则该对象就不会被使用,进入“消亡”状态,就可以被回收了。

它的实现很简单,判定的效率也很高。但缺点在于很难解决对象之间相互循环引用而无法回收的问题

例如: objA=objB; objB=objA;  objA=null;objB=null; 除此之外,无任何对象的引用。实际上这两个对象不可能再被访问了,但是由于相互引用了对方,导致引用计数器不为1,gc无法回收它们。

②可达性分析算法(Java/C#/Lisp)

基于对象相互引用而无法回收的问题,产生了可达性分析算法


通过GC Roots作为起点开始往下搜索,所走过的路径为引用链。当一个对象到GC Roots没有任何引用链时,则该对象不可达,即该对象是不可用的,那么就将其判定为可回收的对象

其中,可作为GC Roots的对象包括四种:a.虚拟机栈(栈帧中的本地变量表)中的引用对象;

  b.方法区中的静态属性引用的对象;

  c.方法区中常量引用的对象;

  d.本地方法栈中JNI(即一般的native方法)引用的对象。

========================补充:引用=======================================

引用的原始定义:如果引用类型的数据中存储的是另一个内存的起始地址,就成为这个内存代表着一个引用。但太过狭隘。希望这样描述对象:当内存空间还足够时,则能保留在内存之中,如果内存空间在进行垃圾收集后还很紧张,则可以抛弃这些对象。

JDK1.2以后,引用分为强引用、软引用、弱引用、虚引用四种

强引用:普遍存在,类似Object obj=new Object();只要强引用还在,GC就永远不会回收掉被引用的对象

软引用:还有用但非必需的对象。在系统发生内存溢出异常之前将这些对象列入回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。JDK1.2之后,提供了SoftReference类来实现软引用

弱引用:描述非必需对象,但比软引用更弱一些。被弱引用关联的对象只能生存到下一次GC发生之前。GC工作时,无论内存是否足够,都会回收掉只被弱引用关联的对象。提供了WeakReference类来实现。

虚引用:幽灵引用或幻影引用。最弱的一种关系引用。一个对象是否有虚引用的存在完全不会对其生存时间构成影响,也无法提供虚引用来取得一个对象实例虚引用的唯一目的是能在对象被GC回收时可以收到一个系统通知,提供了PhantomReference来实现

=================================================================================

不可达对象是否非死不可?

不是。要真正“死亡”,至少得被标记两次:如果对象在进行可达性分析之后没有与GC Roots相连接的引用链,那么将会第一次被标记且进行一次筛选,筛选条件是该对象是否有必要执行finalize()方法(没有覆盖finalize()方法或者该方法已被调用,就没有必要执行)。如果有必要执行则将会放在一个F-Queue的队列之中,被JVM自动建立、低优先级的Finalizer线程去执行它,但JVM只是触发这个方法,并不会等待它运行结束(防止死循环等导致GC系统崩溃)。如果在这个过程中对象重新和引用链上任意一个对象建立了关联(如把自己的this赋值给了某个类变量或对象的成员变量),那么第二次将会被移除即将回收的集合。若没有逃脱,那基本上就被回收了。

****对象可以在被GC时自我拯救,这种机会只有一次,因为一个对象的finalize()最多只能被系统自动调用一次***

不建议使用finalize来拯救对象,运行代价高昂,不确定性大,无法保证各个对象的调用顺序。

---->回收方法区

方法区(HotSpot虚拟机中永久代)可以不要求垃圾回收,因为效率很低,也很复杂。

永久代垃圾回收主要为废弃的常量和无用的类。

回收废旧常量和Java堆类似。没有引用常量且发送了内存回收,有必要的话就将之从常量池清除。

无用的类的判定很苛刻。需要满足:a.该类所有实例都已经被回收,即Java堆中不存在该类的任何实例;b.加载该类的ClassLoader已经被回收;c.该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类方法。

满足条件的可以被回收,但不是必然。JVM提供了参数的控制来进行。

在大量使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载功能,以保证永久代不会溢出。

如何回收

运用各种垃圾收集算法。

标记清除算法(Mark-Sweep):分为标记和清除两个阶段:先标记出需要回收的对象,在标记完成后统一回收所有被标记的对象。

不足之处:效率问题,标记和清除效率都不高。空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾回收动作。


复制算法:将可用内存划分为大小相等的两块,每次只使用其中的一块。当这块用完了,就将还存货的复制到另一块上,然后将这一块一次性清除。运行还算高效,但不足之处是将内存缩小为原来的一半,代价太高。用空间换时间。


商业虚拟机都是采用该方法来回收新生代,因为新生代98%都是朝生夕死的。较大Eden和两个较小的survivor空间。每次使用其中一块Eden和survivor,回收时将存活的放入另一块survivor中,再清理掉之前的。默认比例8:1:1。survivor空间不够时,需要依赖其他内存(老年代)进行分配担保,即让对象进入老年代。

③标记整理算法:复制在对象存活率较高时效率很低。根据老年代的特点提出该算法。标记过程同标记清除一样,但不是直接对可回收对象进行清理,而是让存活对象朝着一端移动,然后直接清理掉端边界外的内存。


分代收集算法:当前商业虚拟机都采用该算法。

将Java堆分为新生代和老年代,永久代是方法区。在新生代中,对象有大量死去少量存活,就选用复制方法,而老年代中因为存活率高且没有额外空间对它进行分配担保,就必须使用“标记清除”或“标记整理”来进行回收。

HotSpot算法实现

1.枚举根结点

GC Roots在全局性引用(常量或类静态属性)和执行上下文(栈帧的本地变量表)中,如果太多的话不可能一一进行检查,太消耗时间。同时,GC检查时会出现GC停顿,即可达性分析工作必须在一个能确保一致性的快照中进行,此时在整个分析期间整个执行系统仿佛被冻结,对象的引用关系不会出现变化,否则分析结果无法保证。即GC进行时必须停顿所有的Java执行线程。目前虚拟机使用的都是准确式GC(虚拟机自己知道内存中某个位置的具体数据是什么类型,即知道哪些地方存放着对象引用)。在类加载完成时,使用OopMap数据结构(OOP,普通对象指针)来进行查看对象的存放地址。GC扫描时就可以直接得到信息。

2.安全点

GC Roots枚举的问题:可能导致引用关系变化,或者说OopMap内容变化的指令非常多。如果每一条指定都生成OopMap,那将会需要大量的额外空间,GC的空间成本将会变很高

安全点:没有每条指令都生成OopMap,只在特定位置记录了信息,即程序执行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停。由于安全点的选定既不能太少以致于让GC等待时间太长,也不能太过于频繁以致于过分增大运行时的负荷。因此,安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”来选定的长时间运行即指令序列复用,例如方法调用、循环跳转、异常跳转等

另一个问题是如何在GC发生时让所有线程(不包括执行JNI调用的线程)都到最近的安全点上再停顿下来

有两种方式:抢先式中断和主动式中断。

抢先式中断即不需要线程的先执行代码主动配合,而是在GC发生时,先全部中断,然后发现有线程中断的不在安全点上,就恢复线程,让其跑到安全点上再停顿。(几乎没用这种方式。)

主动式中断当GC需要中断时,不直接对线程操作,仅仅简单设置一个标志,各线程主动去轮询这个标志,发现中断标志时就自己中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方

3.安全区域

安全点机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的安全点。但是对于不执行(即没有分配CPU时间)的程序,如线程处于sleep或Blocked状态,线程就无法响应JVM的中断请求,走到安全点去挂起。JVM也不太可能等待线程重新被分配CPU时间。这时就需要安全区域来解决了。

安全区域即在一段代码片段中,引用关系不会发生变化。在这个区域任意地方开始GC都是安全的。可以看作是被扩展了的安全点

线程到达安全区域时,先标识自己进入安全区域。当这段时间内JVM要发起GC时,就不用管标识安全区域状态的线程了。当线程要离开安全区域时,先检查系统是否完成了根结点枚举或整个GC过程,完成了就继续执行,没有就等待直到收到可以安全离开安全区域的信号为止。

垃圾收集器

垃圾收集器是垃圾收集的具体实现。

以下为JDK1.7 Update14之后的HotSpot虚拟机的所有收集器、


上面为年轻代,下面为年老代。有连线的可以搭配使用。G1收集器仍处于实验状态。

1.Serial收集器

最悠久,最基本的收集器,JDK1.3.1之前唯一一个。单线程收集器只使用一个CPU或一条收集线程去完成垃圾收集工作,且在收集时,必须暂停其他所有的工作线程,直到收集结束。新生代采取复制算法。老年代采取标记整理算。Stop the world

依然是虚拟机运行在Client模式下的默认新生代收集器。简单而高效(与其他收集器单线程相比),对于单个CPU环境来说,Serial收集器由于没有线程交互的开销,专注于垃圾回收,因此能获得最高的单线程收集效率。收集一两百兆的新生代,停顿时间完全可以控制在200ms以内。越少越小。
2.ParNew收集器

Serial收集器的多线程版。多条线程进行垃圾收集。其余和Serial收集器一样。

是许多运行在Server模式下的虚拟机中首选的新生代收集器,因为除了Serial以外只有它能与CMS收集器配合工作

JDK1.5中使用CMS收集器,Concurrent Mark Sweep,真正意义上的并发收集器,第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。它无法和JDK1.4中的新生代收集器Parallel Scavenge配合工作。

ParNew在单线程会存在线程交互的开销,效果并不能比Serial好。但随着CPU的数量增加,它对于GC时系统资源的有效利用还是很有好处的。默认开启的收集线程数与CPU数量相同。可以使用-XX:ParallerGCThreads来限制垃圾收集的线程数。

=============================补充:并行与并发(超级重要,理解透彻)=====================

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

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

 并发和并行都是多个线程,并发的意思是我一个cpu进行轮转执行你的多个线程,并行是n个cpu分别处理n个线程 。
若对于并行来说,如果cpu退化到了只有一个,那么就只能开启并发了。


=================================================================================

3.Parallel Scavenge收集器

新生代收集器,使用复制算法。并行的多线程收集器

CMS的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而PS收集器的目的则是达到一个可控制的吞吐量(Throughput)。吞吐量即CPU用于运行用户代码的时间与CPU总消耗时间的比值(吞吐量=运行用户代码的时间/(运行用户代码的时间+垃圾收集的时间))。

停顿时间越短越适合用户交互的程序。高吞吐量可以高效利用CPU时间,尽快完成运算,适合在后台运算而不需太懂交互的任务。

Parallel Scavenge收集器可以精确控制吞吐量,控制最大垃圾收集停顿时间-XX:MaxGCPauseMillis,直接设置吞吐量大小-XX:GCTimeRatio。

GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的。PS收集器也为“吞吐量优先”收集器。

前面几个都是复制算法。

4.Serial Old收集器

Serial老年代版本单线程收集器。老年代采取使用标记整理算法。给Client模式下的虚拟机使用。

可以与JDK1.5及之前的Parallel Scavenge搭配使用;作为CMS收集器的后备预案,在并发收集发生Concureent Mode Failure时使用

5.Parallel Old收集器

Parallel Old收集器,Parallel Scavenge老年代版本,多线程,标记整理。JDK1.6。没有PO时只有SO,单线程老年代收集中无法充分利用服务器多CPU的处理能力,在老年代很大而且硬件比较高级的环境中,这种组合的吞吐量还不一定有PN+CMS给力。

在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑PS+PO收集器组合。

6.CMS收集器

Concurrent Mark Sweep,以获取最短回收停顿时间为目标的收集器。互联网站或B/S系统。标记清除算法。并发收集,低停顿。

四步:

①初始标记;Stop the World,仅标记GCRoots能关联的对象,速度很快。

②并发标记;进行GCRootsTracing的过程。

③重新标记;修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。比1长但远比2短。

④并发清除;并发清除与并发标记耗时最长。收集器线程都可以与用户线程一起工作。并发清理以后重置线程。

总体来说,CMS收集器的内存回收过程就是与用户线程一起并发执行的

缺点:

a.CMS对CPU资源非常敏感。面向并发设计的程序都对CPU敏感。不会导致用户线程停顿,但由于占用了CPU资源而导致应用程序变慢,总吞吐量降低。默认启动的回收线程数为(CPU数+3)/4。增量式CMS,i-CMS,尽量减少GC线程独占资源的时间,对用户程序影响较小,但垃圾收集过程加长,现已不提倡使用。

b.CMS无法处理浮动垃圾(Floating Garbage),可能出现Concurrent Mode Failure失败而导致另一次Full GC的产生。并发清理时用户线程还在运行着,新的垃圾不断产生。在标记之后无法在当次收集中处理它们,只好留待下一次GC时再清理。即为浮动垃圾。还需要预留足够空间给用户线程使用,CMS收集器不能象其他收集器那样等到老年代几乎满了再收集。若在此期间内存无法满足需要,就会出现CMF失败,虚拟机将启动后背方案:临时启用SO来重新进行老年代收集,停顿时间就加长。

-XX:CMSInitiatingOccupancyFraction不能设置过高。

c.CMS是标记清除,会产生大量碎片空间,对大对象内存分配带来麻烦。不得不提前触发一次Full GC.解决办法:-XX:+UseCMSCompactAtFullCollection开关参数(默认开启的)进行内存整理(即无法并发,但空间碎片问题没有了,停顿时间不得不变长),还有-XX:CNSFullGCsBeforeCompaction,设置执行多少次不压缩的Full GC之后跟着来一次带压缩的(默认为0,即每次进入FullGC时都进行碎片整理)。

=================================补充:Full GC===================

除直接调用System.gc外,触发Full GC执行的情况有如下四种。
1. 旧生代空间不足
旧生代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下错误:
java.lang.OutOfMemoryError: Java heap space 
为避免以上两种状况引起的FullGC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。
2. Permanet Generation空间满
PermanetGeneration中存放的为一些class的信息等,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation可能会被占满,在未配置为采用CMS GC的情况下会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出如下错误信息:
java.lang.OutOfMemoryError: PermGen space .为避免Perm Gen占满造成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC。
3. CMS GC时出现promotion failed和concurrent mode failure
对于采用CMS进行旧生代GC的程序而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure两种状况,当这两种状况出现时可能会触发Full GC。
promotionfailed是在进行Minor GC时,survivor space放不下、对象只能放入旧生代,而此时旧生代也放不下造成的;concurrent mode failure是在执行CMS GC的过程中同时有对象要放入旧生代,而此时旧生代空间不足造成的。
应对措施为:增大survivorspace、旧生代空间或调低触发并发GC的比率,但在JDK 5.0+、6.0+的版本中有可能会由于JDK的bug29导致CMS在remark完毕后很久才触发sweeping动作。对于这种状况,可通过设置-XX:CMSMaxAbortablePrecleanTime=5(单位为ms)来避免。
4. 统计得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间
这是一个较为复杂的触发情况,Hotspot为了避免由于新生代对象晋升到旧生代导致旧生代空间不足的现象,在进行Minor GC时,做了一个判断,如果之前统计所得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间,那么就直接触发Full GC。
例如程序第一次触发MinorGC后,有6MB的对象晋升到旧生代,那么当下一次Minor GC发生时,首先检查旧生代的剩余空间是否大于6MB,如果小于6MB,则执行Full GC。
当新生代采用PSGC时,方式稍有不同,PS GC是在Minor GC后也会检查,例如上面的例子中第一次Minor GC后,PS GC会检查此时旧生代的剩余空间是否大于6MB,如小于,则触发对旧生代的回收。
除了以上4种状况外,对于使用RMI来进行RPC或管理的Sun JDK应用而言,默认情况下会一小时执行一次Full GC。可通过在启动时通过

- java-Dsun.rmi.dgc.client.gcInterval=3600000来设置Full GC执行的间隔时间或通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc。

=================================================================

7.G1收集器

JDK1.7。(实验状态6U14开始实验状态,7U4)。面向服务端应用的垃圾收集器。希望可以替换掉1.5的CMS

特点:

1.并行与并发;多CPU多核的充分利用。

2,分代收集。

3,空间整合,整体是标记整理,局部是复制。G1运作期间是不会产生空间碎片的,有利于程序长时间运行

4.可预测的停顿,相对于CMS的一大优势。能明确指定在一个M毫秒的时间片段内消耗在垃圾收集上的时间不得超过N毫秒,实时Java(RTSJ)垃圾收集器特征。

将整个堆分为大小相等的独立区域,新生代和老年代不再是物理隔离的,都是一部分区域(不需要连续)的集合。

运作分为:初始标记,并发标记,最终标记(进入一个集合),筛选回收(根据用户期望的GC机制来制定回收计划)。

<------------------------------------------分界线-------------------------------------->

理解GC日志

0.756: [Full GC (System) 0.756: [CMS: 0K->1696K(204800K), 0.0347096 secs] 11488K->1696K(252608K), [CMS Perm : 10328K->10320K(131072K)], 0.0347949 secs] [Times: user=0.06 sys=0.00, real=0.05 secs]  1.728: [GC 1.728: [ParNew: 38272K->2323K(47808K), 0.0092276 secs] 39968K->4019K(252608K), 0.0093169 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]  2.642: [GC 2.643: [ParNew: 40595K->3685K(47808K), 0.0075343 secs] 42291K->5381K(252608K), 0.0075972 secs] [Times: user=0.03 sys=0.00, real=0.02 secs]  4.349: [GC 4.349: [ParNew: 41957K->5024K(47808K), 0.0106558 secs] 43653K->6720K(252608K), 0.0107390 secs] [Times: user=0.03 sys=0.00, real=0.02 secs]  5.617: [GC 5.617: [ParNew: 43296K->7006K(47808K), 0.0136826 secs] 44992K->8702K(252608K), 0.0137904 secs] [Times: user=0.03 sys=0.00, real=0.02 secs]  7.429: [GC 7.429: [ParNew: 45278K->6723K(47808K), 0.0251993 secs] 46974K->10551K(252608K), 0.0252421 secs]  
最前面的数字“0.756”:从虚拟机启动以来经过的秒数,代表了GC发生的时间;

[GC(或[Full GC)说明垃圾收集的停顿类型,不是用来区分新生代还是老年代的。有Full 则说明发送了Stop the world;

[CMS则表示GC发生的区域。有CMS,ParNew等。名称由收集器决定的。

方括号内0k->1696k表示GC前该内存区域已使用量->GC后该内存区域已使用容量(该内存区域总容量)

方括号外11488K->1696K(252608K)表示Java堆GC前后已使用容量(Java堆总容量)。

0.0347949 secs:该内存区域GC所占用的时间,单位为秒。

内存分配与回收策略

1.对象的内存分配,主要就是在堆上分配。主要分配在新生代的Eden区,若启动了本地线程分配缓冲,将按线程优先在TLAB上分配

2.对象优先在Eden上分配。没有足够空间时,虚拟机将发起一次Minor GC

-Xms20m -Xmx20m -Xmn10M  -XX:+PrintGCDetails -XX:SurvivorRatio=8

Java堆大小为20M,不可扩展,其中10M为新生代。Eden区和Survivor区的空间比例是8:1。新生代(Eden区+1个Survivor区,共两个Survivor区).

3.新生代GC,发送在新生代的垃圾回收动作,朝生夕死,非常频繁,回收速度很快。

老年代GC(Major GC/Full GC):在老年代的GC。出现了Major GC,经常会伴随至少一次MinorGC(非绝对,PS中就直接进行MajorGC的过程)。Major GC比MinorGC慢10倍以上

4.大对象直接进入老年代大对象,即大量连续内存空间的Java对象,最典型的是那种很长的字符串及数组。大对象容易导致内存还有不少空间就提前触发垃圾收集以获取足够的连续空间来安置它们。大对象安置在老年代是为了避免Eden 和Survivor区之间发生大量的内存复制。(新生代是采用的复制的算法。)

-XX:PretenureSizeThreshold.

5.长期存活的对象进入老年代。设置对象年龄计数器。对象在Eden出生并经过第一次MinorGC后仍存活,年龄+1,移入Survivor区。以后每经过一次MinorGC年龄加一,当达到15时(默认的)就进入老年代、

-XX:MaxTenuringThreshold。

6.动态对象的年龄判定

并不是对象年龄必须达到最大阈值才会进入老年代。如果survivor空间中相同年龄所有对象大小总和大于其空间的一般,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到阈值时才进入。

7.空间分配担保

发生minorGC之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,成立,MinorGC可以确保是安全的。不成立,则检查HandlePromotionFailure设置值是否允许担保失败。允许,则检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小。大于将尝试MinorGC,小于或者不允许冒险,也要进行一次FullGC.

老年代分配担保,将survivor无法容纳的对象直接进入老年代。依然担保失败,则只好在失败后重新发起一次Full GC。

大部分情况下担保是打开的,避免FullGC过于频繁

阅读全文
0 0
原创粉丝点击