JVM调优
# JVM配置参数-X与-XX的区别
启动JVM时通过指定配置参数来指导虚拟机按照我们的要求提供服务,这一点对大多数的Java程序员来说已经是司空见惯。
在指定配置参数时,会有-X和-XX两种形式,那么它们两者有什么区别呢,今天我想借这篇文章总结一下。
下面是我们的某个Java项目在正式环境上启动JVM时的一个典型命令,在该命令中指定了各种启动参数:
java -Xmx15G \
-Xms10G \
-Xmn3G \
-Xss512k \
-XX:MaxPermSize=512M \
-XX:PermSize=512M \
-XX:+PrintFlagsFinal \
-XX:MaxTenuringThreshold=1 \
-XX:SurvivorRatio=23 \
-XX:TargetSurvivorRatio=80 \
-Xnoclassgc \
-XX:+UseParNewGC \
-XX:+UseConcMarkSweepGC \
-XX:CMSInitiatingOccupancyFraction=80 \
-XX:ParallelGCThreads=24 \
-XX:ConcGCThreads=24 \
-XX:+CMSParallelRemarkEnabled \
-XX:+CMSScavengeBeforeRemark \
-XX:+ExplicitGCInvokesConcurrent \
-XX:+UseTLAB \
-XX:TLABSize=64K, -verbose:gc \
-XX:+PrintGCDetails \
-XX:+PrintGCDateStamps \
-XX:+PrintGCTimeStamps \
-XX:+PrintGCApplicationStoppedTime \
-Xloggc:./gc.log
Java HotSpot VM的官方文档中将启动参数分为如下两类:
配置参数 | 类型 | 说明 | 举例 |
---|---|---|---|
-X | non-standard | 非标准参数。<br/><br/>这些参数不是虚拟机规范规定的。因此,不是所有VM的实现(如:HotSpot,JRockit,J9等)都支持这些配置参数。 | -Xmx、-Xms、-Xmn、-Xss |
-XX | not-stable | 不稳定参数。<br/><br/>这些参数是虚拟机规范中规定的。这些参数指定虚拟机实例在运行时的各种行为,从而对虚拟机的运行时性能有很大影响。 | -XX:SurvivorRatio、-XX:+UseParNewGc |
补充: -X和-XX两种参数都可能随着JDK版本的变更而发生变化,有些参数可以能会被废弃掉,有些参数的功能会发生改变,但是JDK官方不会通知开发者这些变化,需要使用者注意。
-XX参数被称为不稳定参数,是因为这类参数的设置会引起JVM运行时性能上的差异,配置得当可以提高JVM性能,配置不当则会使JVM出现各种问题, 甚至造成JVM崩溃。
国外有个哥们从HotSpot VM的源码里发现了934个此类型的配置参数,因此能对JVM做出很多组合配置,对JVM的调优也没有统一的标准,需要我们在实践中不断总结经验,并结合实际业务来进行操作,最终找到最适合当前业务的那些配置。
# 一些有用的-XX配置
对于-XX类型的配置选项,虚拟机规范有一些惯例,针对不同的平台虚拟机也会提供不同的默认值。
对于布尔(Boolean)类型的配置选项,通过-XX:+<option>来开启,通过-XX:-<option>来关闭。
对于数字(Numberic)类型的配置选项,通过-XX:<option>=<number>来配置。<number>后面可以携带单位字母,比如: 'k'或者'K'代表千字节,'m'或者'M'代表兆字节,'g'或者'G'代表千兆字节。
对于字符串(String)类型的配置选项,通过-XX:<option>=<string>来配置。这种配置通过用来指定文件,路径或者命令列表。
# 参考
<http://www.nituchao.com/jvm-tuning/8.html>
# java -X命令
C:\Users\wale>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:system
(仅限 Linux)显示系统或容器
配置并继续
-XshowSettings:all
显示所有设置并继续
-XshowSettings:vm 显示所有与 vm 相关的设置并继续
-XshowSettings:properties
显示所有属性设置并继续
-XshowSettings:locale
显示所有与区域设置相关的设置并继续
-X 选项是非标准选项。如有更改,恕不另行通知。
# 堆栈相关参数
- -Xms
堆最小值
- -Xmx
堆最大堆值。-Xms与-Xmx 的单位默认字节都是以k、m做单位的。
通常这两个配置参数相等,避免每次空间不足,动态扩容带来的影响。
- -Xss
每个线程池的堆栈大小。在jdk5以上的版本,每个线程堆栈大小为1m,jdk5以前的版本是每个线程池大小为256k。一般在相同物理内存下,如果减少-xss值会产生更大的线程数,但不同的操作系统对进程内线程数是有限制的,是不能无限生成。
- -Xmn
新生代大小
- -XX:NewRatio
设置新生代与老年代比值,-XX:NewRatio=4 表示新生代与老年代所占比例为1:4 ,新生代占比整个堆的五分之一。如果设置了-Xmn的情况下,该参数是不需要在设置的。
- -XX:PermSize
设置永久代初始值,默认是物理内存的六十四分之一
- -XX:MaxPermSize
设置永久代最大值,默认是物理内存的四分之一
- -XX:MaxTenuringThreshold
新生代中对象存活次数,默认15。(若对象在eden区,经历一次MinorGC后还活着,则被移动到Survior区,年龄加1。以后,对象每次经历MinorGC,年龄都加1。达到阀值,则移入老年代)
- -XX:SurvivorRatio
Eden区与Subrvivor区大小的比值,如果设置为8,两个Subrvivor区与一个Eden区的比值为2:8,一个Survivor区占整个新生代的十分之一
- -XX:+UseFastAccessorMethods
原始类型快速优化
- -XX:+AggressiveOpts
编译速度加快
- -XX:PretenureSizeThreshold
对象超过多大值时直接在老年代中分配
# 垃圾回收器的JVM参数
- -XX:+UseSerialGC
串行垃圾回收,现在基本很少使用。
- -XX:+UseParNewGC
新生代使用并行,老年代使用串行;
- -XX:+UseConcMarkSweepGC
新生代使用并行,老年代使用CMS(一般都是使用这种方式),CMS是Concurrent Mark Sweep的缩写,并发标记清除,一看就是老年代的算法,所以,它可以作为老年代的垃圾回收器。CMS不是独占式的,它关注停顿时间
- -XX:ParallelGCThreads
指定并行的垃圾回收线程的数量,最好等于CPU数量
- -XX:+DisableExplicitGC
禁用System.gc(),因为它会触发Full GC,这是很浪费性能的,JVM会在需要GC的时候自己触发GC。
- -XX:CMSFullGCsBeforeCompaction
在多少次GC后进行内存压缩,这个是因为并行收集器不对内存空间进行压缩的,所以运行一段时间后会产生很多碎片,使得运行效率降低。
- -XX:+CMSParallelRemarkEnabled
降低标记停顿
- -XX:+UseCMSCompactAtFullCollection
在每一次Full GC时对老年代区域碎片整理,因为CMS是不会移动内存的,因此会非常容易出现碎片导致内存不够用的
- -XX:+UseCmsInitiatingOccupancyOnly
使用手动触发或者自定义触发cms 收集,同时也会禁止hostspot 自行触发CMS GC
- -XX:CMSInitiatingOccupancyFraction
使用CMS作为垃圾回收,使用70%后开始CMS收集
- -XX:CMSInitiatingPermOccupancyFraction
设置perm gen使用达到多少%比时触发垃圾回收,默认是92%
- -XX:+CMSIncrementalMode
设置为增量模式
- -XX:+CmsClassUnloadingEnabled
CMS是不会默认对永久代进行垃圾回收的,设置此参数则是开启
- -XX:+PrintGCDetails
开启详细GC日志模式,日志的格式是和所使用的算法有关
- -XX:+PrintGCDateStamps
将时间和日期也加入到GC日志中
# JVM调优
# JVM调优有哪些工具?
# jstat-jvm状态信息
jstat可以打印出当前JVM运行的各种状态信息,例如新生代内存使用情况,老年代内存使用情况,以及垃圾回收的时间。Minor GC发生总次数,总耗时,Full GC发生总次数,总耗时。(jmap -heap命令也可以打印出堆中各个分区的内存使用情况,但是不能定时监测,持续打印。例如每1s打印当前的堆中各个分区的内存使用情况,一直打印100次。)
//5828是java进程id,1000是打印间隔,每1000毫秒打印一次,100是总共打印100次
jstat -gc 5828 1000 100
打印结果如下:
各个参数的含义如下:
S0C
新生代中第一个survivor(幸存区)的总容量 (字节)S1C
新生代中第二个survivor(幸存区)的总容量 (字节)S0U
新生代中第一个survivor(幸存区)目前已使用空间 (字节)S1U
新生代中第二个survivor(幸存区)目前已使用空间 (字节)EC
新生代中Eden区的总容量 (字节)EU
新生代中Eden区目前已使用空间 (字节)OC
老年代的总容量 (字节)OU
老年代代目前已使用空间 (字节)YGC
目前新生代垃圾回收总次数YGCT
目前新生代垃圾回收总消耗时间FGC
目前full gc次数总次数FGCT
目前full gc次数总耗时,单位是秒GCT
垃圾回收总耗时
一般还可以使用jstat -gcutil \<pid>
:统计gc信息,这样打印出来的结果是百分比,而不是实际使用的空间,例如jstat -gcutil 1 1000 100
例如,S0代表 新生代中第一个survivor区的空间使用了73.19%,E代表新生代Eden区使用了51%,O代表老年代食堂了98%
参数 | 描述 |
---|---|
S0 | 年轻代中第一个survivor(幸存区)已使用的占当前容量百分比 |
s1 | 年轻代中第二个survivor(幸存区)已使用的占当前容量百分比 |
E | 年轻代中Eden已使用的占当前容量百分比 |
O | old代已使用的占当前容量百分比 |
M | 元空间(MetaspaceSize)已使用的占当前容量百分比 |
CCS | 压缩使用比例 |
YGC | 年轻代垃圾回收次数 |
FGC | 老年代垃圾回收次数 |
FGCT | 老年代垃圾回收消耗时间 |
GCT | 垃圾回收消耗总时间 |
# jstack-jvm的线程快照
jstack可以生成当前JVM的线程快照,也就是当前每个线程当前的状态及正在执行的方法,锁相关的信息。jstack -l 进程id
,-l代表除了堆栈信息外,还会打印锁的附加信息。jstack还会检测出死锁信息。一般可以用于定位线程长时间停顿,线程间死锁等问题。
例如在下面的例子中,第一个线程获取到lock1,再去获取lock2,第二个线程先获取到lock2,然后再去获取lock1。每个线程都只获得了一个锁,同时在获取另外一个锁,就会进入死锁状态。
public static void main(String[] args) {
final Integer lock1 = new Integer(1);
final String lock2 = new String();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(new Runnable() {
@Override
public void run() {
synchronized (lock1) {
System.out.println("线程1获得了lock1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程1休眠结束");
System.out.println("线程1开始尝试获取lock2");
synchronized (lock2) {
System.out.println("线程1获得了lock2");
}
}
}
});
executorService.execute(new Runnable() {
@Override
public void run() {
synchronized (lock2) {
System.out.println("线程2获得了lock2");
System.out.println("线程2开始尝试获取lock1");
synchronized (lock1) {
System.out.println("线程2获得了lock2");
}
}
}
});
}
使用jstack -l 进程id
就可以打印出当前的线程信息
以及各个线程的状态,执行的方法(pool-1-thread-1和pool-1-thread-2分别代表线程池的第一个线程和第二个线程):
# jmap
# jmap -heap-堆栈快照
这个命令可以生成当前堆栈快照。使用 jmap -heap 进程id
可以打印出当前堆各分区内存使用情况的情况,新生代(Eden区,To Survivor区,From Survivor区),老年代区的内存使用情况。
使用jmap -heap查看内存使用情况的案例
# jmap -histo
jmap -histo 进程id 打印出当前堆中的对象统计信息,包括类名,每个类的实例数量,总占用内存大小。
instances列:表示当前类有多少个实例。
bytes列:说明当前类的实例总共占用了多少个字节
class name列:表示的就是当前类的名称,class name 对于基本数据类型,使用的是缩写。解读:B代表byte ,C代表char ,D代表double, F代表float,I代表int,J代表long,Z代表boolean
前边有[代表数组,[I 就相当于int[]
对象数组用`[L+类名`表示
# jmap -dump
使用jmap -dump:format=b,file=dump.hprof 进程id
可以生成当前的堆栈快照,堆快照和对象统计信息,对生成的堆快照进行分析,可以分析堆中对象所占用内存的情况,检查大对象等。执行jvisualvm
命令打开使用Java自带的工具Java VisualVM来打开堆栈快照文件,进行分析。可以用于排查内存溢出,内存泄露问题。在Java VisualVM里面可以看到每个类的实例对象占用的内存大小,以及持有这个对象的实例所在的类等等信息。
也可以配置启动时的JVM参数,让发送内存溢出时,自动生成堆栈快照文件。
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/home/liuke/jvmlogs/
使用jmap -dump:format=b,file=/存放路径/heapdump.hprof 进程id就可以得到堆转储文件,然后执行jvisualvm命令就可以打开JDK自带的jvisualvm软件。
例如在这个例子中会造成OOM问题,通过生成heapdump.hprof文件,可以使用jvisualvm查看造成OOM问题的具体代码位置。
public class Test018 {
ArrayList\<TestObject> arrayList = new ArrayList\<TestObject>();
public static void main(String[] args) {
Test018 test018 =new Test018();
Random random = new Random();
for (int i = 0; i \< 10000000; i++) {
TestObject testObject = new TestObject();
test018.arrayList.add(testObject);
}
}
private static class TestObject {
public byte[] placeholder = new byte[64 * 1024];
}
}
-Xms20m -Xmx20m -verbose:gc -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/存放路径/heapdump.hprof
造成OOM问题的代码位置:
堆内对象列表
占用内存最多的实例对象就是这个placeholder对象
# MAT
MAT主要可以用于分析内存泄露,可以查询dump堆转储文件中的对象列表,以及潜在的内存泄露的对象。
通过导入hprof文件,主页会展示潜在的内存泄露问题,比如下面这个例子中
public class Test018 {
static ArrayList\<TestObject> arrayList = new ArrayList\<TestObject>();
public static void main(String[] args) {
Random random = new Random();
for (int i = 0; i \< 10000000; i++) {
TestObject testObject = new TestObject();
Test018.arrayList.add(testObject);
}
}
private static class TestObject {
public byte[] placeholder = new byte[64 * 1024];
}
}
在详情页面Shortest Paths To the Accumulation Point表示GC root对象到内存消耗聚集点的最短路径,内存聚集点的意思就是占用了大量内存的对象,也就是可能发生; 内存泄露的对象。
然后在主页点击Histogram,进入Histogram页面可以看到对象列表,with incomming references 也就是可以查看所有对这个对象的引用(思路一般优先看占用内存最大对象;其次看数量最多的对象。)。我们这个例子中主要是byte[]数组分配了占用了大量的内存空间,而byte[]主要来自于Test018类的静态变量arrayList的每个TestObject类型的元素的placeholder属性。
同时可以点击 内存快照对比 功能对两个dump文件进行对比,判断两个dump文件生成间隔期间,各个对象的数量变化,以此来判断内存泄露问题。