- 一、JVM调试
- 1.1 JDK自带命令
- 1.1.1 jps:虚拟机进程状况工具
- 1.1.2 jstat:虚拟机统计信息监视工具
- 1.1.3 jinfo:Java配置信息工具
- 1.1.4 jmap:Java内存映像工具
- 1.1.5 jstack:Java堆栈跟踪工具
- 1.2 JDK的可视化工具JConsole
- 二、JVM参数
- 2.1 常见垃圾回收器组合参数
- 2.2 通用JVM参数
- 2.3 Parallel常用参数
- 2.4 CMS常用参数
- 2.5 G1常用参数
- 2.6 JVM参数使用示例
- 三、GC日志
- 3.1 CMS日志
- 3.2 G1日志
Sun JDK监控和故障处理命令如:
- 1、jps
JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。 - 2、jstat
JVM statistics Monitoring,用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。 - 3、jmap
JVM Memory Map,用于生成heap dump文件。 - 4、jhat
JVM Heap Analysis Tool,与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看。 - 5、jstack
用于生成java虚拟机当前时刻的线程快照。 - 6、jinfo
JVM Configuration info,用于实时查看和调整虚拟机运行参数。
查看当前java进程,jsp命令格式:
jps [options] [hostid]
jps执行样例:
D:DevelopJavajdk1.6.0_21bin>jps-l 2388 D:Developglassfishbin..modulesadmin-cli.jar 2764 com.sun.enterprise.glassfish.bootstrap.ASMain 3788 sun.tools.jps.Jps1.1.2 jstat:虚拟机统计信息监视工具
用于监视虚拟机各种运行状态信息,jstat命令格式为:
jstat [option vmid [interval [s|ms] [count]]]
假设需要每250毫秒查询一次进程2764垃圾收集状况,一共查询20次,那命令应当是:
jstat-gc 2764 250 201.1.3 jinfo:Java配置信息工具
实时地查看和调整虚拟机各项参数,jinfo命令格式:
jinfo [option] pid
执行样例:查询CMSInitiatingOccupancyFraction参数值:
C:>jinfo-flag CMSInitiatingOccupancyFraction 1444 -XX:CMSInitiatingOccupancyFraction=851.1.4 jmap:Java内存映像工具
jmap(Memory Map for Java)命令用于生成堆转储快照(可以将VM 中的heap,以二进制输出成文本)。jmap的作用并不仅仅是为了获取dump文件,它还可以查询finalize执行队列、Java堆和永久代的详细信息,如空间使用率、当前用的是哪种收集器等。
jmap命令格式:
jmap [option] vmid
使用jmap生成dump文件示例:
C:UsersIcyFenix>jmap-dump:format=b,file=eclipse.bin 3500 Dumping heap to C:UsersIcyFenixeclipse.bin…… Heap dump file created1.1.5 jstack:Java堆栈跟踪工具
jstack(Stack Trace for Java)命令用于生成虚拟机当前时刻的线程快照(一般称为threaddump或者javacore文件)。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等都是导致线程长时间停顿的常见原因。线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做些什么事情,或者等待着什么资源。
jstack用于打印出给定的java进程ID或core file或远程调试服务的Java堆栈信息。
jstack命令格式:
jstack [option] vmid1.2 JDK的可视化工具JConsole
JConsole(Java Monitoring and Management Console)是一种基JMX(Java Management Extensions,即Java管理扩展)的可视化监视、管理工具。
通过JDK/bin目录下的“jconsole.exe”启动JConsole后,将自动搜索出本机运行的所有虚拟机进程。
“概述”页签显示的是整个虚拟机主要运行数据的概览,其中包括“堆内存使用情况”、“线程”、“类”、“CPU使用情况”4种信息的曲线图,这些曲线图是后面“内存”、“线程”、“类”页签的信息汇总,具体内容将在后面介绍。
- 1、内存监控
“内存”页签相当于可视化的jstat命令,用于监视受收集器管理的虚拟机内存(Java堆和永久代)的变化趋势。我们通过运行代码清单4-8中的代码来体验一下它的监视功能。运行时设置的虚拟机参数为:-Xms100m-Xmx100m-XX:+UseSerialGC,这段代码的作用是以64KB/50毫秒的速度往Java堆中填充数据,一共填充1000次,使用JConsole的“内存”页签进行监视,观察曲线和柱状指示图的变化:
static class OOMObject{
public byte[]placeholder=new byte[64*1024];
}
public static void fillHeap(int num) throws InterruptedException{
List list = new ArrayList();
for(int i=0;i
//稍作延时,令监视曲线的变化更加明显
Thread.sleep(50);
list.add(new OOMObject());
}
System.gc();
}
public static void main(String[]args) throws Exception{
fillHeap(1000);
}
程序运行后,在“内存”页签中可以看到内存池Eden区的运行趋势呈现折线状,如图4-6所示。而监视范围扩大至整个堆后,会发现曲线是一条向上增长的平滑曲线。并且从柱状图可以看出,在1000次循环执行结束,运行了System.gc()后,虽然整个新生代Eden和Survivor区都基本被清空了,但是代表老年代的柱状图仍然保持峰值状态,说明被填充进堆中的数据在System.gc()方法执行之后仍然存活。笔者的分析到此为止,现提两个小问题供读者思考一下,答案稍后给出。
1)虚拟机启动参数只限制了Java堆为100MB,没有指定-Xmn参数,能否从监控图中估计出新生代有多大?
2)为何执行了System.gc()之后,图4-6中代表老年代的柱状图仍然显示峰值状态,代码需要如何调整才能让System.gc()回收掉填充到堆中的对象?
问题1答案:图4-6显示Eden空间为27328KB,因为没有设置-XX:SurvivorRadio参数,所以Eden与Survivor空间比例为默认值8:1,整个新生代空间大约为27 328KB×125%=34160KB。
问题2答案:执行完System.gc()之后,空间未能回收是因为List<OOMObject>list对象仍然存活,fillHeap()方法仍然没有退出,因此list对象在System.gc()执行时仍然处于作用域之内。如果把System.gc()移动到fillHeap()方法外调用就可以回收掉全部内存。
- 2、线程监控
如果上面的“内存”页签相当于可视化的jstat命令的话,“线程”页签的功能相当于可视化的jstack命令,遇到线程停顿时可以使用这个页签进行监控分析。前面讲解jstack命令的时候提到过线程长时间停顿的主要原因主要有:等待外部资源(数据库连接、网络资源、设备资源等)、死循环、锁等待(活锁和死锁)。通过以下代码别演示一下这几种情况。
public static void createBusyThread(){
Thread thread=new Thread(new Runnable(){
@Override
public void run(){
while(true);//第41行
}
},"testBusyThread");
thread.start();
}
public static void createLockThread(final Object lock){
Thread thread=new Thread(new Runnable(){
@Override
public void run(){
synchronized(lock){
try{
lock.wait();
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
},"testLockThread");
thread.start();
}
public static void main(String[]args)throws Exception{
BufferedReader br = new BufferedReader(
new InputStreamReader(System.in));
br.readLine();
createBusyThread();
br.readLine();
Object obj=new Object();
createLockThread(obj);
}
程序运行后,首先在“线程”页签中选择main线程,如图4-7所示。堆栈追踪显示BufferedReader在readBytes方法中等待System.in的键盘输入,这时线程为Runnable状
态,Runnable状态的线程会被分配运行时间,但readBytes方法检查到流没有更新时会立刻归还执行令牌,这种等待只消耗很小的CPU资源。
接着监控testBusyThread线程,如图4-8所示,testBusyThread线程一直在执行空循环,从堆栈追踪中看到一直在MonitoringTest.java代码的41行停留,41行为:while(true)。这时候线程为Runnable状态,而且没有归还线程执行令牌的动作,会在空循环上用尽全部执行时间直到线程切换,这种等待会消耗较多的CPU资源。
图4-9显示testLockThread线程在等待着lock对象的notify或notifyAll方法的出现,线程这时候处于WAITING状态,在被唤醒前不会被分配执行时间。
testLockThread线程正在处于正常的活锁等待,只要lock对象的notify()或notifyAll()方法被调用,这个线程便能激活以继续执行。代码清单4-10演示了一个无法再被激活的死锁等待。
static class SynAddRunalbe implements Runnable{
int a,b;
public SynAddRunalbe(int a,int b){
this.a=a;
this.b=b;
}
@Override
public void run(){
synchronized(Integer.valueOf(a)){
synchronized(Integer.valueOf(b)){
System.out.println(a+b);
}
}
}
}
public static void main(String[]args){
for(int i=0;i<100;i++){
new Thread(new SynAddRunalbe(1,2)).start();
new Thread(new SynAddRunalbe(2,1)).start();
}
}
这段代码开了200个线程去分别计算1+2以及2+1的值,其实for循环是可省略的,两个线程也可能会导致死锁,不过那样概率太小,需要尝试运行很多次才能看到效果。一般的话,带for循环的版本最多运行2~3次就会遇到线程死锁,程序无法结束。造成死锁的原因是Integer.valueOf()方法基于减少对象创建次数和节省内存的考虑,[-128,127]之间的数字会被缓存 [3] ,当valueOf()方法传入参数在这个范围之内,将直接返回缓存中的对象。也就是说,代码中调用了200次Integer.valueOf()方法一共就只返回了两个不同的对象。假如在某个线程的两个synchronized块之间发生了一次线程切换,那就会出现线程A等着被线程B持有的Integer.valueOf(1),线程B又等着被线程A持有的Integer.valueOf(2),结果出现大家都跑不下去的情景。
出现线程死锁之后,点击JConsole线程面板的“检测到死锁”按钮,将出现一个新的“死锁”页签,如图4-10所示。
图4-10中很清晰地显示了线程Thread-43在等待一个被线程Thread-12持有Integer对象,而点击线程Thread-12则显示它也在等待一个Integer对象,被线程Thread-43持有,这样两个线程就互相卡住,都不存在等到锁释放的希望了。
JVM的参数分为三种:
- 标准参数,-开头的参数,所有版本JDK都支持,直接输入java查看;
- 非标准参数, -X开头的参数,输入java -X 查看,不同的JVM实现不同;
- 不稳定参数,-XX开头的参数,每个版本可能不同,使用java -XX:+PrintFlagsFinal查看。
- -XX:+UseSerialGC = Serial + Serial Old
适用于小型程序。默认情况下不会是这种选项,HotSpot会根据计算及配置和JDK版本自动选择收集器。 - -XX:+UseParNewGC = ParNew + SerialOld
在一般未明确指定Old区垃圾回收器时,就代表用的是SerialOld。这个组合已经很少用(在某些版本中已经废弃)。 - -XX:+UseConc(urrent)MarkSweepGC = ParNew + CMS + Serial Old
红色的字体代表在某些JDK版本上要加上,示例的在JDK1.8上不需要加红色的。 - -XX:+UseParallelGC = Parallel Scavenge + Parallel Old
JDK1.8默认的垃圾回收器组合。 - -XX:+UseParallelOldGC = Parallel Scavenge + Parallel Old
- -XX:+UseG1GC = G1
- -Xmn -Xms -Xmx -Xss
年轻代 最小堆 最大堆 栈空间,示例:
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k
- -XX:+UseTLAB
使用TLAB,默认打开【一般不需要调整】。 - -XX:+PrintTLAB
打印TLAB的使用情况【一般不需要调整】。 - -XX:TLABSize
设置TLAB大小【一般不需要调整】。 - -XX:+DisableExplicitGC
使显式调用System.gc()时不起作用 (System.gc()会产生FGC)。
System.gc()默认会触发一次Full GC,如果在代码中不小心调用了System.gc()会导致JVM间歇性的暂停。
- -XX:+PrintGC
打印GC信息。 - -XX:+PrintGCDetails
打印GC详细信息。 - -XX:+PrintHeapAtGC
GC的时候打印堆栈情况。 - -XX:+PrintGCTimeStamps
打印GC发生时的时间戳。 - -XX:+PrintGCApplicationConcurrentTime
打印应用程序时间(重要性低)。 - -XX:+PrintGCApplicationStoppedTime
打印应用程序暂停时长(重要性低)。 - -XX:+PrintReferenceGC
记录回收了多少种不同引用类型的引用(重要性低)。 - -verbose:class
打印类加载详细过程。 - -XX:+PrintVMOptions
可以在程序运行时,打印虚拟机接受的命令行显式参数。 - -XX:+PrintFlagsFinal / -XX:+PrintFlagsInitial
打印出最终的参数/初始的参数。 - -Xloggc:opt/log/gc.log
生成GC日志。 - -XX:MaxTenuringThreshold
升代年龄(该参数主要是控制新生代需要经历多少次GC晋升到老年代中的最大阈值),最大值15。 - -XX:PreBlockSpin
锁自旋次数。 - -XX:CompileThreshold
热点代码检测参数。
2.3 Parallel常用参数以上这些常用JVM参数,不建议更改。
- -XX:SurvivorRatio
Eden和S区的比例【一般不需要调整】。
该值默认为8,即Eden占新生代的8/10,From幸存区和To幸存区各占新生代的1/10。 - -XX:PreTenureSizeThreshold
大对象阈值,即大于这个值的参数直接在老年代分配。 - -XX:+ParallelGCThreads
并行收集器的线程数,同样适用于CMS,一般设为和CPU核数相同。 - -XX:+UseAdaptiveSizePolicy
自动选择各区大小比例。
开启:-XX:+UseAdaptiveSizePolicy;
关闭:-XX:-UseAdaptiveSizePolicy
在JDK1.8 中,如果使用CMS,无论UseAdaptiveSizePolicy如何设置,都会将UseAdaptiveSizePolicy设置为false;不过不同版本的JDK存在差异;
UseAdaptiveSizePolicy不要和SurvivorRatio参数显示设置搭配使用,一起使用会导致参数失效;
由于AdaptiveSizePolicy会动态调整 Eden、Survivor 的大小,有些情况存在Survivor 被自动调为很小,比如十几MB甚至几MB的可能,这个时候YGC回收掉 Eden区后,还存活的对象进入Survivor 装不下,就会直接晋升到老年代,导致老年代占用空间逐渐增加,从而触发FULL GC,如果一次FULL GC的耗时很长(比如到达几百毫秒),那么在要求高响应的系统就是不可取的。
- -XX:+UseConcMarkSweepGC
使用CMS垃圾回收器。 - -XX:ParallelCMSThreads
CMS线程数量。 - -XX:CMSInitiatingOccupancyFraction
使用多少比例的老年代后开始CMS收集,默认是68%(近似值),如果频繁发生SerialOld卡顿,应该调小,(频繁CMS回收)。 - -XX:+UseCMSCompactAtFullCollection
在FGC时进行压缩。 - -XX:CMSFullGCsBeforeCompaction
多少次FGC之后进行压缩。 - -XX:+CMSClassUnloadingEnabled
回收方法区不用的class。 - -XX:CMSInitiatingPermOccupancyFraction
达到什么比例时进行Perm回收。 - -XX:GCTimeRatio
设置GC时间占用程序运行时间的百分比。 - -XX:MaxGCPauseMillis
停顿时间,是一个建议时间,GC会尝试用各种手段达到这个时间,比如减小年轻代。
- -XX:+UseG1GC
使用GC垃圾回收器。 - -XX:MaxGCPauseMillis
每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。 - -XX:GCPauseIntervalMillis
GC的间隔时间。 - -XX:+G1HeapRegionSize
一个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围从1M到32M,且是2的指数(1 2 4 8 16 32)。如果不设定,那么G1会根据Heap大小自动决定。。
随着size增加,垃圾的存活时间更长,GC间隔更长,但每次GC的时间也会更长
(ZGC对此做了改进,是动态区块大小)。 - -XX:G1NewSizePercent
新生代最小比例,默认为5%。 - -XX:G1MaxNewSizePercent
新生代最大比例,默认为60%。 - -XX:GCTimeRatio
GC时间建议比例,G1会根据这个值调整堆空间,值为0-100的整数。
常见的调优用的是 -X命令。
如:java -XX:+PrintFlagsFinal命令可以查看所有的XX参数,如果在Linux环境下,可以+ |grep CMS 来过滤指定命令。
示例:
package Basic;
import java.util.List;
import java.util.LinkedList;
public class HelloGC {
public static void main(String[] args) {
System.out.println("HelloGC!");
List list = new LinkedList();
for(;;) {
byte[] b = new byte[1024*1024];
list.add(b);
}
}
}
在Eclipse上编写此程序会,会在项目的bin目录下生成对应的HelloGC.class文件,如在"E:WorkSpaceHelloWorldbinBasic"目录:
在CMD窗口,切换到bin目录:
然后就可以调试各种参数。当然也可以在Eclipse中设置JVM参数,此处以CMD方式为例。执行java -XX:+PrintCommandLineFlags Basic.HelloGC命令,可以看默认的虚拟机参数设置:
图中的现象是:内存溢出(要区分内存泄露与内存溢出)。图中的参数:
-XX:InitialHeapSize=65738496 --> 起始堆大小
-XX:MaxHeapSize=1051815936 --> 最大堆大小
-XX:+UseCompressedClassPointers --> 之前提过的类型指针压缩
-XX:+UseCompressedOops --> 普通指针压缩
假如要设置参数,可以使用类似命令:java -Xmn10M -Xms40M -Xmx60M -XX:+PrintCommandLineFlags -XX:+PrintGC Basic.HelloGC。
-Xmn 新生代大小
-Xms 初始堆大小
-Xmx 最大堆大小
-XX:+PrintGC 显示GC回收信息
在打印GC方面,除了PrintGC,还有一些类似的命令:
PrintGCDetails 打印更详细的信息
PrintGCTimeStamps 打印GC时的时间
PrintGCCauses 打印GC产生的原因
上面命令的执行结果:
发现最大堆大小、最小堆大小、年轻代大小已经设置成功。
如果使用CMS的话,打印的信息会更详细一些,命令:java -XX:+UseConcMarkSweepGC -XX:+PrintCommandLineFlags -XX:+PrintGC Basic.HelloGC,结果:
要查看GC日志的详细意义,可以参考:
一般未显示"Full GC",就代表是YGC;
4544K->259K,分别代表回收前后的年轻代大小;6144K代表总的年轻代大小;
后面的代表此次回收所用的时间;
接着的4544K->4356K代表整个堆回收前后的大小;
19840K代表整个堆的大小;
后面的时间分别表示占了用户态多少时间、内核态多少时间、总共多少时间。
发生OOM时的详细dump信息:
total = eden + 1个survivor。
eden space 5632K, 94% used [0x00000000ff980000,0x00000000ffeb3e28,0x00000000fff00000)
3.1 CMS日志后面的内存地址指的是,起始地址,使用空间结束地址,整体空间结束地址。从第一个地址到第三个地址之间的长度是5632K。从第一个地址到第二个地址代表使用完的地址,占总地址的94%。
Mataspeace,元数据区。最后一个是全元数据区所预留的全部空间,倒数第二个是已经占用的空间,倒数第一个是目前的容量是多少,第一个是真正使用的空间。
测试命令:
java -Xms20M -Xmx20M -XX:+PrintGCDetails -XX:+UseConcMarkSweepGC com.test.jvm.gc.T15_FullGC_Problem01
小例子:
[GC (Allocation Failure) [ParNew: 6144K->640K(6144K), 0.0265885 secs] 6585K->2770K(19840K), 0.0268035 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] ParNew:年轻代收集器 6144->640:收集前后的对比 (6144):整个年轻代容量 6585 -> 2770:整个堆的使用情况 (19840):整个堆大小
[GC (CMS Initial Mark) [1 CMS-initial-mark: 8511K(13696K)] 9866K(19840K), 0.0040321 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
//CMS Initial Mark : 初始标记
//8511 (13696) : 老年代使用(最大)
//9866 (19840) : 整个堆使用(最大)
[CMS-concurrent-mark-start]
[CMS-concurrent-mark: 0.018/0.018 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]
//CMS-concurrent-mark : 并发标记
//这里的时间意义不大,因为是并发执行
[CMS-concurrent-preclean-start]
[CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
//标记Card为Dirty,也称为Card Marking
[GC (CMS Final Remark) [YG occupancy: 1597 K (6144 K)][Rescan (parallel) , 0.0008396 secs][weak refs processing, 0.0000138 secs][class unloading, 0.0005404 secs][scrub symbol table, 0.0006169 secs][scrub string table, 0.0004903 secs][1 CMS-remark: 8511K(13696K)] 10108K(19840K), 0.0039567 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
// CMS Final Remark : 重新标记
//STW阶段,YG occupancy:年轻代占用及容量
//[Rescan (parallel):STW下的存活对象标记
//weak refs processing: 弱引用处理
//class unloading: 卸载用不到的class
//scrub symbol(string) table:
//cleaning up symbol and string tables which hold class-level metadata and
//internalized string respectively
//CMS-remark: 8511K(13696K): 阶段过后的老年代占用及容量
//10108K(19840K): 阶段过后的堆占用及容量
[CMS-concurrent-sweep-start]
[CMS-concurrent-sweep: 0.005/0.005 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
//标记已经完成,进行并发清理
[CMS-concurrent-reset-start]
[CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
//重置内部结构,为下次GC做准备
对于CMS而言,在日志中更多的是,关注GC是否频繁,和耗费的时间在不在允许范围之内。
3.2 G1日志 G1可以设置参数,表明每次回收建议的暂停时间,虚拟机参考这个时间,动态调整年轻代大小(以尽量拟合到设置的时间)。
G1的调优目标:尽量不要FGC。
在G1的日志中,YGC和Mixed GC常常是混在一起的。
[GC pause (G1 Evacuation Pause) (young) (initial-mark), 0.0015790 secs]
//young -> 年轻代 Evacuation-> 复制存活对象 (即表示是YGC)
//initial-mark 混合回收的阶段,这里是YGC混合老年代回收
[Parallel Time: 1.5 ms, GC Workers: 1] //一个GC线程
[GC Worker Start (ms): 92635.7]
[Ext Root Scanning (ms): 1.1]
[Update RS (ms): 0.0]
[Processed Buffers: 1]
[Scan RS (ms): 0.0]
[Code Root Scanning (ms): 0.0]
[Object Copy (ms): 0.1]
[Termination (ms): 0.0]
[Termination Attempts: 1]
[GC Worker Other (ms): 0.0]
[GC Worker Total (ms): 1.2]
[GC Worker End (ms): 92636.9]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.0 ms]
[Other: 0.1 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.0 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.0 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
[Eden: 0.0B(1024.0K)->0.0B(1024.0K) Survivors: 0.0B->0.0B Heap: 18.8M(20.0M)->18.8M(20.0M)]
//这是此次回收的一个总结:18.8M->18.8M,代表没有回收,有内存泄露。
[Times: user=0.00 sys=0.00, real=0.00 secs]
//以下是混合回收其他阶段
[GC concurrent-root-region-scan-start]
[GC concurrent-root-region-scan-end, 0.0000078 secs]
[GC concurrent-mark-start]
//无法evacuation(复制),进行FGC
[Full GC (Allocation Failure) 18M->18M(20M), 0.0719656 secs]
[Eden: 0.0B(1024.0K)->0.0B(1024.0K) Survivors: 0.0B->0.0B Heap: 18.8M(20.0M)->18.8M(20.0M)], [Metaspace: 38
76K->3876K(1056768K)] [Times: user=0.07 sys=0.00, real=0.07 secs]



