JVM调优
了解 JVM 中内存管理的原理后,下一步就是如何调优了。
JVM参数
-D、-X、-XX 区别
其一是标准参数(-),所有的 JVM 实现都必须实现这些参数的功能,而且向后兼容;(包括-D 等)JVM 的标准参数都是以”-“开头,通过输入”java -help”或者”java -?”,可以查看 JVM 标准参数列表
其二是非标准参数(-X),默认 jvm 实现这些参数的功能,但是并不保证所有 jvm 实现都满足,且不保证向后兼容;但是列出来的都是当前可用的。
其三是非 Stable 参数(-XX),此类参数各个 jvm 实现会有所不同,将来可能会随时取消,需要慎重使用;
栈大小
- Xss512k:用来设置每个线程的堆栈大小;
堆空间结构调整
- Xmx4g:JVM 最大允许分配的堆内存,按需分配;
- Xms4g:JVM 初始分配的堆内存,一般和 Xmx 配置成一样以避免每次 gc 后 JVM 重新分配内存;
- XX:MetaspaceSize=64m 初始化元空间大小;
- XX:MaxMetaspaceSize=128m 最大化元空间大小。
Metaspace 建议不要设置,一般让 JVM 自己启动的时候动态扩容就好了,没必要自己去设置。如果不动态加载 class ,当启动起来的时候,一般是很少有变化的。
从这个角度我们可以认为我们的 JVM 内存的大小是堆+Metaspace+io(运行时产生的大小)。
- -Xms
[g|m|k] - -Xmx
[g|m|k] 堆大小 - -Xmn
[g|m|k] 年轻代大小 - -XX:NewRatio=老年代/新生代 比例
- -XX:MaxMetaspaceSize=
[unit] 元空间 - -XX:NewSize=
[unit] 初始年轻代大小 - -XX:SurvivorRatio=
# 代表分代回收算法新生代中 Eden:Survivor 的比例,注意 Survivor 是有两块的,如果 Eden:Survivor = 3,则新生代将被分割为 5 份,Eden 占 3 份
堆的最大和最小限制,网上很多资料都将二者设置成一样的值,我觉得这是不好的习惯,因为 JVM 只有在内存使用量达到-Xms 的值时才会开始 gc,设置成一样的值也就是说只有在 JVM 使用完内存后才会开始 gc,这会导致最大暂停时间偏长,用户体验不好,当然设置成一样也可以减轻堆伸缩带来的压力。当然这些都是直观的看法,根据 [3] 的说法,这个调整策略和 JVM 中使用的垃圾回收算法相关,如果是 IBM JVM(采用 sweep-compact),设置不一样较好;如果是 Sun JVM(采用分代回收),设置成一样较好。
选择哪个 GC 收集器
- -XX:+UseSerialGC
- -XX:+UseParallelGC
- -XX:+USeParNewGC
- -XX:+UseG1GC
- -XX:-UseConcMarkSweepGC
对老生代采用并发标记交换算法进行 GC
GC 的时候生成文件
- -XX:+UseGCLogFileRotation 用文件循环策略
- -XX:NumberOfGCLogFiles=10 最大文件数
- -XX:GCLogFileSize=50M 文件大小
- -Xloggc:/home/user/log/gc.log 位置
OOM后排错
- -XX:+HeapDumpOnOutOfMemoryError 打出 dump 文件当发生 out of memory
- -XX:HeapDumpPath=./java_pid
.hprof 文件名 - -XX:OnOutOfMemoryError=”< cmd args >;< cmd args >” 发生异常的时候执行的命令
- -XX:+UseGCOverheadLimit 在 GC 之前
Java Management Extensions
Java 管理扩展,打开 JMX 端口,就可以用标准的端口来监控 server 端的 jvm 了。
-Djava.rmi.server.hostname=
-Dcom.sun.management.jmxremote.port=
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false
print 所有+XX 参数(Java6 后)
-XX:+PrintFlagsFinal
-XX:+PrintFlagsInitial
生产环境参数设置
- 实际生产中很少去设置 gc 相关的详细参数,一般只要把 thread dump 处理好(及异常的时候生成 demp 文件)和 jmx 端口打开;
- 能设置好几个分代的内存空间就不错了。这个可以通过 jvm 的监控来设置根据 cpu 和 gc 的情况;
- 因为随着 JVM 的版本的升级,jvm 垃圾回收会也来越智能,但是我们必须要了解这些,因为面试的时候大牛为了显摆自己会问这些问题。
- 根据我们上面介绍的就算你设置+XX 有的时候也不一定有用,说不定哪个小版本里面就失灵了。jdk1.8 关于 gc 的最多开启+XX:+UseConcMarkSweepGC。
调优工具
对 JVM 的监控可以分为以下几个方面:
- 内存状况分析(GC)
- 线程状态分析
关于 GC 的监控,比较重要的有三个方面:
- 各个区的容量,主要是堆中新生代与老年代的内存分配。
- Full GC、Young GC 发生的次数,原则上尽量避免发生 Full GC,Young GC 能少则少。
- 当前系统的内存比、CPU 使用率。
jps
jps 列出正在运行的虚拟机进程
包括 PID、主类、jar 包等
1 | jps -ml |
jinfo
Java 配置信息工具,并且支持运行时动态修改部分参数
查看某些配置值或开关是否打开:
1 | jinfo -flag MaxTenuringThreshold 8737 |
打开开关
1 | jinfo -flag MaxTenuringThreshold=10 8737 |
查看虚拟机的默认配置参数还可以在运行时打开虚拟机的 PrintFlagsFinal 开关
1 | java -XX:+PrintFlagsFinal Test |
jstat
常用选项
1 | jstat -gcutil # 垃圾收集统计数据 |
统计数据列含义:
数据列 | 描述 | 支持的 jstat 选项 |
---|---|---|
S0C | Survivor0 的当前容量 | -gc -gccapacity -gcnew -gcnewcapacity |
S1C | S1 的当前容量 | -gc -gccapacity -gcnew -gcnewcapacity |
S0U | S0的使用量 | -gc-gcnew |
S1U | S1 的使用量 | -gc-gcnew |
EC | Eden 区的当前容量 | -gc -gccapacity -gcnew -gcnewcapacity |
EU | Eden 区的使用量 | -gc -gcnew |
OC | old 区的当前容量 | -gc -gccapacity -gcnew -gcnewcapacity |
OU | old区的使用量 | -gc-gcnew |
PC | 方法区的当前容量 | -gc-gccapacity -gcold -gcoldcapacity -gcpermcapacity |
PU | 方法区的使用量 | -gc -gcold |
YGC | Young GC 次数 | -gc -gccapacity -gcnew -gcnewcapacity -gcold -gcoldcapacity -gcpermcapacity -gcutil -gccause |
YGCT | Young GC 累积耗时 | -gc -gcnew -gcutil -gccause |
FGC | Full GC次数 | -gc -gccapacity -gcnew -gcnewcapacity -gcold -gcoldcapacity -gcpermcapacity -gcutil -gccause |
FGCT | Full GC 累积耗时 | -gc-gcold -gcoldcapacity -gcpermcapacity -gcutil -gccause |
GCT | GC 总的累积耗时 | -gc -gcold -gcoldcapacity -gccapacity -gcpermcapacity -gcutil -gccause |
NGCMN | 新生代最小容量 | -gccapacity -gcnewcapacity |
NGCMX | 新生代最大容量 | -gccapacity -gcnewcapacity |
NGC | 新生代当前容量 | -gccapacity -gcnewcapacity |
OGCMN | 老年代最小容量 | -gccapacity -gcoldcapacity |
OGCMX | 老年代最大容量 | -gccapacity -gcoldcapacity |
OGC | 老年代当前容量 | -gccapacity -gcoldcapacity |
PGCMN | 方法区最小容量 | -gccapacity -gcpermcapacity |
PGCMX | 方法区最大容量 | -gccapacity -gcpermcapacity |
PGC | 方法区当前容量 | -gccapacity -gcpermcapacity |
PC | 方法区的当前容量 | -gccapacity -gcpermcapacity |
PU | 方法区使用量 | -gccapacity -gcold |
LGCC | 上一次 GC 发生的原因 | -gccause |
GCC | 当前 GC 发生的原因 | -gccause |
TT | 存活阀值,如果对象在新生代移动次数超过此阀值,则会被移到老年代 | -gcnew |
MTT | 最大存活阀值,如果对象在新生代移动次数超过此阀值,则会被移到老年代 | -gcnew |
DSS | survivor 区的理想容量 | -gcnew |
jmap
1 | jmap -histo pid -F |
JConsole
略
JVisualVM
略
GC日志
关于输出 GC 日志的参数有以下几种:
- -XX:+PrintGC 输出 GC 日志
- -XX:+PrintGCDetails 输出 GC 的详细日志
- -XX:+PrintGCTimeStamps 输出 GC 的时间戳(以基准时间的形式)
- -XX:+PrintGCDateStamps 输出 GC 的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
- -XX:+PrintHeapAtGC 在进行 GC 的前后打印出堆的信息
- -Xloggc:../logs/gc.log 日志文件的输出路径
比如对 PrintGCDetails 这个参数:
1 | public class GCLogTest { |
在 IDE 中设置 VM 参数-XX:+PrintGCDetails
,再运行,可以得到:
1 | [GC (System.gc()) [PSYoungGen: 5051K->776K(38400K)] 5051K->784K(125952K), 0.0014035 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] |
第一行是 YoungGC,其结构如下图所示:
第二行是 FullGC,其结构如下图所示:
- GC日志开头的
[GC
和[Full GC
说明了这次垃圾收集的停顿类型,如果有Full
,说明这次 GC 发生了Stop-The-World
。因为是调用了System.gc()
方法触发的收集,所以会显示[Full GC (System.gc())
,不然是没有后面的(System.gc())
的。 [PSYoungGen
和[ParOldGen
是指 GC 发生的区域。- 在方括号中
PSYoungGen:
后面的5051K->776K(38400K)
代表的是GC前该内存区域已使用的容量->GC后该内存区域已使用的容量(该内存区域总容量)
- 在方括号之外的
5051K->784K(125952K)
代表的是GC前Java堆已使用容量->GC后Java堆已使用容量(Java堆总容量)
,注意已使用容量是减掉一个 Servivor 区、线程栈等区域后的大小。 - 再往后的
0.0014035 secs
代表该内存区域 GC 所占用的时间,单位是秒。 - 再后面的
[Times: user=0.01 sys=0.00, real=0.00 secs]
,user 代表进程在用户态消耗的 CPU 时间,sys 代表进程在内核态消耗的 CPU 时间、real 代表程序从开始到结束所用的时钟时间。这个时间包括其他进程使用的时间片和进程阻塞的时间(比如等待 I/O 完成)。 - 至于后面的
eden
代表的是 Eden 空间,还有from
和to
代表的是 Survivor 空间。
问题排查
OOM
有两种情况可能导致 OOM:
- 内存泄露(Memory Leak),某些对象是需要被释放的但是却由于某些原因释放不了,查看泄露对象对 GC Roots 的引用链,再追本溯源进行分析;
- 内存溢出(Memory Overflow),对象太多了导致堆放不下了,查看堆是否可以调大,或者某些对象活太久了。
产生 OutOfMemoryError 错误的具体原因有以下几种:
- java.lang.OutOfMemoryError: Java heap space 表示 Java 堆空间不足。当应用程序申请更多的内存时,若 Java 堆内存已经无法满足应用程序的需要,则将抛出这种异常。
1
2
3
4
5
6
7
8
9
10
11
12
13/*
VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
*/
public class HeapOOM {
static class OOMObject {}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<>();
while(true) {
list.add(new OOMObject());
}
}
} - java.lang.OutOfMemoryError: PermGen space,表示 Java 永久代(方法区)的空间不足。永久代用于存放类的字节码和常量池,类的字节码被加载后存放在这个区域,这和存放对象实例的堆区是不同的。大多数 JVM 的实现都不会对永久代进行垃圾回收,因此,只要类加载过多就会出现这个问题。一般的应用程序都不会产生这个错误,然而,对于 Web 服务器会产生大量的 JSP,JSP 在运行时被动态地编译为 Java Servlet 类,然后加载到方法区,因此,有很多 JSP 的 Web 工程可能会产生这个异常。使用 intern 测试运行时常量池是“永久代”的还是“元空间”的:
1
2
3
4
5
6
7
8
9
10
11
12/*
VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
*/
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
// 使用List保持对常量池的引用,避免Full GC回收常量池
List<String> list = new ArrayList<String>();
for(int i = 0;; i++) {
list.add(String.valueOf(i).intern());
}
}
}方法区溢出测试(使用 CGLib 动态生成类):1
2
3
4String str1 = new StringBuilder("计算机").append("软件").toString();
System.out.println(str1.intern() == str1);
String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern() == str2);jdk1.6 及之前版本,因为 HotSpot 实行“永久代”(PermGen),方法区保存到永久代中,虽然逻辑上属于堆,但是在这块空间上并没有实行 GC。在这种情况下,上面代码发生堆溢出是必然的。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21/*
VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
*/
public class MethodAreaOOM {
static class OOMObject {}
public static void main(String[] args) {
while(true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(HeapOOM.OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method,
Object[] objects, MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(o, args);
}
});
enhancer.create();
}
}
}
jdk1.7 及之后版本,因为“去永久代”,引入了“元空间”(Metaspace),这种策略使得大部分类元数据都保存在本地内存中,元空间使用一个全局空闲组块列表(一个大数组)表示,每创建一个类加载器都会从这个列表中获取一个自己的组块,用处当然是存储元信息(指针碰撞方式分配),当一个类加载器不再活动后,其所持有的组块列表也就返还给全局组块列表了,也就是说,类也是可能会被 GC 回收掉的。
运行时常量池是分配于方法区的,所以可以这么认为:1.6 及之前常量是分配在一个“全局静态区”的,而 1.7 及之后则在堆中分配。
运行时常量池导致的溢出不常见,上面的例子感觉也有点极端。
方法区导致的溢出在实际应用中常见:一些框架 Spring、Hibernate 在对类进行增强时会使用到 CGLib 这类字节码技术;JVM 上的动态语言(Groovy)会持续创建类来实现语言的动态特性;拥有大量 JSP 页面或会动态生成 JSP 文件的应用(JSP 第一次运行时会被编译为 Servlet);基于 OSGi 的应用(即使是同一个类文件,被不同的加载器加载也会视为不同的类)等。 - java.lang.OutOfMemoryError: unable to create new native thread,本质原因是创建了太多的线程,而系统允许创建的线程数是有限制的。
- java.lang.OutOfMemoryError:GC overhead limit exceeded,是并行(或者并发)垃圾回收器的 GC 回收时间过长、超过 98%的时间用来做 GC 并且回收了不到 2%的堆内存时抛出的这种异常,用来提前预警,避免内存过小导致应用不能正常工作。
- 栈溢出
可以先使用-Xoss 参数设置本地方法栈大小(在 HotSpot 中无效,因为它将两个栈合并了),-Xss 参数设置栈容量大小,设得稍微小一些都没有问题。
实验非常简单,就是定义并调用一个无限递归的方法,在调用深度到达一定程度后就会报错,并且使用一个 stackLength 成员变量记录栈深度有两种可能的错误,第一种是线程请求的栈深度超出了虚拟机的允许范围,会产生 StackOverflowError 异常,第二种是虚拟机在扩展栈时无法申请到足够空间,会产生 OutOfMemoryError 异常。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/*
VM Args: -Xss228k
*/
public class StackSOF {
private int stackLength = 1;
public void stackLeak() {
stackLength++;
stackLeak();
}
public static void main(String[] args) {
StackSOF oom = new StackSOF();
try {
oom.stackLeak();
} catch (Throwable e) {
System.out.println("stack length:" + oom.stackLength);
throw e;
}
}
}
/*
VM Args: -Xss2M
*/
public class StackOOM {
private void dontstop() {
while(true) {}
}
public void stackLeakByThread() {
while(true) {
new Thread(new Runnable() {
@Override
public void run() {
dontstop();
}
}).start();
}
}
public static void main(String[] args) {
new StackOOM().stackLeakByThread();
}
}
注意,一个 java 进程的内存容量是由操作系统决定的,Windows 下限制为 2GB,减去 Xmx(最大堆容量)、MaxPermSize(最大方法区容量)、程序计数器(很小)、虚拟机本身耗费的内存,剩下的就由虚拟机栈和本地方法栈瓜分了。
前者比较容易找出错误,因为会有错误堆栈可以分析;
后者往往是因为线程分配过多了,导致操作系统分配的内存用尽,事实上,每个线程的主要空间都被栈(虚拟机栈和本地方法栈)占用了,所以为了可以分配更多的线程,可以减少最大堆容量或者减少栈容量。 - 本机直接内存溢出
这个例子比较复杂,首先-XX:MaxDirectMemorySize 指定了直接内存大小(默认是-Xmx),然后越过了 DirectByteBuffer,反射获取 Unsafe 实例进行内存分配,allocateMemory 等价于 malloc直接内存,或者说堆外内存,不是在 java 虚拟机规范中定义的存储区域,一般是不受虚拟机控制的,但是 NIO 提供了 Native 函数库可以直接分配堆外内存。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15/*
VM Args: -Xmx20M -XX:MaxDirectMemorySize=10M
*/
public class DirectMemoryOOM {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) throws IllegalAccessException {
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while(true) {
unsafe.allocateMemory(_1MB);
}
}
}
由 DirectMemory 导致的内存溢出在 Heap Dump 中不会有明显的异常,如果 OOM 之后 Dump 文件很小,且程序中又直接或间接使用了 NIO,就可以考虑是这方面的问题。
分析内存
为排查内存泄露、young gc耗时过长等问题,我们需要分析内存结构。
判断死锁
使用 jstack 判断死锁,下面是一段测试用的死锁代码:
1 | public class DeadLockTest { |
使用 jps 查看 Java 进程:
1 | # jps |
使用 jstack 查看 Java 进程中的所有线程:
1 | # jstack 3992 |
服务器 CPU 打满怎么排查
CPU 打满会导致服务器响应速度变慢甚至夯住,一般查看内存没有明显问题后我们就可以怀疑是有线程运行将 CPU 打满了。
1、查 CPU 占用率较高的进程
top 命令查进程占用 CPU
2、查该进程占用 CPU 最高的线程top -H -p <查出的进程号>
1 | root@app02:~# top -H -p 1153 |
转成 16 进制:
1 | hero@app02:~$ printf "%x\n" 1487 |
之后我们会用到这个值,因为 jstack 输出的 log 中使用十六进制表示线程编号。
3、输出
1 | root@app02:~# jstack 1153 > test_jstack.txt |
从结果中可以搜到上面给出的线程编码5cf
:
1 | 11179 "New I/O worker #168" #299 daemon prio=5 os_prio=0 tid=0x00007f3a75127000 nid=0x5cf runnable [0x00007f37278f7000] |
SWAP 影响 GC
SWAP 和 GC 同时发生会导致 GC 时间很长,JVM 严重卡顿,甚至导致服务崩溃。
JVM 进行 GC 时,需要对相应堆分区的已用内存进行遍历;假如 GC 的时候,有堆的一部分内容被交换到 SWAP 中,遍历到这部分的时候就需要将其交换回内存,同时由于内存空间不足,就需要把内存中堆的另外一部分换到 SWAP 中去;于是在遍历堆分区的过程中,(极端情况下)会把整个堆分区轮流往 SWAP 写一遍。
QA
栈溢出和由栈引起的OOM有什么关系?
虽然都是由递归调用引起的,但是这两种异常引起的条件并不相同:
- 栈溢出(StackOverflowError)
方法调用栈深度超出了虚拟机的允许范围。 - 栈引起的OOM
虚拟机在扩展栈时无法申请到足够空间。