阅读GC日志是一件很有意义的事情,通过阅读GC日志,我们可以了解到JVM的内存分配和回收策略。本文不打算讨论JVM的构成和垃圾回收算法,阅读本文前需要读者有这方面的知识储备。
public class Test01 { public static void main(String[] args) { byte[] b1 = new byte[4*1024*1024]; byte[] b2 = new byte[4*1024*1024]; byte[] b3 = new byte[4*1024*1024]; byte[] b4 = new byte[4*1024*1024]; } }
JVM启动参数为:
-Xms5m -Xmx20m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags
运行代码,我们会得到类似下面的GC日志:
[GC (Allocation Failure) [PSYoungGen: 796K->504K(1536K)] 14108K->13896K(15360K), 0.0042751 secs] [Times: user=0.06 sys=0.00, real=0.00 secs] [Full GC (Ergonomics) [PSYoungGen: 504K->502K(1536K)] [ParOldGen: 13392K->13368K(13824K)] 13896K->13870K(15360K), [Metaspace: 2735K->2735K(1056768K)], 0.0073296 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
详细解释如下,为了便于阅读,我把对应的解释标记成了不同的颜色:
GC & Full GC :表示垃圾回收时的停顿类型,GC针对年轻代;Full GC针对老年代,说明这次GC是发生了Stop-The-World,此时收集会暂停所有工作线程,会进行堆内存全扫描。这也是为什么不建议在程序中显式调用System.gc(),因为它会触发Full GC。
[PSYoungGen: 796K->504K(1536K)] :796K表示GC前的Young区占用;504K表示GC后的Young区占用;1536K表示Yong区总大小。
14108K->13896K(15360K) :14108K表示YoungGC前的整个堆的占用;13896K表示YoungGC后的整个堆的占用;15360K表示整个堆的大小
0.0042751 secs :YoungGC的耗时。
[Times: user=0.06 sys=0.00, real=0.00 secs] :user用户态消耗CPU时间;sys内核消耗CPU时间;real标识从开始到结束所经过的墙钟时间(Wall Clock Time)。墙钟时间的意思是,从进程从开始运行到结束,时钟走过的时间,这其中包含了进程在阻塞和等待状态的时间,包含了各种非运算耗时,比如磁盘I/O耗时,线程阻塞等待时间等,而user和sys不包含这些,但当系统有多CPU或者多核的话,多线程操作会叠加这些CPU时间,所以user或sys时间超过real时间是完全正常的。
我们还可以看到堆内存信息,类似如下:
Heap PSYoungGen total 1536K, used 531K [0x00000000ff980000, 0x00000000ffc80000, 0x0000000100000000) eden space 1024K, 3% used [0x00000000ff980000,0x00000000ff987c68,0x00000000ffa80000) from space 512K, 97% used [0x00000000ffa80000,0x00000000ffafd208,0x00000000ffb00000) to space 512K, 0% used [0x00000000ffc00000,0x00000000ffc00000,0x00000000ffc80000) ParOldGen total 13824K, used 13357K [0x00000000fec00000, 0x00000000ff980000, 0x00000000ff980000) object space 13824K, 96% used [0x00000000fec00000,0x00000000ff90b798,0x00000000ff980000) Metaspace used 2767K, capacity 4486K, committed 4864K, reserved 1056768K class space used 298K, capacity 386K, committed 512K, reserved 1048576K
详细解释如下,为了便于阅读,我把对应的解释标记成了不同的颜色:
PSYoungGen :垃圾回收类型,PS是Parallel Scavenge收集器的缩写,表示年轻代使用了Parallel Scavenge,它对应的新生代称为PSYoungGen,新生代又分为eden space、from space和to space这三部分。
需要注意的是,不同的新生代垃圾回收算法对应的名字也不相同,会有如下三种新生代垃圾回收算法,读者可以自行修改下面给出的参数,运行感受一下:
GC[ParNew,表示使用的是parNew收集器,对应的参数为 -XX:+UseSerialGC。
GC[DefNew,表示用的是serial收集器,对应的参数为 -XX:+UseParNewGC。
GC[PSYoungGen,表示用的是Parallel Scavenge收集器,对应的参数为 -XX:+UseParallelGC。
ParOldGen :Parallel Scavenge收集器配套的老年代。
它与新生代一样,不同的垃圾回收算法对应的名字也不相同:
tenured generation,表示使用的是Serial Old收集器。
ParOldGen,表示使用的是Parallel Old收集器。
concurrent mark-sweep generation,表示使用的是CMS收集器。
Metaspace :永久代。
total & used :总的空间和用掉的空间。
1、JVM采用的是分代收集,不同的对象生命周期是不一样的,使用不同垃圾回收算法也不同,这是为了提高回收效率。
2、没有最好的收集器,也没有万能的收集器,只有最适合的收集器。
3、可以根据日志中分区的名称推断出使用的垃圾回收器。
4、垃圾回收期之间可以组合使用,在公司中比较常见的搭配为 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC,新生代使用ParNew,老年代使用CMS。当然使用CMS的前提是,可以提供更大的内存和更多的CPU(堆最少为3G内存以上,建议最小4G),也就是硬件比较好。
5、使用CMS垃圾回收时,通常还会配置如下几个参数:
-XX:+UseCMSInitiatingOccupancyOnly:
仅仅使用手动定义(CMSInitiatingOccupancyFraction)开始CMS收集。
-XX:CMSInitiatingOccupancyFraction=70:
CMS在年老代, 使用70%后开始CMS收集,如果你的年老代增长不是那么快,并且希望降低CMS次数的话,可以适当调高此值。
-XX:+CMSParallelRemarkEnabled:开启并行Remark,默认是开启的,所以可以不设置此参数。
6、经过我的测试,G1并没有比CMS表现更好,相反,CMS无论是吞吐率还是停顿时间,都比G1表现更好,不能相信网上的大话。当然这并不一定是CMS比G1更好,可能某些情况下,G1可能比CMS会好,只是我现在还不确定,如果有了解的同学欢迎留言。
在文章的最后,我为大家准备了一份JVM参数模板,供大家参考,关注微信公众号 “大熊的技术轶事”或者扫描下面二维码,回复“JVM参数”即可获取。