JVM内存模型和垃圾回收机制

JVM内存模型

根据Java虚拟机规范,Java数据区域分为五大数据区域。

其中方法区和堆是所有线程共享的,虚拟机栈、本地方法栈和程序计数器则为线程私有的。

有的博客称方法区是永久代,那是因为前者是JVM的规范,而后者则是JVM规范的一种实现,并且只有HotSpot才有永久代,

JDK8中已经彻底移除了方法区,JDK8中引入了一个新的内存区域叫metaspace(元空间),后边详细介绍。

栈区

栈分为虚拟机栈和本地方法栈。

虚拟机栈

每个方法执行都会创建一个栈帧,用于存放局部变量表,操作栈,动态链接,方法出口等。每个方法从被调用,直到被执行完。对应着一个栈帧在虚拟机中从入栈到出栈的过程。 通常说的栈就是指局部变量表部分,存放编译期间可知的8种基本数据类型及对象引用和指令地址。局部变量表是在编译期间完成分配,当进入一个方法时,这个栈中的局部变量分配内存大小是确定的。

常见的的两种异常StackOverFlowError和OutOfMemoneyError。当线程请求栈深度大于虚拟机所允许的深度就会抛出StackOverFlowError错误;虚拟机栈动态扩展,当扩展无法申请到足够的内存空间时候,抛出OutOfMemoneyError。

本地方法栈

本地方法栈与虚拟机栈的作用十分类似,不过本地方法是为native方法服务的。部分虚拟机(比如Sun HotSpot虚拟机)直接将本地方法栈与虚拟机栈合二为一。与虚拟机栈一样,本地方法栈也会抛出StactOverflowError与OutOfMemoryError异常。

程序计数器

当前线程所执行的行号指示器。通过改变计数器的值来确定下一条指令,比如循环,分支,跳转,异常处理,线程恢复等都是依赖计数器来完成。

Java虚拟机多线程是通过线程轮流切换并分配处理器执行时间的方式实现的。为了线程切换能恢复到正确的位置,每条线程都需要一个独立的程序计数器,所以它是线程私有的。

唯一一块Java虚拟机没有规定任何OutofMemoryError的区块。

方法区

方法区/永久代是被所有线程共享区域,用于存放已被虚拟机加载的类信息、常量、静态变量等数据。永久代的垃圾回收和老年代的垃圾回收是绑定的,一旦其中一个区域被占满,这两个区都要进行垃圾回收。

在JDK1.7之前运行时常量池逻辑包含字符串常量池存放在方法区,此时hotspot虚拟机对方法区的实现为永久代

在JDK1.7字符串常量池被从方法区拿到了堆中,这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区,也就是hotspot中的永久代

在JDK1.8 hotspot移除了永久代用元空间(Metaspace)取而代之,这时候字符串常量池还在堆, 运行时常量池还在方法区,只不过方法区的实现从永久代变成了元空间(Metaspace)

移除永久代的影响

永久代在JDK8中被删除,被一个叫做元空间的区域所替代了。这项改动是很有必要的,因为对永久代进行调优是很困难的。永久代中的元数据可能会随着每一次Full GC发生而进行移动。并且为永久代设置空间大小也是很难确定的,因为这其中有很多影响因素,比如类的总数,常量池的大小和方法数量等。

默认情况下,元空间的最大可分配空间就是系统可用内存空间。因此,我们就不会遇到永久代存在时的内存溢出错误,也不会出现泄漏的数据移到交换区这样的事情。最终用户可以为元空间设置一个可用空间最大值,如果不进行设置,JVM会自动根据类的元数据大小动态增加元空间的容量。

注意:永久代的移除并不代表自定义的类加载器泄露问题就解决了。还必须监控内存消耗情况,因为一旦发生泄漏,会占用大量的本地内存,

堆区

堆被所有线程共享区域,在虚拟机启动时创建,唯一目的是存放对象实例。

堆区是垃圾回收的主要区域,通常情况下分为两个区块年轻代和老年代。年轻代又分为Eden区(存放新创建对象),From survivor区和To survivor区(两个survivor区保存gc后幸存下的对象)。默认情况下各自占比 8:1:1。

java虚拟机规范对这块的描述是:所有对象实例及数组都要在堆上分配内存,但随着逃逸分析技术的成熟,这个说法也不是那么绝对,但是大多数情况都是这样的。

逃逸分析:通过逃逸分析来决定某些实例或者变量是否要在堆中进行分配,如果开启了逃逸分析,即可将这些变量直接在栈上进行分配,而非堆上进行分配。这些变量的指针可以被全局所引用,或者被其它线程所引用。

在JVM运行时,可以通过配置以下参数改变整个JVM堆的配置比例

  1. JVM运行时堆的大小 -Xms,堆的最小值 -Xmx,堆空间的最大值
  2. 新生代堆空间大小调整 -XX:NewSize新生代的最小值 -XX:MaxNewSize,新生代的最大值 -XX:NewRatio,设置新生代与老年代在堆空间的大小 -XX:SurvivorRatio,新生代中Eden所占区域的大小
  3. 永久代大小调整 -XX:MaxPermSize 4.其他 -XX:MaxTenuringThreshold,设置将新生代对象转到老年代时需要经过多少次垃圾回收,但是仍然没有被回收

OutOfMemoryError报错及解决方法

  1. java.lang.OutOfMemoryError:java heap space 这种是java堆内存不够,一个原因是内存真不够,另一个原因是程序中有死循环。如果是java堆内存不够的话,可以通过调整JVM下面的配置来解决:-Xms-Xmx
  2. java.lang.OutOfMemoryError:GC overhead limit exceeded 这是JDK6新增错误类型,当GC为释放很小空间占用大量时间时抛出;一般是因为堆太小,导致异常的原因,没有足够的内存。解决方案:
    1. 查看系统是否有使用大内存的代码或死循环;
    2. 通过添加JVM配置,来限制使用内存:-XX:-UseGCOverheadLimit
  3. java.lang.OutOfMemoryError: PermGen space 这一部分用于存放Class和Meta的信息,Class在被Load的时候被放入PermGen space区域。所以如果你的APP会LOAD很多CLASS的话,就很可能出现PermGen space错误。这种是永久代内存不够,可通过调整JVM的配置: -XX:MaxPermSize-XXermSize
  4. java.lang.OutOfMemoryError: Direct buffer memory 可能原因是本身资源不够或者申请的太多内存。如果不是内存泄漏的话,可以使用参数-XX:MaxDirectMemorySize参数,或者-XX:MaxDirectMemorySize
  5. java.lang.OutOfMemoryError: unable to create new native thread 可能原因是系统内存耗尽,无法为新线程分配内存或者创建线程数超过了操作系统的限制。通过两个途径解决:
    1. 排查应用是否创建了过多的线程。通过jstack确定应用创建了多少线程
    2. 调整操作系统线程数阈值。操作系统会限制进程允许创建的线程数,使用ulimit -u命令查看限制。某些服务器上此阈值设置的过小,比如1024。一旦应用创建超过1024个线程,就会遇到java.lang.OutOfMemoryError: unable to create new native thread问题。如果是这种情况,可以调大操作系统线程数阈值。
    3. 增加机器内存。如果上述两项未能排除问题,可能是正常增长的业务确实需要更多内存来创建更多线程。如果是这种情况,增加机器内存。
    4. 减小堆内存。一个老司机也经常忽略的非常重要的知识点:线程不在堆内存上创建,线程在堆内存之外的内存上创建。所以如果分配了堆内存之后只剩下很少的可用内存,依然可能遇到java.lang.OutOfMemoryError: unable to create new native thread。考虑如下场景:系统总内存6G,堆内存分配了5G,永久代512M。在这种情况下,JVM占用了5.5G内存,系统进程、其他用户进程和线程将共用剩下的0.5G内存,很有可能没有足够的可用内存创建新的线程。如果是这种情况,考虑减小堆内存。
    5. 减小线程栈大小。线程会占用内存,如果每个线程都占用更多内存,整体上将消耗更多的内存。每个线程默认占用内存大小取决于JVM实现。可以利用-Xss参数限制线程内存大小,降低总内存消耗。例如,JVM默认每个线程占用1M内存,应用有500个线程,那么将消耗500M内存空间。如果实际上256K内存足够线程正常运行,配置-Xss256k,那么500个线程将只需要消耗125M内存。(注意,如果-Xss设置的过低,将会产生java.lang.StackOverflowError错误)。
  6. java.lang.StackOverflowError 这也内存溢出错误的一种,即线程栈的溢出,要么是方法调用层次过多(比如存在无限递归调用),要么是线程栈太小。可以通过优化程序设计,减少方法调用层次;调整-Xss参数增加线程栈大小。

垃圾回收算法

新生代采用复制算法。老年代采用标记/清除算法或标记/整理算法。由于老年代存活率高,没有额外空间给他做担保,必须使用这两种算法。

标记-清除(Mark Sweep)算法

算法分为2个阶段:

  1. 标记处需要回收的对象
  2. 回收被标记的对象

标记算法分为两种:

  1. 引用计数算法(Reference Counting)
  2. 可达性分析算法(Reachability Analysis) 由于引用技术算法无法解决循环引用的问题,所以这里使用的标记算法均为可达性分析算法。下文将介绍两种标记算法。

如图所示,当进行过标记清除算法之后,出现了大量的非连续内存。当java堆需要分配一段连续的内存给一个新对象时,发现虽然内存清理出了很多的空闲,但是仍然需要继续清理以满足“连续空间”的要求。所以说,这种方法比较基础,效率也比较低下。

复制(Copying)算法

为了解决效率与内存碎片问题,复制(Copying)算法出现了,它将内存划分为两块相等的大小,每次使用一块,当这一块用完了,就将还存活的对象复制到另外一块内存区域中,然后将当前内存空间一次性清理掉。这样的对整个半区进行回收,分配时按照顺序从内存顶端依次分配,这种实现简单,运行高效。不过这种算法将原有的内存空间减少为实际的一半,代价比较高。

从图中可以看出,整理后的内存十分规整,但是白白浪费一般的内存成本太高。然而这其实是很重要的一个收集算法,因为现在的商业虚拟机都采用这种算法来回收新生代。IBM公司的专门研究表明,新生代中的对象98%都是“朝生夕死”的,所以不需要按照1:1的比例来划分内存。HotSpot虚拟机将Java堆划分为年轻代(Young Generation)、老年代(Tenured Generation),其中年轻代又分为一块Eden和两块Survivor。

所有的新建对象都放在年轻代中,年轻代使用的GC算法就是复制算法。其中Eden与Survivor的内存大小比例为8:2,其中Eden由1大块组成,Survivor由2小块组成。每次使用内存为1Eden+1Survivor,即90%的内存。由于年轻代中的对象生命周期往往很短,所以当需要进行GC的时候就将当前90%中存活的对象复制到另外一块Survivor中,原来的Eden与Survivor将被清空。但是这就有一个问题,我们无法保证每次年轻代GC后存活的对象都不高于10%。所以在当活下来的对象高于10%的时候,这部分对象将由Tenured进行担保,即无法复制到Survivor中的对象将移动到老年代。

标记-整理算法

复制算法在极端情况下(存活对象较多)效率变得很低,并且需要有额外的空间进行分配担保。所以在老年代中这种情况一般是不适合的。

所以就出现了标记-整理(Mark-Compact)算法。与标记清除算法一样,首先是标记对象,然而第二步是将存货的对象向内存一段移动,整理出一块较大的连续内存空间。

垃圾回收的几种形式

Minor GC

在年轻代(包括Eden区和Survivor区)中的垃圾回收称之为 Minor GC。Minor GC当年轻代中eden区分配满的时候触发,只会清理年轻代。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。

Full GC

full gc是收集整个堆,包括young gen、old gen、perm gen(如果存在的话)、元空间(1.8及以上)等所有部分的模式。

  1. 手动调用System.gc()方法 [增加了full GC频率,不建议使用而是让jvm自己管理内存,可以设置-XX:+ DisableExplicitGC来禁止RMI调用System.gc]
  2. 发现perm gen(如果存在永久代的话)需分配空间但已经没有足够空间
  3. 老年代空间不足,比如说新生代的大对象大数组晋升到老年代就可能导致老年代空间不足。

mixed GC(G1特有)

混合GC 收集整个young gen以及部分old gen的GC。只有G1有这个模式

垃圾回收的两种判定方法

1. 引用计数算法

在JDK1.2之前,使用的是引用计数器算法,即当这个类被加载到内存之后,就会产生方法区,堆栈、程序计数器等一系列信息,当创建对象的时候,为这个对象在堆栈空间中分配对象,同时会产生一个引用计数器,同时引用计数器+1,当有新的引用时,引用计数器继续+1,而当其中一个引用销毁时,引用计数器-1,当引用计数器减为0的时候,标志着这个对象已经没有引用了,可以回收了!但是这样会有一个问题:当我们的代码出现这样的情况时: ObjA.obj=ObjB ObjB.obj=ObjA 这样的代码会产生如下引用情形ObjA指向ObjB,而ObjB又指向objA,这样当其他所有的引用都消失了之后,ObjA和ObjB还有一个相互的引用,也就是说两个对象的引用计数器各为1,而实际上这两个对象都已经没有额外的引用,已经是垃圾了。

2.可达性分析算法

可达性分析算法是从离散数学中的图论引入的,程序把所有的引用关系看做一张图,从一个节点GC Root开始,寻找对应的引用节点,找到这个节点之后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。

目前Java中可作为GC Root的对象有:

  1. 虚拟机栈中引用的对象(本地变量表)
  2. 方法区中静态属性引用的对象
  3. 方法区中常量引用的对象(final的常量值)
  4. 本地方法栈中引用的对象(Native对象)。

java中存在的四种引用

  1. 强引用:只要引用存在,垃圾回收器永远不会回收。
  2. 软引用:非必须引用,内存溢出之前进行回收。代码示例:
Object obj=new Object();
SoftReference<Object> sf=new SoftRerence<Object>(obj);
obj=null;
sf.get();//有时会返回null

这时候sf是对obj的一个软引用,通过sf.get()方法可以取到这个对象,当然这个对象被标记为需要回收的对象时,则返回null;

软引用主要用于用户实现类似缓存的功能,在内存不足的情况下直接通过软引用取值,无需从繁忙的真实来源查询数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真实的来源查询这些数据。

  1. 弱引用:弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。可以通过如下代码实现
Object obj=new Object();
WeakReference<Object> wf=new WeakReference<Object>(obj);
obj=null;
wf.get();//有时会返回null
wf.isEnQueued();//返回是否被垃圾回收器标记为即将回收的垃圾

弱引用是在第二次垃圾回收时回收,短时间内通过弱引用取对应的数据,可以取到,当执行过第二次垃圾回收时,将返回null。弱引用主要用于监控对象是否已经被标记为即将回收的垃圾,可以通过弱引用的isEnQueues方法返回对象是否被垃圾回收器标记。

  1. 虚引用:虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。在java中用java.lang.ref.PhantomReference类表示。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。

要注意的是,虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之 关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。可以通过如下代码实现

import java.lang.ref.ReferenceQueue;  
public class Main {  
    public static void main(String[] args) {  
        ReferenceQueue<String> queue = new ReferenceQueue<String>();  
        PhantomReference<String> pr = new PhantomReference<String>(new String("hello"), queue);  
        System.out.println(pr.get());  
    }  
}

垃圾收集器

如果说垃圾回收算法是内存回收的方法论,那么垃圾收集器就是具体实现。jvm会结合针对不同的场景及用户的配置使用不同的收集器。 年轻代收集器: Serial、ParNew、Parallel Scavenge 老年代收集器: Serial Old、Parallel Old、CMS收集器 特殊收集器: G1收集器(新型,不在年轻、老年代范畴内)

年轻代收集器

Serial

最基本、发展最久的收集器,在jdk3以前是gc收集器的唯一选择,Serial是单线程收集器,Serial收集器只能使用一条线程进行收集工作,在收集的时候必须得停掉其它线程,等待收集工作完成其它线程才可以继续工作。

虽然Serial看起来很坑,需停掉别的线程以完成自己的gc工作,但是也不是完全没用的,比如说Serial在运行在Client模式下优于其它收集器(简单高效,不过一般都是用Server模式,64bit的jvm甚至没Client模式)

串行收集器组合 Serial + Serial Old

优点:对于Client模式下的jvm来说是个好的选择。适用于单核CPU(现在基本都是多核了) 缺点:收集时要暂停其它线程,有点浪费资源,多核下显得。

ParNew收集器

可以认为是Serial的升级版,因为它支持多线程[GC线程],而且收集算法、Stop The World、回收策略和Serial一样,就是可以有多个GC线程并发运行,它是HotSpot第一个真正意义实现并发的收集器。默认开启线程数和当前cpu数量相同,如果cpu核数很多不想用那么多,可以通过-XX:ParallelGCThreads来控制垃圾收集线程的数量。

优点:

  1. 支持多线程,多核CPU下可以充分的利用CPU资源
  2. 运行在Server模式下新生代首选的收集器【重点是因为新生代的这几个收集器只有它和Serial可以配合CMS收集器一起使用】

缺点: 在单核下表现不会比Serial好,由于在单核能利用多核的优势,在线程收集过程中可能会出现频繁上下文切换,导致额外的开销。

Parallel Scavenge

采用复制算法的收集器,和ParNew一样支持多线程。 但是该收集器重点关心的是吞吐量(吞吐量 = 代码运行时间 / (代码运行时间 + 垃圾收集时间) 如果代码运行100min垃圾收集1min,则为99%) 对于用户界面,适合使用GC停顿时间短,不然因为卡顿导致交互界面卡顿将很影响用户体验。 对于后台高吞吐量可以高效率的利用cpu尽快完成程序运算任务,适合后台运算

并行收集器组合 Parallel Scavenge + Parallel Old

Parallel Scavenge注重吞吐量,所以也成为"吞吐量优先"收集器。

JDK7和8中,作为年轻代默认的收集器

老年代收集器

Serial Old

和新生代的Serial一样为单线程,Serial的老年代版本,不过它采用"标记-整理算法",这个模式主要是给Client模式下的JVM使用。 如果是Server模式有两大用途:

  1. jdk5前和Parallel Scavenge搭配使用,jdk5前也只有这个老年代收集器可以和它搭配。
  2. 作为CMS收集器的后备。

Parallel Old

支持多线程,Parallel Scavenge的老年版本,jdk6开始出现,采用"标记-整理算法"。 在jdk6以前,新生代的Parallel Scavenge只能和Serial Old配合使用,而且Serial Old为单线程Server模式下无法充分利用多核cpu,这种结合并不能让应用的吞吐量最大化。

Parallel Old的出现结合Parallel Scavenge,真正的形成“吞吐量优先”的收集器组合。 JDK7和8中,作为老年代默认的收集器

CMS收集器

CMS收集器(Concurrent Mark Sweep)是以一种获取最短回收停顿时间为目标的收集器。(重视响应,可以带来好的用户体验,被sun称为并发低停顿收集器)启用CMS:-XX:+UseConcMarkSweepGC

正如其名,CMS采用的是"标记-清除"(Mark Sweep)算法,而且是支持并发的。 它的运作分为4个阶段:

  1. 初始标记(initial mark):标记一下GC Roots能直接关联到的对象
  2. 并发标记(concurrent mark):并发标记就需要标记出GC roots 关联到的对象的引用对象有哪些。比如说 A -> B (A引用B,假设A是GC Roots关联到的对象),那么这个阶段就是标记出B对象,A对象会在初始标记中标记出来。这个过程是可以和用户线程并发执行的。所谓的并发的实现,可以有几种方式,比如说,标记了100个对象,那么就停一停,让用户线程跑一会;再比如说,标记了10ms,再停一停,之类的实现。
  3. 重新标记(remark):为了修正因并发标记期间用户程序运作而产生变动的那一部分对象的标记记录,会有些许停顿,时间上一般 初始标记 < 重新标记 < 并发标记
  4. 并发清除(sweep):将前面标记对象的内存回收,这个阶段GC线程与用户线程并发运行。

以上初始标记和重新标记需要停掉其它运行java线程。之所以说CMS的用户体验好,是因为CMS收集器的内存回收工作是可以和用户线程一起并发执行。 总体上CMS是款优秀的收集器,但是它也有缺点:

  1. cms对cpu特别敏感,cms运行线程和应用程序并发执行需要多核cpu,如果cpu核数多的话可以发挥它并发执行的优势,但是cms默认配置启动的时候垃圾线程数为 (cpu数量+3)/4,它的性能很容易受cpu核数影响,当cpu的数目少的时候比如说为为2核,如果这个时候cpu运算压力比较大,还要分一半给cms运作,这可能会很大程度的影响到计算机性能。
  2. cms无法处理浮动垃圾,可能导致Concurrent Mode Failure(并发模式故障)而触发full GC
  3. 由于cms是采用"标记-清除“算法,因此就会存在垃圾碎片的问题,为了解决这个问题cms提供了-XX:+UseCMSCompactAtFullCollection选项,这个选项相当于一个开关(默认开启),用于CMS要进行full GC时开启内存碎片合并,内存整理的过程是无法并发的,且开启这个选项会影响性能(比如停顿时间变长)

Concurrent mode failure:如果CMS回收过程还没有执行完,老年代的剩余空间就用完了,或者,当前老年代空间不能满足一次内存分配请求(可能对象较大),那么此时将触发担保机制,停顿所有用户线程,串行老年代收集器将会以STW的方式进行一次GC,从而造成较大停顿时间;

浮动垃圾:由于cms支持运行的时候用户线程也在运行,程序运行的时候会产生新的垃圾,这里产生的垃圾就是浮动垃圾,cms无法当次处理,得等下次才可以。

假如有一个对象GC线程没有标记(用户线程之前没在用),然后轮到了用户线程,用户线程说,这个对象我重新又要用了,不要把这个对象GC掉,这个时候怎么办?假如这个时候处理不了,还是GC了,那么程序就直接报错了,这个是不允许的,解决办法可以百度搜索“cms 三色标记法”获取答案

G1收集器

G1(garbage first)收集器是当前最为前沿的收集器之一(1.7以后才开始有),同cms一样也是关注降低延迟,是用于替代cms功能更为强大的新型收集器,因为它解决了cms产生空间碎片等一系列缺陷。

当G1确定有必要进行垃圾回收时,它会先收集存活数据最少的区域(垃圾优先) g1的特别之处在于它强化了分区,弱化了分代的概念,是区域化、增量式的收集器,它不属于新生代也不属于老年代收集器。用到的算法为标记-清理、复制算法

G1是区域化的,它将java堆内存划分为若干个大小相同的区域"region“,JVM可以设置每个region的大小(1-32m,大小得看堆内存大小,必须是2的幂),它会根据当前的堆内存分配合理的region大小。

g1通过并发(并行)标记阶段查找老年代存活对象,通过并行复制压缩存活对象(这样可以省出连续空间供大对象使用)。g1将一组或多组区域中存活对象以增量并行的方式复制到不同区域进行压缩,从而减少堆碎片,目标是尽可能多回收堆空间,且尽可能不超出暂停目标以达到低延迟的目的。g1提供三种垃圾回收模式 young gc、mixed gc 和 full gc,不像其它的收集器,根据区域而不是分代,新生代老年代的对象它都能回收。几个重要的默认值,更多的查看官方文档oracle官方g1中文文档 g1是自适应的回收器,提供了若干个默认值,无需修改就可高效运作

  • -XX:G1HeapRegionSize=n 设置g1 region大小,不设置的话自己会根据堆大小算,目标是根据最小堆内存划分2048个区域
  • -XX:MaxGCPauseMillis=200 最大停顿时间 默认200毫秒

JDK9中,G1作为默认的收集器 JDK7/8,默认关闭的,开启选项 -XX:+UseG1GC

更详细的G1垃圾回收介绍,请查看这篇文章: https://blog.csdn.net/coderlius/article/details/79272773

本文分享自微信公众号 - 全菜工程师小辉(mseddl)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-06-26

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券