简介
最近用java写了一个微型的网络程序,没进行GC调优的情况下就已经满足性能要求。不过想通过对这个程序调优,再熟悉一下GC调优流程。
通过活跃数据大小设置堆内存大小
活跃数据大小指的是应用程序员运行于稳定态时,长期存活的对象在Java堆中占用的空间大小。所以可以在程序稳定运行时,进行多次Full GC,之后再查看Java堆的占用情况来测量活跃数据大小。
调优过程需要一步一步多次确定更合适的值,JVM的初始参数和版本信息如下(堆大小按感觉先随意分配256M,如果有溢出再调整):
-Xms256m
-Xmx256m
-XX:+PrintGCTimeStamps
-XX:+PrintGCDetails
-XX:MaxDirectMemorySize=512m
-XX:+PrintCommandLineFlags
-XX:+UseParallelGC
java version "1.8.0_241"
Java(TM) SE Runtime Environment (build 1.8.0_241-b07)
Java HotSpot(TM) 64-Bit Server VM (build 25.241-b07, mixed mode)
其中-XX:+PrintGCDetails
参数表示打印垃圾回收情况,-XX:+PrintGCTimeStamps
参数表示每次打印的时候带上JVM启动到当前时间的秒数,也可以使用-XX:+PrintGCDateStamps
打印日历时间。-XX:+UseParallelGC
是java8在Windows64上的默认选择,表示同时启用多线程新生代和多线程老年代的Parallel垃圾收集器[1],这一点可能跟旧版本的java不一样(对旧版本java的没太多研究)。
用这个程序来看平均速率在200KB/S左右的直播。运行一段时间之后,程序达到稳定状态。由于这个程序在256M内存下,不经常发生Full GC,所以在程序稳定运行的时候,用visualvm
触发多次Full GC。每次Full GC的结果比较相似,选取其中一个显示如下(已调整换行):
1887.144: [Full GC (System.gc())
[PSYoungGen: 224K->0K(86528K)]
[ParOldGen: 7438K->6220K(175104K)]
7662K->6220K(261632K),
[Metaspace: 19356K->19356K(1067008K)], 0.0254513 secs]
[Times: user=0.11 sys=0.00, real=0.02 secs]
日志显示,Full GC之后的老年代占用6220K,所以活跃数据大约为6M。根据通用法则,堆的初始值和最大值一般设置为老年代活跃数据大小的3-4倍。这里按4倍计算,设置堆的初始值和最大值为24M:-Xms24m -Xmx24m
。
而新生代的空间应该为老年代活跃数据的1-1.5倍。这里按1.5倍计算,设置新生代大小为9M:-Xmn9m
。调整后使用的JVM参数设置如下:
-Xms24m
-Xmx24m
-Xmn9m
-XX:+PrintGCTimeStamps
-XX:+PrintGCDetails
-XX:MaxDirectMemorySize=512m
-XX:+PrintCommandLineFlags
-XX:+UseParallelOldGC
验证设置
经过设置之后,再次让程序运行到稳定的状态。其中Minor GC的频率比较稳定,摘取结果如下:
......
4083.597: [GC (Allocation Failure) [PSYoungGen: 8320K->64K(8704K)] 19028K->10804K(24064K), 0.0013510 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
4085.883: [GC (Allocation Failure) [PSYoungGen: 8256K->64K(8704K)] 18996K->10812K(24064K), 0.0014869 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
4088.088: [GC (Allocation Failure) [PSYoungGen: 8256K->96K(8704K)] 19004K->10852K(24064K), 0.0014876 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
4090.371: [GC (Allocation Failure) [PSYoungGen: 8288K->96K(8704K)] 19044K->10860K(24064K), 0.0017656 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
......
Minor GC持续时间在0.001秒左右,频率是每2.2秒一次。
Full GC日志摘取结果如下:
......
1347.321: [Full GC (Ergonomics) [PSYoungGen: 223K->0K(8704K)] [ParOldGen: 15318K->8750K(15360K)] 15542K->8750K(24064K), [Metaspace: 19171K->19049K(1067008K)], 0.0407358 secs] [Times: user=0.06 sys=0.00, real=0.04 secs]
...
2599.391: [Full GC (Ergonomics) [PSYoungGen: 96K->0K(8704K)] [ParOldGen: 15359K->5356K(15360K)] 15455K->5356K(24064K), [Metaspace: 19315K->19304K(1067008K)], 0.0344051 secs] [Times: user=0.06 sys=0.00, real=0.04 secs]
......
Full GC持续时间在0.04秒左右,频率是每隔20分钟一次。
延迟
对这个程序来说,上面数据显示的延迟可以接受。一般来说,如果延迟没有满足需求,可以再次分别调整新生代和老年代的大小。当调整之后,Full GC持续时间依旧过长时,可以改用并发垃圾收集器[2]。在java8中,并发收集器主要有两个:CMS和G1。G1的目的是替换掉CMS[3],而CMS在java8中已经被标记为过时,并在之后的发行版中删除。
使用G1
G1的调优目前没有太多研究,直接把-XX:+UseParallelGC
参数替换成-XX:+UseG1GC
,删除之前的新生代设置-Xmn
,来启用G1垃圾收集器,直观的感受一下。
长时间观看平均速率在200KB/S左右的直播,发现一直没有发生Full GC,老年代空间一直没有被塞满,维持在一定水平的占用。
总结
通过这次调优,看到配置这么小的堆内存就能满足这个程序的正常运行,还是有点不习惯,毕竟平常配置服务器都是按GB来配置。
参考资料
[1] java8的垃圾收集器介绍:https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/collectors.html
[2] java8的并发垃圾收集器介绍:https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/concurrent.html#mostly_concurrent
[3]G1收集器简介:https://www.oracle.com/technetwork/java/javase/tech/g1-intro-jsp-135488.html
评论
发表评论