此篇从当前实际接触到的生产环境配置出发,聊聊 JVM 的机制以及参数设置。
配置举例
先看一个当前使用的 jvm 配置(为方便阅读我加了换行和适当注释)
1 | /usr/local/jdk8/bin/java |
配置解读
除去一些日志和路径配置,其他主要包含两种配置
内存配置
1 | -XX:MetaspaceSize=256M // 初始元空间大小(也是初始的阈值,即初始的high-water-mark),达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值 |
可以看到我们目前使用的参数是 4G 内存,其中新生代指定 1G,老年代则为剩下的 3G。元数据区的大小为 256M。单线程最大 256k。
这边值得一提的是,从 JAVA 8 开始,永久代被移除出 JVM,改为元数据(metadata)区。
GC 策略配置
1 | -XX:SurvivorRatio=8 // Eden 区与 Survivor 区的大小比值 1:1:8 |
首先可以看到使用的收集器为 ParNew + CMS,这里简单介绍一下两种 GC 策略
ParNew 收集器
ParNew 常被用作新生代的收集器,具体策略如下:
- 等待所有执行中的用户线程进行到 safepoint,然后 Stop the World
- 并行进行 GC,采用标记-复制算法清理新生代
CMS 收集器
CMS 是目前最常用的老年代收集器,其主要步骤如下:
- 等待所有执行中的用户线程进行到 safepoint,然后 Stop the World
- 单线程进行初始标记,标记 GC Roots(本地变量、方法区中静态对象、常量、JNI 中对象)
- 和用户进程并行,进行并发标记,逐级搜索 GC Roots 进行可达性分析
- 再次等待所有执行中的用户线程进行到 safepoint,然后 Stop the World
- 和用户进程并行,进行重新标记,修正标记产生变动的那一部分
- 和用户进程并行,清理老年代对象(并不整理)
- 和用户进程并行,清理并恢复在 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