后端调优基础——GC调优

GC类型

  • Minor GC :清理新生代,Minor GC是最频繁触发的GC,速度也最快的,主要工作原理是:
    对象在young区的eden创建,当eden空间满后触发Minor GC,将还存活的对象复制到一个survivor0中,另一个survivor1也会将对象复制过去,然后对eden和survivor1进行全部清理,survivor0和survivor1就这样不断交替,总有一个是空着的,当对象放不下或者是对象年龄足够老(默认15)会将其放入Old区,由此可见Minor GC时不但会清理对象,还会将对象放入Old区。

可以通过设置-XX:MaxTenuringThreshold=n来指定对象经过多少次Minor GC后就进入Old 区,默认15
可以通过设置-XX:SurvivorRatio=n设置Survivor区和eden区的比例,如-XX:SurvivorRatio=8 那么比例就是8:1:1 ,没有设置的话就以-XX:InitialSurvivorRatio=n为默认设置,这个值默认是8

  • Major GC :清理老年代,Major GC比Minor GC慢十倍以上,单独触发Major GC只有CMS和G1有这个能力,因为cms和G1处理老年代不需要触发full gc,其他的老年代回收器回收老年代需要触发full gc。
  • Full GC: 清理整个堆空间,包括新生代、永久代和老年代,它会启动老年代收集器和新生代收集器一起工作,全程Stop The World,尽量避免Full GC
Full gc:

full gc是对新生代,老年代、永久久代的统一回收,由于是对整个空间的回收,并且会触发系统的停顿(stop-the-world),因此应当尽量的减少系统full gc的次数。

触发的full gc的几个条件
  • 老年代空间不足:新生代如果容量不足会将对象放到老年代,老年代空间不足是会触发full gc,通过-Xmn设置新生代大小,也可以通过-XX:NewRatio=n设置比例,默认值是2,老年代:新生代=》2:1
  • 永久代空间不足:永久代在jdk7是存放在堆中的,可以通过-XX:PermSize=n-XX:MaxPermSize=n设置大。
    在jdk8中叫做元空间,使用的是计算机的本地内存通过-XX:MaxMetaspaceSize=n设置大小,如果不设置,默认最大内存大小是计算机的本地内存,由于运行时常量池在方法区中,而永久代又是方法区的实现,所以运行时常量池随着jdk8也移动到了本地内存,但是无论是jdk7还是jdk8,字符串常量池还是在堆中,字符串常量的创建是需要消耗堆内存的
  • CMS 产生碎片过多已经扛不住压力了就会调用full gc进行整合:可以通过-XX:CMSFullGCsBeforeCompaction=n设置要执行多少次full GC才会做压缩。默认是0,也就是每次full gc时就会对空间碎片进行整理,在默认配置下如果碎片过多CMS GC顶不住了,就要转入full GC的时候都会做压缩。(如果Full GC比较频繁,那么就不能每次都整理内存空间,不然积少成多,停顿的时间也是很可观的,此时就要调大该参数,让CMS在经过多次Full GC后再对内存空间进行压缩整理,而如果Full GC发生的不频繁,间隔时间较长,就可以设置成每次Full GC后都会对内存空间进行压缩整理,影响也不大。)
  • CMS GC时出现了promotion failed和concurrent mode failure:
    1、promotion failed意思是晋升失败是由于新生代把一些对象往老年代扔,然后老年代空间不足则抛出“promotion failed”,触发full gc,可能的原因是:Survivor空间过小或者老年代空间小或者碎片多,或者两者同时发生
    2、concurrent mode failure是CMS设置启动的老年代内存占比阈值过高,所以导致系统无法预留足够的空间满足程序需求,就会出现concurrent mode failure,启动担保机制,老年代增长过快触发full gc进行清理,解决方法是降低触发CMS的阀值,使用-XX:CMSInitiatingOccupancyFraction调低阈值,默认值是68,可以调到50
  • 统计得到新生代minor gc时晋升到老年代的平均大小大于老生代剩余空间
    原因有几个:1、代码问题大量大对象直接进入老年代 2、老年代空间不足,通过-XX:NewRatio=n可以调整老年代和年轻的堆比例
  • 代码直接调用System.gc()会建议系统调用full gc:在GC日志中会显示为[Full GC(System),可以开启-XX:-DisableExplicitGC禁止此类full gc

GC日志

通过设置系统参数:-XX:+PrintGCDetails可以GC日志打印到控制台,以下几个命令可以设置GC日志

    -XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
    -XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2020-10-23T21:53:59.234+0800)
    -XX:+PrintGCDetails   打印出GC的详细信息
    -verbose:gc  开启gc日志   开启这个按钮后就算是使用了+PrintGCDetails也无法在控制台看到,因为打印的内容会到日志文件中
    -Xloggc:d:/gc.log   gc日志的存放位置
实战:

运行这段代码,由于堆内存被这些常量占满,马上就会触发gc了

public class Tzb {
    public static void main(String[] args) {
        while (true){
            String str=System.currentTimeMillis()+ UUID.randomUUID().toString();
            str.intern();
        }
    }
}

打印了下列的内容,我把它们一部分复制出来

-XX:InitialHeapSize=1048576 -XX:MaxHeapSize=1048576 -XX:+PrintCommandLineFlags -XX:+PrintGCDetails 
-XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC 
---------------------------------------------------------------------------------------------------------------------------------
[GC (Allocation Failure) [PSYoungGen: 505K->488K(1024K)] 505K->488K(1536K), 0.0006483 secs]
 [Times: user=0.00 sys=0.00, real=0.00 secs] 
---------------------------------------------------------------------------------------------------------------------------------
[Full GC (Ergonomics) [PSYoungGen: 488K->441K(1024K)] [ParOldGen: 400K->318K(512K)] 888K->759K(1536K), 
[Metaspace: 4010K->4010K(1056768K)], 0.0033631 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

上面的有三部分我用虚线标出来了行中

  • 第一部分:我使用了-XX:+PrintCommandLineFlags命令打印的部分jvm参数信息:初始化堆大小,最大堆大小等信息,还有-XX:+UseParallelGC使用的是Parallel Scavenge收集器

  • 第二部分(重点):非full gc 信息

1、PSYoungGen:新生代GC区域的信息,这个新生代区域的名字会跟着收集器的不同而改变,ParNew收集器叫做ParNew,Serial叫做DefNew,Parallel Scavenge叫做PSYoungGen,由于我使用的是Parallel Scavenge收集器所以新生代显示为PSYoungGen
2、[PSYoungGen:488K->441K(1024K)]:“GC前该内存区域已使用容量->GC后该内存区域已使用容量(该内存区域可用总容量,这里Eden+Survivor0,另外一个由于是进行复制用的,所以不计入容量)”
3、505K->488K(1536K), 0.0006483 secs:“GC前Java堆已使用容量->GC后Java堆已使用容量(Java堆总容量),“0.0006483 secs”表示该内存区域GC所占用的时间,单位是秒”
4、[Times: user=0.00 sys=0.00, real=0.00 secs]:这里面的user、sys和real与Linux的time命令所输出的时间含义一致,分别代表用户态消耗的CPU时间、内核态消耗的CPU事件和操作从开始到结束所经过的墙钟时间(Wall Clock Time)。CPU时间与墙钟时间的区别是,墙钟时间包括各种非运算的等待耗时,例如等待磁盘I/O、等待线程阻塞,而CPU时间不包括这些耗时,但当系统有多CPU或者多核的话,多线程操作会叠加这些CPU时间,所以读者看到user或sys时间超过real时间是完全正常的

  • 第三部分(重点):full gc 信息

触发full gc会严重影响程序的运行性能,因为发生了牛批的stop-the-world
1、PSYoungGen:新生代GC区域发生了full gc
2、ParOldGen:发生老年代的区域,由于收集器不同老年代的区域名称也会不同,Serial old叫做Tenured,Parallel Old叫做ParOldGen,由于我使用的是Parallel Scavenge收集器会自动激活Parallel Old收集器所以老年代叫做ParOldGen
3、Metaspace:元空间,jdk7叫做Perm也就是方法区也成为永久代,jdk8后把方法区移除了,变成了元空间也就是这个Metaspace

下面列出gc日志中各个新生代收集器默认配合的老年代收集器及各自对应的新生代区域和老年代区域的名字:

  • 1、Serial:DefNew ——> Serial Old:Tenured
  • 2、ParNew:ParNew ——> Serial Old:Tenured
  • 3、Parallel Scavenge:PSYoungGen ——> parNew Old:ParOldGen
  • 4、CMS是比较特殊的使用的标记清除法,只能ParNew、Serial配合,一般是选择ParNew与CMS配合,CMS是多线程的老年代收集器有很低的停顿时间,所以性能也比较不错,可惜它不能和Parallel Scavenge配合,所以CMS不是默认使用的收集器,要使用必须自己配,选择了CMS就会默认开启ParNew和Serial Old,启动担保机制,后面我会专门讲它的
  • 5、G1是老少通吃的,不用和其他收集器配合,并且性能高停顿时间少,到jdk9后是默认的收集器,越到后面越牛批

这是目前官方推荐的配合主流

控制台这玩意在线上能看?试一下生产日志文件?

打开后长这样:


这里有一个GC日志分析工具,在线的,自己上去玩吧
https://gceasy.io/

GC收集器

  • 并发:收集器和用户程序同时进行
  • 并行:暂停用户程序使用多线程执行GC

Serial

Serial是串行新生代收集器,client的默认收集器,Serial Old是老年代的Serial,这两个特点除了工作区域与算法不一样其他没啥区别,一般适用于小型应用和单处理器,单核GC效率较高,但是会发生Stop-The-World,可以与CMS,Serial Old使用,由于没有线程开销,所以在单核情况下性能无敌(现在没什么服务器是单核了吧!!)

  • -XX:+UseSerialGC:如何没有配置其他的老年代,这个配置可以同时打开Serial和Serial Old,否则只打开Serial

ParNew(jdk9已经不支持)

是Serial的多线程版,本并行收集器,除了多线程与Serial没什么区别,这两款是共用了绝大部分的代码,所以特点几乎没什么区别,一样会发生Stop-The-World。许多Server选它只是为了和CMS配合而已。(如何不是为了配合CMS最好不要用这个收集器)

  • -XX:+UseParNewGC:如何没有配置其他的老年代,这个配置可以同时打开ParNew和Serial Old,否则只打开ParNew
  • -XX:ParallelGCThreads=n:设置并行收集器收集时使用的线程数,如果cpu核数小于8最好与CPU数目相等,如果大于8可以设置为3+(cpu core*5)/8。

Parallel Scavenge

并行收集器,这个收集器比较重要,是jdk7和jdk8 Server端默认的新生代收集器,Parallel Scavenge的特点是多线程,追求高吞吐量(吞吐量就是假如系统运行100分钟,收集器工作1分钟,吞吐量为99%),它有自适应调节的功能,通过收集系统的信息动态调节自身的参数,达到高吞吐的目的,吞吐高代表CUP使用率高,优点很明显但是缺点也很明显,它无法与老年代的CMS适配,同样是高性能的CMS追求的是最短的停顿时间,鱼和熊掌不可兼得,jvm还是选择了Parallel Scavenge作为默认Server的收集器,为了配合它的高吞吐,jvm还专门弄了一个ParNew Old,它是ParNew的老年版本。(这么牛批但是依然会发生Stop-The-World)

  • -XX:+UseParallelGC:通过这个配置,可以同时打开Parallel Scavenge和ParNew Old
  • -XX:ParallelGCThreads=n:设置并行收集器收集时使用的线程数,如果cpu核数小于8最好与CPU数目相等,如果大于8可以设置为3+(cpu core*5)/8。
  • -XX:MaxGCPauseMillis=n:设置新生代每次并行垃圾回收的最大暂停时间。设置这个值回收器会尽可能的去实现,jvm会根据这个停顿时间,把扫描的范围缩小成一个小的堆进行GC扫描达到缩短停顿的目的
  • -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)。默认是99
  • -XX:+UseAdaptiveSizePolicy:自适应策略,自动选择新生代区大小和相应的Survivor区比例、吞吐量(GCTimeRatio),停顿时间(MaxGCPauseMillis),这个是在调优比较复杂的场合下可以使用
  • -XX:+ScavengeBeforeFullGC:在full gc前触发一次minor gc

CMS(jdk9已经不建议使用,被G1取代)

这个比较牛批的老年代收集器,使用的是标记清除法,由于追求的是最短停顿时间,所以可以带给用户良好的体验,非常适合互联网和B/S的服务器。
一般老年代和永久代的回收是需要触发full gc的,但是CMS可以在不触发full gc的情况下单独对老年代和永久代进行gc,但是它的gc是需要检查永久代和老年代空间使用率默认是达到92%才启动,还要通过-XX:+CMSInitiatingOccupancyFraction=n设置,gc有7个步骤

1、初始标记(STW initial mark),会Stop-The-World
2、并发标记(Concurrent marking),并发操作,不会Stop-The-World
3、并发预清理(Concurrent precleaning),并发操作,不会Stop-The-World
5、重新标记(Final remark),会Stop-The-World
6、并发清理(Concurrent sweeping),并发操作,不会Stop-The-World
7、并发重置(Concurrent reset),并发操作,不会Stop-The-World

我们看到只是标记的部分都是会发生Stop-The-World的,但是标记的操作是非常短的,可以忽略不计的,所以CMS能做到最短的停顿时间,效率极高,但是缺点就是无法与Parallel Scavenge适配,CMS启动时会同时默认启动ParNew、Serial Old,为了防止SMC的回收失败,使用了CMS+ParNew+Serial Old的担保机制

担保机制:

由于CMS为了降低停顿,有一定的并发,启动时需要预留足够的内存给用户线程,所以CMS需要在内存空间满之前就得启动,jdk5默认是当老年代65%时启动,而jdk6以后是默认92%,还要通过-XX:+CMSInitiatingOccupancyFraction设置更高的启百分比,如果垃圾生产的太快了或者阈值太高,在CMS运行期间发现无法预留足够的内存,就会出现concurrent mode failure而运行异常,这时JVM就会启动备用的Serial Old去处理老年代垃圾,所以CMS+ParNew+Serial Old的担保机制是为了防止CMS的concurrent mode failure的异常

  • -XX:+UseConcMarkSweepGC:使用此命令可以同时开启CMS、ParNew、Serial Old。
  • -XX:ParallelCMSThreads=n: 设定 CMS 的并发线程数量。默认线程数是(ParallelGCThreads+3)/4。意味着当默认条件下ParallelGCThreads为4时,只有一个线程并发
  • -XX:+CMSParallelRemarkEnabled:采用并行标记方式降低Stop-The-World的时间。
  • -XX:+UseCMSCompactAtFullCollection:此开关默认开启,用于消除碎片。每一次FULL GC都伴随一次碎片回收
  • -XX:CMSFullGCsBeforeCompaction=n:设置在执行多少次Full GC后对内存空间进行压缩整理。设置这个的前提是UseCMSCompactAtFullCollection开启
  • -XX:+CMSInitiatingOccupancyFraction:设置 CMS 收集器在老年代空间被使用多少后触发,默认为 68%,设置得太高会触发Full gc影响性能。
  • -XX:+CMSClassUnloadingEnabled:允许对类元数据进行回收。也就是允许CMS回收方法区
  • -XX:CMSInitatingPermOccupancyFraction=n:当永久区占用率达到这一百分比后,启动 CMS 回收 (前提是-XX:+CMSClassUnloadingEnabled激活了)。
  • -XX:UseCMSInitatingOccupancyOnly:表示只在到达阈值的时候,才进行 CMS 回收。

CMS示例, 如图:我没有选择任何的新生代收集器,只是开启了CMS

我已经圈出来了,可以只要开启CMS就会自动选择ParNew

我将CMS的一部分复制下来

[GC (CMS Initial Mark) [1 CMS-initial-mark: 723K(768K)] 742K(1920K), 0.0002026 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
---------------------------------------------------------------------------------------------------------------------------------
[CMS-concurrent-mark-start]
[CMS-concurrent-mark: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
---------------------------------------------------------------------------------------------------------------------------------
[CMS-concurrent-preclean-start]
[CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
---------------------------------------------------------------------------------------------------------------------------------
[GC (CMS Final Remark) [YG occupancy: 75 K (1152 K)][Rescan (parallel) , 0.0001481 secs][weak refs processing, 0.0000063 secs]
[class unloading, 0.0001892 secs][scrub symbol table, 0.0003164 secs][scrub string table, 0.0000845 secs]
---------------------------------------------------------------------------------------------------------------------------------
[1 CMS-remark: 723K(768K)] 799K(1920K), 0.0007957 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
---------------------------------------------------------------------------------------------------------------------------------
[CMS-concurrent-sweep-start]
[CMS-concurrent-sweep: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
---------------------------------------------------------------------------------------------------------------------------------
[CMS-concurrent-reset-start]
[CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
---------------------------------------------------------------------------------------------------------------------------------

一共有7部分,对应着上面列出的CMS的7个步骤

G1

G1收集器是JDK9默认的收集器,jdk7和jdk8中都可以用它,到了jdk9才正式成为默认,G1完全取代了CMS,针对CMS的碎片化进行了改进,使用G1不再需要与其他的收集器配合,原因是在于G1对之前的内存结构做了非常大的改动,不再把新生代和老年代分别分配成一整块一整块的大区域,而是把之前的老年代和新生代的Eden,survivior分割成一小块一小块的Region放在同一片堆区域,每块Region最大是32M,堆空间最多存放2048个Region,就是最多是60G到70G,G1基于复制算法,高效的整理剩余内存,而不需要管理内存碎片,
G1有以下的特点:

  • 1、并行与并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU来缩短stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
  • 2、空间整合:G1收集器从整体上看是采用标记整理算法,主要是把每一块当做一个标记单位,但是从局部也就每一块Region是基于复制算法的,把需要清理的区域中存活的对象复制到其他区域,所以不会产生碎片,分配大对象时不会因为找不到足够大的连续内存空间而触发下一次GC
  • 3、可预测停顿:降低停顿时间是CMS和G1共同的关注点,但是G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为N毫秒时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
  • 4、分代收集:物理结构改变了,但是仍然保留着之前的分代概念,收集垃圾的原理依旧是分代收集

G1的收集算法也比较独特,采用了分代并且分区的算法,分代就是根据不同的块的特点,例如新生代的块就会使用复制算法,老年代的块使用标记压缩,并且每个块都分配一个remember set集合记录所属块和其他块的对象引用关系,通过检查这个集合找出垃圾对象,避免了扫描整个确定可达关系。

一般来说,空间越大扫描的范围就越大,停顿时间就长,但是进行分块可以减少每次扫描整个堆空间的时间

内存结构如图,其中Humongous是大对象,大对象直接进入Humongous的Region

同时还有一个并发标记过程记录的G区,G区是垃圾对象比率较高的区域,混合回收会优先清理的区域

G1的GC模式两种:
  • 1、Young GC:G1对新生代的 Region进行GC
  • 2、Mixed GC:根据并发标记过程统计得出收集收益高的Region进行GC。在用户指定的开销目标范围内尽可能选择收益高 Region,以此来控制Mixed GC的时间开销,达到可预测停顿,这里处理的Region就G区,老年代和新生代的Region都可能成为G区
G1的工作流程主要分两个阶段:并发标记和混合GC
1、并发标记:
  • 初始化标记:这个过程会产生STW,标记可达对象,同时进行一次young gc清理eden区
[GC pause (G1 Evacuation Pause) (young) (initial-mark), 0.0013482 secs]
  • 根区域扫描:并发扫描survivor区在old区的可达对象,这个过程中young gc会停止
[GC concurrent-root-region-scan-start]
[GC concurrent-root-region-scan-end, 0.0000189 secs]

[Ext Root Scanning (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 0.7]
  • 并发标记:并发标记堆中还存活的对象,这个过程可以被young gc打断
[GC concurrent-mark-start]
  • 重新标记:这个过程会产生STW,暂停程序重新标记存活的对象
  • 独占清理:会STW,计算各个区域的存活对象和GC回收比例,并做排序,标记可供混合回收的区域,同时更新remember set(下面是更新rs和扫描rs的gc日志)
[Update RS (ms): Min: 0.0, Avg: 0.3, Max: 0.4, Diff: 0.4, Sum: 1.7]
         [Processed Buffers: Min: 0, Avg: 0.8, Max: 1, Diff: 1, Sum: 5]
      [Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
  • 并发清理:清理空闲区域
2、混合清理:

并发标记完成后,G1会根据设置的停顿时间,优先选择清理性价比高的区域,也就是G区

full gc

和CMS一样,G1也是个并发的回收器,当不足以提供足够内存给用户程序时同样会触发full gc

[GC concurrent-mark-start]
[Full GC (Allocation Failure)  1712K->690K(2048K), 0.0023713 secs]
   [Eden: 0.0B(1024.0K)->0.0B(1024.0K) Survivors: 0.0B->0.0B Heap: 1712.1K(2048.0K)->690.3K(2048.0K)], [Metaspace: 3469K->3469K(1056768K)]
 [Times: user=0.02 sys=0.00, real=0.00 secs] 
[GC concurrent-mark-abort]
  • -XX:+UseG1GC : 让 JVM使用G1垃圾收集器, jdk9被设为默认垃圾收集器;所以如果你的版本比较新则不再需要使用该参数
  • -XX:MaxGCPauseMillis=200: 设置最大GC停顿时间指标,JVM会尽力实现,但不保证. 默认值为200毫秒.
  • -XX:InitiatingHeapOccupancyPercent=45: 如果整个堆内存的45%的时候启动G1,此时会触发并发标记和一次mixGc。值为0则表示一直执行GC循环, 默认值为45,设置过大会导致full gc。
  • -XX:G1MixedGCLiveThresholdPercent=n:默认值是85%,确定要回收的Region的时候,必须是存活对象低于85%的Region才可以回收。
  • -XX:G1ReservePercent=n: 设置堆的空闲内存预留百分比,以降低内存溢出的风险. 默认值是 10,基本不变.
  • -XX:ConcGCThreads=n: 并发垃圾收集器使用的线程数量. 默认与cup数量一致
  • -XX:MetaspaceSize=256M: 初始化元空间,默认20M。
  • -XX:MaxMetaspaceSize=512M: 最大元空间
G1收集器演示:

G1真的比较复杂,算法和机制都比较难,日志多了很多

GC篇总结:

  • 停顿时间参数,如:G1的MaxGCPauseMillis,parallel的MaxGCPauseMillis如果调低了GC次数就会上升。
  • 线程参数,如:parallel的ParallelGCThreads,CMS的ParallelCMSThreads,G1的ConcGCThreads调高在垃圾生产多并且cup紧张的场景会消耗cpu性能
  • 堆空间参数:通过Xmx Xms调整堆的大小,堆越多gc次数自然就会减少,但是堆空间大反而会增长gc时间,可以根据情况设置停顿时间去缩短每次gc时间、或者使survivor减小有效的减少复制空间减少复制操作让对象尽块进入老年代然后使用CMS进行并发处理、或者直接使用G1收集器
  • 启动阈值:G1的InitiatingHeapOccupancyPercent,CMS的CMSInitiatingOccupancyFraction启动阈值过高会容易触发full gc,启动阈值过低会增加普通gc次数
  • System.gc:如果代码中有频繁使用 System.gc(),那么需要使用-XX:-DisableExplicitGC禁止full gc
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章