前言
本文是对JVM flag系列文章的翻译和精简
- JDK 作者是基于JDK6的,本人为JDK8.
- 示例代码 命令行以$开头的为复制原作者,*λ *为本人实测
一 纯解释、纯编译还是混合
版本信息
-version
λ java -version
java version "1.8.0_151"
Java(TM) SE Runtime Environment (build 1.8.0_151-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.151-b12, mixed mode)
打印版本信息
-showversion
加强版的version,打印版本信息并执行后续任务
λ java -showversion -jar myjar.jar
java version "1.8.0_151"
Java(TM) SE Runtime Environment (build 1.8.0_151-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.151-b12, mixed mode)
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v1.4.2.RELEASE)
解释、编译还是混合?
-Xint
强制bytecode在解释模式运行,可以节省一点点编译时间但是运行极其缓慢(至少10倍)
-Xcomp
强制bytecode编译为原生代码后运行,编译时间稍长,但是运行效率有部分提升.最大的缺点是会导致JIT无法生效(JIT需要在运行时发现热点(hotspot)代码,并执行大量优化,JIT是hotspot vm的关键,也正是JIT的各种神优化才使java慢慢脱离了运行缓慢的非议)
-Xmixed
hotspot的默认配置, 对于热点代码优化,对于极少运行的代码以解释方式运行,在编译时间和运行效率取得平衡
二 标志分类和详细编译打印
JVM的标志分类
-abc
JVM标准标志.基本不会发生改变 如 -server -version
-Xabc
JVM实验性质标志,未来可能发生改变,虽然没有保障但仍然较为稳定,使用 java -X显示标志列表(会有遗漏)
λ java -X
-Xmixed 混合模式执行 (默认)
-Xint 仅解释模式执行
-Xbootclasspath:<用 ; 分隔的目录和 zip/jar 文件>
设置搜索路径以引导类和资源
-Xbootclasspath/a:<用 ; 分隔的目录和 zip/jar 文件>
附加在引导类路径末尾
-Xbootclasspath/p:<用 ; 分隔的目录和 zip/jar 文件>
置于引导类路径之前
-Xdiag 显示附加诊断消息
-Xnoclassgc 禁用类垃圾收集
-Xincgc 启用增量垃圾收集
-Xloggc:<file> 将 GC 状态记录在文件中 (带时间戳)
-Xbatch 禁用后台编译
-Xms<size> 设置初始 Java 堆大小
-Xmx<size> 设置最大 Java 堆大小
-Xss<size> 设置 Java 线程堆栈大小
-Xprof 输出 cpu 配置文件数据
-Xfuture 启用最严格的检查, 预期将来的默认值
-Xrs 减少 Java/VM 对操作系统信号的使用 (请参阅文档)
-Xcheck:jni 对 JNI 函数执行其他检查
-Xshare:off 不尝试使用共享类数据
-Xshare:auto 在可能的情况下使用共享类数据 (默认)
-Xshare:on 要求使用共享类数据, 否则将失败。
-XshowSettings 显示所有设置并继续
-XshowSettings:all
显示所有设置并继续
-XshowSettings:vm 显示所有与 vm 相关的设置并继续
-XshowSettings:properties
显示所有属性设置并继续
-XshowSettings:locale
显示所有与区域设置相关的设置并继续
-X 选项是非标准选项, 如有更改, 恕不另行通知。
-XXabc
JVM实验性质标志,未来很可能发生改变,不太稳定,但很少变化
-XX 的特殊语法
-XX:+<name>
仅对boolean标志,开启选项,相应的-XX:-<name>关闭选项
-XX:<name>=<value>
设置选项
显示JIT编译信息
-XX:+PrintCompilation
在程序运行时显示JIT的编译信息
$ java -server -XX:+PrintCompilation Benchmark
1 java.lang.String::hashCode (64 bytes)
2 java.lang.AbstractStringBuilder::stringSizeOfInt (21 bytes)
3 java.lang.Integer::getChars (131 bytes)
4 java.lang.Object::<init> (1 bytes)
--- n java.lang.System::arraycopy (static)
5 java.util.HashMap::indexFor (6 bytes)
6 java.lang.Math::min (11 bytes)
7 java.lang.String::getChars (66 bytes)
8 java.lang.AbstractStringBuilder::append (60 bytes)
...
-XX:+CITime
在程序运行时显示JIT编译时间
$ java -server -XX:+CITime Benchmark
[...]
Accumulated compiler times (for compiled methods only)
------------------------------------------------
Total compilation time : 0.178 s
Standard compilation : 0.129 s, Average : 0.004
On stack replacement : 0.049 s, Average : 0.024
[...]
比较编译时间
拿公司的jar跑的,没跑起来,结果并不准确
- -Xcomp
Accumulated compiler times (for compiled methods only)
------------------------------------------------
Total compilation time : 45.480 s
Standard compilation : 44.531 s, Average : 0.001
On stack replacement : 0.949 s, Average : 0.045
Detailed C1 Timings
Setup time: 0.000 s ( 0.0%)
Build IR: 3.282 s (37.8%)
Optimize: 0.186 s ( 2.1%)
RCE: 0.038 s ( 0.4%)
Emit LIR: 2.887 s (33.2%)
LIR Gen: 0.633 s ( 7.3%)
Linear Scan: 2.226 s (25.6%)
LIR Schedule: 0.000 s ( 0.0%)
Code Emission: 0.974 s (11.2%)
Code Installation: 1.543 s (17.8%)
Instruction Nodes: 2332355 nodes
- -Xint
Accumulated compiler times (for compiled methods only)
------------------------------------------------
Total compilation time : 0.000 s
Standard compilation : 0.000 s, Average : -1.#IO
On stack replacement : 0.000 s, Average : -1.#IO
Total compiled methods : 0 methods
Standard compilation : 0 methods
On stack replacement : 0 methods
Total compiled bytecodes : 0 bytes
Standard compilation : 0 bytes
On stack replacement : 0 bytes
Average compilation speed: -2147483648 bytes/s
nmethod code size : 0 bytes
nmethod total size : 0 bytes
3 -Xmixed
Accumulated compiler times (for compiled methods only)
------------------------------------------------
Total compilation time : 18.903 s
Standard compilation : 17.893 s, Average : 0.004
On stack replacement : 1.011 s, Average : 0.014
Detailed C1 Timings
Setup time: 0.000 s ( 0.0%)
Build IR: 0.554 s (43.9%)
Optimize: 0.033 s ( 2.6%)
RCE: 0.007 s ( 0.6%)
Emit LIR: 0.438 s (34.7%)
LIR Gen: 0.095 s ( 7.5%)
Linear Scan: 0.339 s (26.8%)
LIR Schedule: 0.000 s ( 0.0%)
Code Emission: 0.141 s (11.2%)
Code Installation: 0.128 s (10.2%)
Instruction Nodes: 345057 nodes
Total compiled methods : 4749 methods
Standard compilation : 4679 methods
On stack replacement : 70 methods
Total compiled bytecodes : 959126 bytes
Standard compilation : 921785 bytes
On stack replacement : 37341 bytes
Average compilation speed: 50738 bytes/s
nmethod code size : 8556928 bytes
nmethod total size : 16044616 bytes
可以看出-Xint完全没有编译,即使结果不准确也很容易理解-Xmixed模式的编译时间会比-Xcomp短
解锁JVM 实验性质标志
-XX:+UnlockExperimentalVMOptions
解锁JVM实验性质标志.
如果你设置了某个-XX标志,但是程序启动后立刻输出Unrecognized VM option,一般是拼写错误.如果Error: VM option 'Xxxx' is experience and must be enabled via -XX:+UnlockXXXVMOptions.是这可能是JVM的安全策略,类似于二次确认,某些标志可能会带来意向不到的问题比如生成大量日志,所以在遇到这个问题后最好再去确定下标志的作用,比如下面这个例子
λ java -XX:+LogCompilation -jar myjar.jar
Error: VM option 'LogCompilation' is diagnostic and must be enabled via -XX:+UnlockDiagnosticVMOptions.
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.
另外有些标志是为jvm开发人员提供的,需要debug build的jvm才能使用,如果在stable build中使用会有相应的错误提示
更详细的编译信息
-XX:+LogCompilation
将更详细的编译信息(编译线程及编译的方法)保存到hotspot.log文件,需要添加UnockDiagnostic标志
-XX:+PrintOptoAssembly
显示并保存编译出的native code到hotspot.log
在debug build才能使用
λ java -XX:+PrintOptoAssembly -jar myjar.jar
Error: VM option 'PrintOptoAssembly' is notproduct and is available only in debug version of VM.
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.
三
打印-XX标志
-XX:+PrintFlagsFinal
打印所有生效的-XX标志,如果没有手动设置会显示默认设置
λ java -XX:+PrintFlagsFinal -jar myjar.jar
[Global flags]
uintx AdaptiveSizeThroughPutPolicy = 0 {product}
uintx AdaptiveTimeWeight = 25 {product}
bool AdjustConcurrency = false {product}
bool AggressiveOpts = false {product}
intx AliasLevel = 3 {C2 product}
bool AlignVector = true {C2 product}
intx AllocateInstancePrefetchLines = 1 {product}
intx AllocatePrefetchDistance = 256 {product}
intx AllocatePrefetchInstr = 3 {product}
intx AllocatePrefetchLines = 3 {product}
intx AllocatePrefetchStepSize = 64 {product}
intx AllocatePrefetchStyle = 1 {product}
bool AllowJNIEnvProxy = false {product}
格式为
值类型 名称 [:]= 值 环境类型
带有:表示被手动覆盖(JVM智能分析或用户手动设置), 只有=为默认值
-XX:+PrintFlagsInitial
与上面类似,不过即使手动设置了也只显示默认值,
额外显示实现和分析相关标志
添加 -XX:+UnlockExperimentalVMOptions 和 -XX:+UnlockDiagnosticVMOptions
- 对于 Final,未添加时为723,添加后为867
- 对于Initial没有影响
统计文件行数可使用 wc -l filename
只显示手动设置标志
-XX:+PrintCommandLineFlags
只显示用户设置或JVM智能设置的标志(也就是上面的 :=)
λ java -XX:+PrintCommandLineFlags myjar.jar
-XX:InitialHeapSize=266699648 -XX:MaxHeapSize=4267194368 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
(这里作者推荐将这个选项添加到jvm默认配置里,这样有利于jvm调优)
四 内存调优
JVM中共享内存分为堆区和方法区
对于G1之前的garbage collector
- 堆区按照如下方式划分
- 新生代,包含新创建/朝生夕死的对象
- 老年代,经历过一定次数GC仍然存活的新生代对象会移动到老年代
- 方法区也称,永久代,包含类信息,常量池等
对于G1,永久代被移除,变为metaSpace区,metaSpace区不再占用JVM中的内存,而是使用本地堆内存(native heap),常量池也移动到堆区(JDK7),新生代和老年代的概念保留,但是已经没有物理界限
内存设置
-Xms128(k|m|g) (or: -XX:InitialHeapSize)
设置堆内存大小为128k/m/g,ms(memory size) 总内存大小,一般表示堆内存下限
-Xms128(k|m|g) (or:-XX:MaxHeapSize)
堆内存上限,JVM的运行中会动态调整总内存,但是确保不会超过上限
值得注意的是如果要查看启动时设置的堆内存,应该查找InitialHeapSize和MaxHeapSize这两个字段,而非Xms或Xmx
堆内存溢出自动保存heap dump文件
-XX:+HeapDumpOnOutOfMemoryError -XX:+HeapDumpPath
堆内存溢出时,默认在jvm启动目录生成dump文件,通过设置-XX:+HeapDumpPath可自定义保存位置,文件名为java_pid<pid>.hprof
由于堆内存溢出时dump文件通常会很大,所以推荐设置自定义保存位置
堆内存溢出时执行自定命令
-XX:OnOutOfMemoryError
使用示例如下
$ java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof -XX:OnOutOfMemoryError ="sh ~/cleanup.sh" MyApp
设置永久代内存(仅对G1之前的)
-XXPermSize -XX:MaxPermSize
设置永久代内存大小,永久代是不占用堆内存的,堆内存和永久代内存是不相关的,但是这两块内存都是由JVM管理的,而JDK8的metaSpace使用的direct memory是由计算机管理而非JVM
native code缓存区
JVM有一块内存区域用来保存编译后的native code,如果这块内存满了,不会再有任何编译行为发生,也就是说JIT被停止了,现有的未编译代码会以解释模式执行,这会导致严重的性能问题
-XX:InitialCodeCacheSize -XX:ReservedCodeCacheSize
设置初始代码缓存区大小 设置最大代码缓存区大小
-XX:+UseCodeCacheFlushing
在缓存区满时释放部分缓存,可以避免缓存区满时剩余代码全部以解释模式运行的问题
五 新生代GC
新生代采用复制算法(参见另一篇文章)
新生代分为1个eden和两个survivor,两个survivor只会使用一个,每次eden区快满时会发生一次minor gc,将eden和正在使用的survivor中存活的对象复制到另一个存活的survivor中.如果一个对象存活超过一定次数minor gc就会晋升到老年代
minor gc vs major gc vs full gc
我对这三种gc也是傻傻分不清,网上查到的资料也不是太全,下面根据自己的理解分析一下,如果错误请指正,下面只是内存快满导致的gc:
- minor gc 新生代快满时会触发minor gc,具体流程如上
- major gc 在触发minor gc后,会有部分对象晋升到老年代,如果这时老年代快满则会触发major gc.
- full gc 对于jdk8之前,如果永久代快满,会导致full gc
设置新生代内存
-XX:NewSize -XX:MaxNewSize
类似于-Xms和-Xmx,因为堆内存由新生代和老年代组成,推荐两者的最大内存相同
设置新生代和老年代的内存占比
-XX:NewRatio
//新生代:老年代=1:3,堆内存新生代占1/4,老年代占3/4
-XX:NewRatio=3
如果同时设置了固定范围和比率,固定范围拥有更高的优先级,考虑如下配置
$ java -XX:NewSize=32m -XX:MaxNewSize=512m -XX:NewRatio=3 MyApp
初始时JVM会尽量设计新生代和老年代的比例为1:3,但是无论如何新生代内存大于32m小于512m
设计eden和survivor的内存占比
-XX:SurvivorRatio
//eden:survivor=10:1,由于survivor是有两块的,所有新生代的内存eden占比10/12,两个survivor各占1/12
-XX:SurvivorRatio=10
eden和survivor的失衡可能会导致如下两种我们不期望发生的现象
- eden很大
由于eden很大,可以容纳很多对象,minorGC的频率就会降低,如果分配的对象大都是朝生夕死的,进行minorGC时虽然survivor很小但仍然可以容纳存活下来的少数对象,这是理想状态,但是如果大多数对象不是朝生夕死的,由于survivor不能容纳这么多存活对象,这些对象只能被移动到老年代,且不论移动过程的耗费,这还会大大提高majorGC发生的几率,而majorGC的耗费远高于minorGC. - eden很小
只能容纳少量对象,minorGC发生的几率会增大.
survivor和晋升年龄调优
-XX:+PrintTenuringDistribution
打印survivor中对象,年龄信息及晋升年龄
Desired survivor size 75497472 bytes, new threshold 15 (max 15)
- age 1: 19321624 bytes, 19321624 total
- age 2: 79376 bytes, 19401000 total
- age 3: 2904256 bytes, 22305256 total
以上信息为:survivor的期望容量(并非survivor总容量)约75MB,当前晋升年龄为15(当对象年龄大于改阈值就会晋升到老年代),最大晋升年龄也是15
- age 1: 19321624 bytes, 19321624 total
年龄为1的对象约为19MB,小于等于该年龄的对象约为19MB
现在假设进行了一个minorGC,GC后如下
Desired survivor size 75497472 bytes, new threshold 2 (max 15)
- age 1: 68407384 bytes, 68407384 total
- age 2: 12494576 bytes, 80901960 total
- age 3: 79376 bytes, 80981336 total
- age 4: 2904256 bytes, 83885592 total
可以看到年龄为4的对象约为3M,和GC前年龄为3的大小相同,说明之前年龄为3的对象全部存活,年龄为3的同理.
同时目前的survivor实际总容量约为83MB,超过了survivor的期望容量,因此当前晋升年龄调整为2,下次GC时年龄大于2的对象将晋升到老年代.
survivor相关设置
-XX:InitialTenuringThreshold, -XX:MaxTenuringThreshold
初始晋升年龄和最大晋升年龄
-XX:TargetSurvivorRatio
设置survivor的期望容量占survivor总容量的比例,如-XX:TargetSurvivorRatio=90说明survivor期望容量为总容量的90%
仅在测试时使用的两个标志
-XX:+NeverTenure -XX:+AlwaysTenure
顾名思义 永不晋升和总是晋升
六 更高的吞吐还是更短的停顿?
GC算法是否优越,主要由下面两个要素决定
- 高吞吐
- 低停顿
GC由几个专用的GC线程负责,发生GC时,GC线程会与业务线程争抢时间片,因此cpu不可能总是在执行业务线程,总有一部分时间是归gc线程的.
吞吐量
业务线程执行总时间占cpu运行总时间的比例就是吞吐量,例如cpu运行了100s,在此期间业务线程运行了99s.那吞吐量就是99%.
停顿时间
gc时会暂停所有业务线程,具体原因参见Stop the world.这里业务线程的暂停时间就是停顿时间.
吞吐vs停顿
鱼与熊掌不可兼得,高吞吐和低停顿是两个相反的目标:
- 高吞吐要求gc尽量少
- 低延迟要求gc尽量多
值得注意的是,通过并发设计(并发和并行是有区别的,推荐大家研究一下,我对这两个概念还是有点分不清,等讲究清楚再分享),G1收集器正在尽量达成高吞吐和低停顿.
高吞吐的设计
gc期间需要确保堆中对象的状态不能发生变化,因此工作线程在gc期间必须停止,也称为stop the world.另外
- gc前要进行一系列预备工作
- gc线程切换需要耗费资源
gc最好在无法避免是才进行比如堆中无法容纳更多对象时,相比高频次的gc,节省了大量预备工作时间和线程切换时间.并以此达到高吞吐.
低延迟的设计
上面高吞吐的设计,因为堆中有大量对象需要处理,停顿时间不可避免的很长,为了达到低延迟,gc要尽可能的频繁,这样每次只需要回收少量对象,停顿时间也会大大减少.
面向高吞吐
下面几个标志都是指定gc收集器,具体参见gc收集器
- -XX:+UseSerialGC 新生代和老年代都使用单线程收集器
- -XX:+UseParallelGC JDK1.6表示新生代使用多线程收集器,在JDK1.7中作用与-XX:+UseParallelOldGC相同
- -XX:+UseParallelOldGC 新生代和老年代都是用多线程收集器
设置parallelGC线程数量
-XX:ParallelGCThreads
设置ParallelGC线程数量,如果没有设置会按照以下规则进行(JDK1.6),获取cpu核心数N
- 如果N<=8 GC线程数就为8
- 如果N>8 GC线程数为 3+5N/8
关闭JVM智能调整功能
-XX:-UseAdaptiveSizePolicy
JDK1.5为JVM引入了称为ergonomics的机制,在运行时,如果有证明修改某些设置能够提高性能,ergonomics就会进行动态调整.它是默认开启的,个人认为一般没有理由去关闭它.
吞吐量设置
-XX:GCTimeRatio
-XX:GCTimeRatio=9表示工作时间占总运行时间的9/10.默认为99,即吞吐量为99%
最大停顿毫秒设置
-XX:MaxGCPauseMillis
也是根据此值在运行是进行分析并调整GC设置.对于新生代和老年代是分别处理的.并且最大停顿比吞吐量有更高的优先级
七 CMS收集器
CMS简介
CMS设置
-XX:+UseConcMarkSweepGC
指定老年代使用CMS
-XX:+UseParNewGC
指定新生代为并行收集,当设置了-XX:+UseConcMarkSweepGC后自动生效,记得之前的-XX:+UseParallelGC吗,为什么不复用这个标志呢?对于CMS,新生代的并行gc采用了另一种实现,因此要用一个新的标志
-XX:+CMSConcurrentMTEnabled
CMS的各个阶段使用多线程并行处理
-XX:ConcGCThreads
并发处理线程数,设置为4时表示CMS的每个阶段都使用4个线程.默认为** (ParallelGCThreads + 3)/4**
-XX:CMSInitiatingOccupancyFraction
GC阈值,当内存占用超过该点时触发gc.对于CMS不能等到内存满了再去GC,因为GC期间业务线程也是在运行的,会进行内存分配.
-XX+UseCMSInitiatingOccupancyOnly
强制使用-XX:CMSInitiatingOccupancyFraction指定的GC阈值,不再由JVM智能推断.
-XX:+CMSClassUnloadingEnabled
开启永久代收集.默认只会在永久代满时才会使用清理,并且不是并行的.
-XX:+CMSIncrementalMode
GC是定期停止gc线程,对于现在的机器意义不大.
System.gc()的优化
-XX:+ExplicitGCInvokesConcurrent
调用System.gc()后使用CMS的GC而非full gc.
XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses
与上面类似,额外增加对永久代的gc.
关闭System.gc()
-XX:+DisableExplicitGC
作者建议这个标志值得添加.
八 GC日志
gc日志并不复杂,简要说明一下
-XX:+PrintGC
打印简要GC日志
//[GC类型,GC前内存占用->GC后占用(堆内存),GC消耗的真实时间]
[GC 246656K->243120K(376320K), 0,0929090 secs]
[Full GC 243120K->241951K(629760K), 1,5589690 secs]
-XX:+PrintGCDetails
打印详细日志
[GC
[PSYoungGen: 142816K->10752K(142848K)] 246648K->243136K(375296K),
0,0935090 secs
]
//user表示所有GC线程消耗的总时间, sys为系统态时间,应该是GC时上下文切换等的额外消耗,real为消耗的真实时间
//
[Times: user=0,55 sys=0,10, real=0,09 secs]
更详细的日志
[Full GC
[PSYoungGen: 10752K->9707K(142848K)]
// 表示堆内存变化
[ParOldGen: 232384K->232244K(485888K)] 243136K->241951K(628736K)
//永久代是不算在堆内存的
[PSPermGen: 3162K->3161K(21504K)],
1,5265450 secs
]
[Times: user=10,96 sys=0,06, real=1,53 secs]
根据每代gc前后的占用内存变化可以推断到底是哪一代引发的gc,像上面的例子,三代基本都没变化,那么有可能是ergonomics 引起的(即使占用未满,ergonomics也可能主动引发一次gc).对于System.gc()引发的GC,GCtype会是Full GC (System)
添加时间信息
-XX:+PrintGCTimeStamps and -XX:+PrintGCDateStamps
如下
//-XX:+PrintGCTimeStamps
0,185: [GC 66048K->53077K(251392K), 0,0977580 secs]
//-XX:+PrintGCDateStamps
2014-01-03T12:08:38.102-0100: [GC 66048K->53077K(251392K), 0,0959470 secs]
把GC日志记录到文件中
-Xloggc
-Xloggc:<file>,把gc日志记录到制定文件中,-XX:+PrintGC and -XX:+PrintGCTimeStamps会被隐式使用
结语
虽然没有看过JVM的源码,单从这些设置就能看出实现是多么复杂.
未来的路还很长.