JVM

JVM GC

2020-08-30  本文已影响0人  今年五年级

一 JVM内存分配与回收

1.1 新创建对象优先在eden区分配

一般来说,新创建的对象都会在堆内存中eden区分配空间。当eden区没有足够空间的时候,jvm将进行一次Minor GC,Minor GC与Full GC区别如下

GC测试:
先添加jvm打印gc详情参数:-XX:+PrintGCDetails

public class Test3 {
    public static void main(String[] args) {
        byte[]
        allocation1 = new byte[28000*1024]/*,
        allocation2 = new byte[900*1024],
        allocation3 = new byte[1000*1024],
        allocation4 = new byte[1000*1024],
        allocation5 = new byte[1000*1024],
        allocation6 = new byte[1000*1024]*/;
    }
}

1.1.1 先只创建allocation1对象

输出结果如下:


可以看到eden内存空间被占满,我们初始化了28M空间,实际eden内存空间使用了33M左右是因为,剩余的5M空间是JVM用于初始化自己的一些对象所占用的,即使我们不创建对象新生代也会使用至少5M的空间,为了验证我们刚刚的说法,我们将main方法中new操作代码全部屏蔽,结果输出如下,可以看出印证了我们的结论

1.1.2 创建allocation1对象和allocation2对象


详细查看方法
取消new allocation2对象的代码注释,运行程序,结果输出如下

在Young Generation(新生代)的Eden区的空间不足以容纳新生成的对象时执行minorGC
可以看到,因为创建allocation2对象需要大概不到1M的空间,使得原本创建allocation1对象以后已经100%的eden空间超过容量,然后开始进行一次minor GC,而这次GC会将allocation1和allcation2理论上都挪移到survivor中的from去,因为这里两个对象理论上都存活,可是from只有5M的空间,光一个allocation1就有28M,因此根据大对象直接移动到老年代原则(分配担保机制),将allocation1对象直接移动到了老年代,我们在上面的红框中可以看到,老年代使用的空间正好是allocation1对象的占用空间(多余的8K为零散数据,无需理会),老年代上的空间足够存放allocation1,所以不会出现Full GC。而allcation2对象则进入了from区

1.1.3 继续加上创建allocation3对象

可以从下图中看到,新创建的allocation3对象在eden区重新分配

1.2 大对象直接进入老年代

大对象就是需要大量连续内存空间的对象,比如字符串,数组。原因是:
因为如果eden区满了,一个对象在survivor中需要经历15次minor gc,如果大对象不直接进入老年代,大对象来来回回在from和to区复制的效率比较低

1.3 长期存活的对象进入老年代

虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别那些对象应放在新生代,那些对象应放在老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器,这个对象年龄是Minor GC年龄,即经历过多少次Minor GC

如果对象在 Eden 出生,此时年龄为0岁,并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为1.对象在 Survivor 中每熬过一次 MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),还没销毁掉,则在下一次GC来临的时候,就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置【tenure 英[ˈtenjə(r)]

二 如何判断对象是否可以回收

堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断那些对象已经死亡(即不能再通过任何途径使用的对象)

2.1 引用计数法

给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0的对象就是不可能再被使用的。

这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。 所谓对象之间的相互引用问题,如下面代码所示:除了对象obj1 和 obj2 相互引用着对方之外,这两个对象再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为0,于是引用计数算法无法通知 GC 回收器回收他们,也就进入一种死循环的状态,可能导致内存泄露

public class MyObject {

    public Object ref =null;
    public static void main(String[] args) {
        MyObject myObject1 =newMyObject();
        MyObject myObject2 =newMyObject();
        myObject1.ref = myObject2;
        myObject2.ref = myObject1;
        myObject1 =null;
        myObject2 =null;
     }
}

2.2 可达性分析算法

通过一系列称为“GC Roots”的对象作为起点,从这些节点开始向下检索,沿着节点所走过的路线叫做引用链,当一个对象和GC Roots之间没有任何一条引用链相连,即没有路可以从GC Roots走到这个对象的时候,则证明该对象是不可用可以被回收的

常见的GC Roots根节点:
类加载器、Thread、虚拟机栈中的本地变量表、static成员、常量引用、本地方法栈中的变量等等

2.3 finalize()方法最终决定对象存活与否

经历可达性分析算法以后的不可达对象(对象在进行可达性分析后发现没有与GC Roots连接的引用链路)并不是直接被回收的,要真正宣告一个对象死亡,还要经历标记过程,标记需要经历两次标记

第1次标记并进行一次判断筛选
判断对象是否有必要执行finalize()方法
当对象没有覆盖父类Object的finalize方法,或者finalize方法已经被调用过(对象在方法中没有进行自救,仍然没有引用链让他指向GC Roots根),则jvm将这两种情况都视为需要执行回收,直接进行GC,对象被回收

第2次标记
如果对象在第一次判断的时候判断为有必要执行finalize()方法,即对象覆盖了父类Object的finalize方法,并且finalize方法没有被调用过

则该对象会被放在名为 F-Queue的队列中等待被一条jvm自动创建的,低优先级的Finalizer线程去执行(因为可能带来性能损耗),但并不承诺会等待他运行结束(因为如果对象的finalize方法执行缓慢,或死循环,很可能导致F-Queue队列中其他对象永久等待,甚至内存回收系统奔溃)

finalize方法是对象避免死亡的最后一次机会,如果对象想要在finalize()方法中自救,则只需要重新与GC Roots引用链撒花姑娘的任何一个对象建立关联即可,譬如把自己赋值给某个类变量或者对象的成员变量,那么第二次标记时它将会被移除即将回收的集合,如果这时候对象还没·逃脱,则其将会被真正的回收

// 重写R中的finalize方法
    @Override
    protected void finalize() throws Throwable {
        System.out.println("对象即将被回收"+status);
    }
// main方法
public class Test {
    public static void main(String[] args) {
        List<Object> list = new ArrayList<>();
        int i=0,j =0;
        for(;;){
            list.add(new R(i++,UUID.randomUUID().toString(),null));
            new R(j--, UUID.randomUUID().toString(),null);
        }
    }
}

注意:finalize()只会在对象内存回收前被调用一次(The finalize method is never invoked more than once by a Java virtual machine for any given object. );
总结:基于在自我救赎中的表现来看,此方法有很大的不确定性(不保证方法中的任务执行完)而且运行代价较高。所以用来回收资源也不会有什么好的表现。因此尽量不要使用 finalize()

2.4 如何判断一个常量是废弃常量

如果在运行时常量池中存在字符串“abc”,且当前没有任何string对象引用该字符串常量的话,则常量“abc”就属于废弃常量,这时候如果发生内存回收,有必要的话(即不一定回收),则“abc”将会被清理出常量池

2.5 如何判断一个类是无用的类

在方法区中回收的是无用的类,判断一个类是否无用,需要同时满足3个条件
(1)该类的所有实例均已经被回收,即堆中无该类实例
(2)加载该类的类加载器classLoader已经被回收
(3)该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射获取该类的方法
满足上面的条件以后,jvm将可能对该无用类进行回收,之所以说可能,是不一定回收,看心情,不像对象一样不使用必然被回收

三 垃圾收集算法

3.1 标记清除算法(mark-sweep)

特点:效率低,内存碎片多

标记清除顾名思义是一种分两阶段对对象进行垃圾回收的算法。
第一阶段:标记。从根结点出发遍历对象(从第一个格子开始遍历所有的格子),对访问过的对象打上标记,表示该对象可达
第二阶段:清除。对那些没有标记的对象进行回收,这样使得不能利用的空间能够重新被利用

算法优点:

  1. 实现简单
  2. 不移动对象,与保守式GC算法兼容。在保守式GC算法中对象是不能移动的。

算法的缺点:

  1. 内存碎片化。清理后的可用内存被存活对象分隔开,导致块是不连续的,容易出现空闲内存很多,但分配大对象时找不到合适的块。比如我要用上三下三的6个格子分配一个对象,但是第没有满足条件的连续的区块供我们使用,这样会导致GC,然而其实空间是够放的
  2. 分配速度慢。即使是First-fit(找到大于等于size的块立即返回),但其操作仍是一个O(n)的操作,最坏情况是每次都要遍历到最后。同时因为碎片化,大对象的分配效率会更慢

3.2 复制算法(coping)

为了解决标记清理算法内存碎片化的缺陷而提出来。按内存容量将内存划分为等大的两块,每次只使用其中一块,当这块被使用的内存满后将里面尚存活的对象复制到另一块上去,把已使用的内存清理掉

算法优点:
实现简单、内存效率高、不易产生碎片
算法缺点:
可用内存被压缩到原来的一半,且存活对象增多的话,复制算法的效率会降低很多

3.3 标记整理算法(mark-compact)

考虑到上面两个算法的缺点,大佬们结合了上面两个算法的优点,提出了标记整理算法

标记整理算法的标记阶段和标记清除算法相同,区别在于标记整理算法在标记阶段完成之后并不是直接清除可回收对象,而是将存活对象转移到内存的一端,然后将这个存活对象端边界外的对象全部清理,这样兼顾了标记清除(简单,用完整的内存)和复制算法(不会产生内存碎片化)的优点

3.4 分代收集算法(generational collecting)

当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法

比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,因为尚存活的对象少,因此要复制的操作比较少,只需要付出少量对象的复制成本就可以完成每次垃圾收集
而老年代的对象存活几率是比较高的,老年代每次都回收少量对象,因此采用标记整理算法,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集

JVM每次只会使用eden和其中一块survivor来为对象服务,所以无论什么时候,都会有一块survivor空间,因此新生代实际可用空间只有90%

四 垃圾收集器


jdk11出现了ZGC收集器(可处理T级别的堆内存GC),jdk9使用G1收集器

GC收集算法是内存回收的方法论,则垃圾收集器就是内存回收的具体执行者,我们可以指定使用哪种垃圾收集器,垃圾收集器会使用某种算法或者某几种算法组合完成GC任务

虽然我们对各个收集器进行比较,但并非为了挑选出一个最好的收集器。因为直到现在为止还没有最好的垃圾收集器出现,更加没有万能的垃圾收集器,我们能做的就是根据具体应用场景选择适合自己的垃圾收集器。试想一下:如果有一种四海之内、任何场景下都适用的完美收集器存在,那么我们的HotSpot虚拟机就不会实现那么多不同的垃圾收集器了

4.1 Serial收集器(-XX:+UseSerialGC -XX:+UseSerialOldGC)

Serial(串行)收集器收集器是最基本、历史最悠久的垃圾收集器了。这个收集器是一个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( "Stop The World" ),直到它收集结束
新生代采用复制算法,老年代采用标记-整理算法

虚拟机的设计者们当然知道Stop The World带来的不良用户体验,所以在后续的垃圾收集器设计中停顿时间在不断缩短(仍然还有停顿,寻找最优秀的垃圾收集器的过程仍然在继续)。

但是Serial收集器有没有优于其他垃圾收集器的地方呢?当然有,它简单而高效(与其他收集器的单线程相比)。Serial收集器由于没有线程交互的开销(回收的时候没有应用程序抢CPU资源),自然可以获得很高的单线程收集效率

4.2 ParNew收集器

是Serial收集器的多线程版本,除了使用多线程收集外,其余行为(控制参数、手机算法、回收策略等等)都和Serial收集器完全一样
新生代采用复制算法,老年代采用标记-整理算法

从上面的一个横杠(一个线程)变成了多个横杠(多个线程)
ParNew收集器是运行在Server模式下的首要选择的垃圾收集器,除了Serial以外,只有它能和CMS(真正意义上的并发收集器)收集器配合工作

并行和并发概念补充:
并行(Parallel) :指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。适合科学计算、后台处理等弱交互场景。 这个概念是针对垃圾收集线程来说的。我们提到的parNew,parallel都是并行收集器
并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行,一个CPU上两个都执行就是并发,两个CPU一个跑用户线程,一个CPU跑垃圾收集线程就是并行,不是100%并行去执行的),用户程序在继续运行,而垃圾收集器运行在另一个CPU上。适合Web应用。CMS和G1收集器都是并发收集器

4.3 Parallel Scavenge收集器(-XX:+UseParallelGC(新生代),-XX:+UseParallelOldGC(老生代))

Parallel Scavenge 收集器类似于ParNew 收集器,是jdk1.8 Server 模式(内存大于2G,2个cpu)下的默认收集器,我们从下图红框可以验证我们的说法

parallel收集器与parNew收集器的区别主要是:
Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU)。CMS等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。 Parallel Scavenge收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解的话,可以选择把内存管理优化交给虚拟机去完成也是一个不错的选择。

新生代采用复制算法,老年代采用标记-整理算法
收集过程基本和上面的parNew收集器一样

4.4 Serial Old收集器

Serial收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS收集器的后备方案

4.5 Parallel Old收集器

Parallel Scavenge收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源的场合,都可以优先考虑 Parallel Scavenge收集器和Parallel Old收集器

4.6 CMS收集器(-XX:+UseConcMarkSweepGC(主要是old区使用))搭配XX:+UseParNewGC)

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它而非常符合在注重用户体验的应用上使用,它是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作

从名字中的Mark Sweep这两个词可以看出,CMS收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:
(1)初始标记:暂停所有的其他线程(STW),并记录下直接与GC Root相连的对象(即GC Roots树的根节点的儿子,非孙子以及以后辈分),给他们打上标记,速度很快 ,用的时间极少甚至0.几ms,对程序几乎没有影响
(2)并发标记: (耗时较长)
同时开启GC和用户线程,继续从第一步的直接与GC Roots相连的对象开始向下标记,可能这个期间用户线程又创建了新的对象,这些新创建的对象也是与GC Roots根连接的。
(3)重新标记:
重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
(4)并发清理: (耗时较长)
开启用户线程,同时GC线程开始对未标记的区域做清扫

优点:
在耗时较长的地方使用并发,让用户线程工作。将一个比较长的GC过程拆成很多小段,对每个小段做特殊处理,才能达到一次GC stw时间尽可能短,从而对应用程序影响尽可能少

CMS是一款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下面三个明显的缺点:

4.6 G1收集器(-XX:+UseG1GC)

jdk1.7出现,jdk1.9默认收集器,相比于CMS,G1收集器收集的能力更强,数倍于CMS收集器。G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要是针对配备了多颗处理器以及最大容量内存的机器。以提高满足GC停顿时间要求的同时,还具备高吞吐量性能

G1将Java堆划分为多个大小相等的独立区域(Region),虽保留新生代和老年代的概念,但不再是物理隔阂了,它们都是(可以不连续)Region的集合。

分配大对象(直接进Humongous区,专门存放短期巨型对象,不用直接进老年代,避免Full GC的大量开销)不会因为无法找到连续空间而提前触发下一次GC。因此G1适合少量大对象的情况
被视为JDK1.7中HotSpot虚拟机的一个重要进化特征。它具备以下特点:
(1)并行与并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程来执行GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。

(2)分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。

(3)空间整合:与CMS的“标记--清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。

(4)可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1 和 CMS 共同的关注点,但G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内完成垃圾收集。

G1收集器的运作大致分为以下几个步骤:

初始标记(initial mark,STW):在此阶段,G1 GC 对根进行标记。该阶段与常规的 (STW) 年轻代垃圾回收密切相关。

并发标记(Concurrent Marking):G1 GC 在整个堆中查找可访问的(存活的)对象。

最终标记(Remark,STW):该阶段是 STW 回收,帮助完成标记周期。

筛选回收(Cleanup,STW):筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。

可以看到和CMS收集器前3个步骤基本一致,就在最后一个步骤不一致,CMS是并发清理,但是G1是只允许GC线程工作,因为它要做到一个指定时间结束GC的功能,因此他为了更快的完成收集任务,没有允许用户线程一起工作

G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region(这也就是它的名字Garbage-First的由来)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了GF收集器在有限时间内可以尽可能高的收集效率

五 垃圾收集器的选择

  1. 优先调整堆的大小让服务器自己来选择

  2. 如果内存小于100M,使用串行收集器

  3. 如果是单核,并且没有停顿时间的要求,串行或JVM自己选择

  4. 如果允许停顿时间超过1秒,选择并行或者JVM自己选

  5. 如果响应时间最重要,并且不能超过1秒,使用并发收集器

下图有连线的可以搭配使用,官方推荐使用G1,因为性能高

六 调优

6.1 调优的目的

JVM调优主要就是调整下面两个指标
停顿时间:垃圾收集器做垃圾回收中断应用执行的时间。 -XX:MaxGCPauseMillis
吞吐量:用户程序执行的时间占总时间的比例。垃圾收集的时间和总时间的占比:1/(1+n),吞吐量为1-1/(1+n) 。 -XX:GCTimeRatio=n

6.2 调优步骤

  1. 打印GC日志

-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:./gc.log

Tomcat则直接加在JAVA_OPTS变量里

  1. 分析日志得到关键性指标

  2. 分析GC原因,调优JVM参数

很多时候项目启动慢,都是大量类导致的元空间满了,进而导致系统Full gc增多,stw时间长。关于为何元空间是堆外内存却也会满的原因参考1
关于为何元空间是堆外内存却也会满的原因参考2

6.3 GC常用参数

堆栈设置

-Xss:每个线程的栈大小

-Xms:初始堆大小,默认物理内存的1/64

-Xmx:最大堆大小,默认物理内存的1/4

-Xmn:新生代大小

-XX:NewSize:设置新生代初始大小

-XX:NewRatio:默认2表示新生代占年老代的1/2,占整个堆内存的1/3。

-XX:SurvivorRatio:默认8表示一个survivor区占用1/8的Eden内存,即1/10的新生代内存。

-XX:MetaspaceSize:设置元空间大小

-XX:MaxMetaspaceSize:设置元空间最大允许大小,默认不受限制,JVM Metaspace会进行动态扩展。

垃圾回收统计信息

-XX:+PrintGC

-XX:+PrintGCDetails

-XX:+PrintGCTimeStamps

-Xloggc:filename

收集器设置

-XX:+UseSerialGC:设置串行收集器

-XX:+UseParallelGC:设置并行收集器

-XX:+UseParallelOldGC:老年代使用并行回收收集器

-XX:+UseParNewGC:在新生代使用并行收集器

-XX:+UseParalledlOldGC:设置并行老年代收集器

-XX:+UseConcMarkSweepGC:设置CMS并发收集器

-XX:+UseG1GC:设置G1收集器

-XX:ParallelGCThreads:设置用于垃圾回收的线程数

并行收集器设置

-XX:ParallelGCThreads:设置并行收集器收集时使用的CPU数。并行收集线程数。

-XX:MaxGCPauseMillis:设置并行收集最大暂停时间

-XX:GCTimeRatio:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)

-XX:YoungGenerationSizeIncrement:年轻代gc后扩容比例,默认是20(%)

CMS收集器设置

-XX:+UseConcMarkSweepGC:设置CMS并发收集器

-XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。

-XX:ParallelGCThreads:设置并发收集器新生代收集方式为并行收集时,使用的CPU数。并行收集线程数。

-XX:CMSFullGCsBeforeCompaction:设定进行多少次CMS垃圾回收后,进行一次内存压缩

-XX:+CMSClassUnloadingEnabled:允许对类元数据进行回收

-XX:UseCMSInitiatingOccupancyOnly:表示只在到达阀值的时候,才进行CMS回收

-XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况

-XX:ParallelCMSThreads:设定CMS的线程数量

-XX:CMSInitiatingOccupancyFraction:设置CMS收集器在老年代空间被使用多少后触发

-XX:+UseCMSCompactAtFullCollection:设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片的整理

G1收集器设置

-XX:+UseG1GC:使用G1收集器

-XX:ParallelGCThreads:指定GC工作的线程数量

-XX:G1HeapRegionSize:指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区

-XX:GCTimeRatio:吞吐量大小,0-100的整数(默认9),值为n则系统将花费不超过1/(1+n)的时间用于垃圾收集

-XX:MaxGCPauseMillis:目标暂停时间(默认200ms)

-XX:G1NewSizePercent:新生代内存初始空间(默认整堆5%)

-XX:G1MaxNewSizePercent:新生代内存最大空间

-XX:TargetSurvivorRatio:Survivor填充容量(默认50%)

-XX:MaxTenuringThreshold:最大任期阈值(默认15)

-XX:InitiatingHeapOccupancyPercen:老年代占用空间超过整堆比IHOP阈值(默认45%),超过则执行混合收集

-XX:G1HeapWastePercent:堆废物百分比(默认5%)

-XX:G1MixedGCCountTarget:参数混合周期的最大总次数(默认8)

上一篇下一篇

猜你喜欢

热点阅读