JVM参数详解

此篇从当前实际接触到的生产环境配置出发,聊聊 JVM 的机制以及参数设置。

配置举例

先看一个当前使用的 jvm 配置(为方便阅读我加了换行和适当注释)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/usr/local/jdk8/bin/java
-Djava.util.logging.config.file=/usr/local/webserver/app-name/conf/logging.properties
-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager
-XX:MetaspaceSize=256M // 初始元空间大小(也是初始的阈值,即初始的high-water-mark),达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值
-XX:MaxMetaspaceSize=256M // 最大元空间大小
-Xms4g // 初始内存 4g
-Xmx4g // 最大内存 4g
-Xmn1g // 新生代 1g
-Xss256k // 每个线程的内存大小
-XX:SurvivorRatio=8 // Eden 区与 Survivor 区的大小比值 1:1:8
-XX:MaxTenuringThreshold=8 // 垃圾最大年龄
-XX:ParallelGCThreads=8 // 并行收集器的线程数
-XX:+UseConcMarkSweepGC // 使用 CMS 内存收集
-XX:+UseParNewGC // 设置年轻代为并行收集 可与 CMS 收集同时使用
-XX:+DisableExplicitGC // 关闭 System.gc()
-XX:+CMSParallelRemarkEnabled // 启用并行标记
-XX:+CMSClassUnloadingEnabled // 启用永久代的类卸载
-XX:CMSInitiatingOccupancyFraction=70 // 使用 70%后开始 CMS 收集
-XX:+UseCMSCompactAtFullCollection // 在 Full GC 的时候,对年老代的压缩
// CMS 是不会移动内存的,因此,这个非常容易产生碎片,导致内存不够用,因此,内存的压缩这个时候就会被启用。
-XX:CMSFullGCsBeforeCompaction=5 // 多少 Full GC 次后进行内存压缩
// 由于并发收集器不对内存空间进行压缩,整理,所以运行一段时间以后会产生"碎片",使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩,整理。增加这个参数是个好习惯。可能会影响性能,但是可以消除碎片
-XX:+CMSScavengeBeforeRemark // 重新标记之前对年轻代做一次 minor GC,以期望在对老年代GC的时候可以清除更多的对象,针对 Remark 停顿太长的情况,代价是多一次 minor GC
-XX:+HeapDumpOnOutOfMemoryError // 内存溢出时导出堆信息
-Xloggc:/usr/local/webserver/app-name/logs/gc.log // gc日志
-XX:+UseGCLogFileRotation // 启用GC日志文件的自动转储
-XX:NumberOfGCLogFiles=10
-XX:GCLogFileSize=10M
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCApplicationStoppedTime
-XX:+PrintGCApplicationConcurrentTime -Dcom.sun.management.jmxremote.password.file=/usr/local/webserver/app-name/conf/jmxremote.password
-Dcom.sun.management.jmxremote.access.file=/usr/local/webserver/app-name/conf/jmxremote.access -Dcom.sun.management.jmxremote.ssl=false
-XX:+CMSConcurrentMTEnabled
-XX:+ExplicitGCInvokesConcurrent
-XX:HeapDumpPath=/usr/local/webserver/app-name/logs/app-name.hprof
-javaagent:/opt/jars/aspectjweaver-1.8.9.jar -Djdk.tls.ephemeralDHKeySize=2048
-Djava.protocol.handler.pkgs=org.apache.catalina.webresources
-Djava.endorsed.dirs=/usr/local/webserver/app-name/endorsed
-classpath /usr/local/webserver/app-name/bin/bootstrap.jar:/usr/local/webserver/app-name/bin/tomcat-juli.jar
-Dcatalina.base=/usr/local/webserver/app-name
-Dcatalina.home=/usr/local/webserver/app-name
-Djava.io.tmpdir=/usr/local/webserver/app-name/temp
org.apache.catalina.startup.Bootstrap start

配置解读

除去一些日志和路径配置,其他主要包含两种配置

内存配置

1
2
3
4
5
6
7
-XX:MetaspaceSize=256M // 初始元空间大小(也是初始的阈值,即初始的high-water-mark),达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值
-XX:MaxMetaspaceSize=256M // 最大元空间大小
-Xms4g // 初始内存 4g
-Xmx4g // 最大内存 4g
-Xmn1g // 新生代 1g
-Xss256k // 每个线程的内存大小
-XX:SurvivorRatio=8 // Eden 区与 Survivor 区的大小比值 1:1:8

可以看到我们目前使用的参数是 4G 内存,其中新生代指定 1G,老年代则为剩下的 3G。元数据区的大小为 256M。单线程最大 256k。

这边值得一提的是,从 JAVA 8 开始,永久代被移除出 JVM,改为元数据(metadata)区。

GC 策略配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-XX:SurvivorRatio=8 // Eden 区与 Survivor 区的大小比值 1:1:8
-XX:MaxTenuringThreshold=8 // 垃圾最大年龄
-XX:ParallelGCThreads=8 // 并行收集器的线程数
-XX:+UseConcMarkSweepGC // 使用 CMS 收集器
-XX:+UseParNewGC // 年轻代使用 ParNew 收集器,可与 CMS 收集同时使用
-XX:+DisableExplicitGC // 关闭 System.gc()
-XX:+CMSParallelRemarkEnabled // 启用并行标记
-XX:+CMSClassUnloadingEnabled // 启用永久代的类卸载
-XX:CMSInitiatingOccupancyFraction=70 // 使用 70%后开始 CMS 收集
-XX:+UseCMSCompactAtFullCollection // 在 Full GC 的时候,对年老代的压缩
// CMS 是不会移动内存的,因此,这个非常容易产生碎片,导致内存不够用,因此,内存的压缩这个时候就会被启用。
-XX:CMSFullGCsBeforeCompaction=5 // 多少 Full GC 次后进行内存压缩
// 由于并发收集器不对内存空间进行压缩,整理,所以运行一段时间以后会产生"碎片",使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩,整理。增加这个参数是个好习惯。可能会影响性能,但是可以消除碎片
-XX:+CMSScavengeBeforeRemark // 重新标记之前对年轻代做一次 minor GC,以期望在对老年代GC的时候可以清除更多的对象,针对 Remark 停顿太长的情况,代价是多一次 minor GC
-XX:+CMSConcurrentMTEnabled // 并发的CMS阶段将以多线程执行(因此,多个GC线程会与所有的应用程序线程并行工作)。
-XX:+ExplicitGCInvokesConcurrent

首先可以看到使用的收集器为 ParNew + CMS,这里简单介绍一下两种 GC 策略

ParNew 收集器

ParNew 常被用作新生代的收集器,具体策略如下:

  1. 等待所有执行中的用户线程进行到 safepoint,然后 Stop the World
  2. 并行进行 GC,采用标记-复制算法清理新生代

CMS 收集器

CMS 是目前最常用的老年代收集器,其主要步骤如下:

  1. 等待所有执行中的用户线程进行到 safepoint,然后 Stop the World
  2. 单线程进行初始标记,标记 GC Roots(本地变量、方法区中静态对象、常量、JNI 中对象)
  3. 和用户进程并行,进行并发标记,逐级搜索 GC Roots 进行可达性分析
  4. 再次等待所有执行中的用户线程进行到 safepoint,然后 Stop the World
  5. 和用户进程并行,进行重新标记,修正标记产生变动的那一部分
  6. 和用户进程并行,清理老年代对象(并不整理)
  7. 和用户进程并行,清理并恢复在 CMS GC 过程中的各种状态,重新初始化 CMS 相关数据结构

再回头来看这套配置:

  • 新生代的 Eden 区与 Survivor 区的大小比值 1:1:8
  • 新生代经过 8 次 minor GC 后进入老年代;
  • GC 的并行数为 8
  • 关闭显式 GC(即 System.gc())
  • 启用类卸载,即会回收元数据空间
  • 老年代使用 70%后开始 CMS 收集
  • 每 5 次 CMS 收集后整理老年代内存(因为 CMS 收集只会对老年代的对象执行清除操作,并不会整理,长期后会产生过多的碎片,导致实际内存足够但无法申请出足够的内存)
  • 在 CMS 的重新标记(第五步)前对年轻代执行一次 minor GC(这样在对老年代重新标记时可以清除出更多的对象)
  • CMS 与用户进程并发的阶段会启动多个 GC 进程并发
  • 开启并行显式 Full GC,但其实在这是个废配置,因为 DisableExplicitGC 已经禁用了显式 Full GC