1 参数调试
1.1 GC日志参数
- -XX:+PrintGCDetails 打印GC详细日志
- -XX:+HeapDumpOnOutOfMemoryError 设置当OutOfMemoryError的时候,dump堆区的情况
- -XX:+HeapDumpBeforeFullGC 设置发现FullGC之前dump堆区信息
- -XX:+HeapDumpAfterFullGC 设置发现FullGC之后dump堆区信息
- -XX:HeapDumpPath 指定dump文件
- -Xloggc 指定GC日志文件
例如
-Xms2048m
-Xmx2048m
-Xmn1048m
-XX:+PrintGCDetails
-XX:+HeapDumpOnOutOfMemoryError
-XX:+HeapDumpBeforeFullGC
-XX:+HeapDumpAfterFullGC
-XX:HeapDumpPath=/tmp/headoom.dump
-Xloggc:log/gc.log
1.2 堆区的参数控制
- -Xmx 堆区最大值
- -Xms 堆区初始大小
- -Xmn 年轻代空间
- -XX:NewRatio 老年代:年轻代的比例, 如2 代表 young:old = 1:2
- -XX:SurvivorRatio Eden:Survivor的比例 如8 代表 eden:s0:s1 = 8:1:1
堆区内存溢出示例:
/**
* 堆溢出示例
* 可以通过调整参数值,看看内存溢出时候GC的情况
* VM-args:-Xms20m -Xmx20m -Xmn10m -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\tempwork\headoom.dump
*/
public class HeapOOM {
static class OOMObject{
private static String test="test";
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<>();
while(true){
list.add(new OOMObject());
}
}
}
1.3 方法区、数据元区参数控制
- -XX:PermSize 永久代初始大小
- -XX:MaxPermSize (永久区,jdk8之后为metaspace)
- -XX:MetaspaceSize=8m 元数据区初始大小
- -XX:MaxMetaspaceSize=80m 元数据区最大值
- -XX:CompressedClassSpaceSize=512m class压缩大小
元数据区内存溢出示例
/**
* 数据元区溢出示例
* 数据元区存储的是类的信息,所以通过cglib不断的创建类,就可以使元数据区内存溢出,
* 默认元数据区就是机器的物理内存,所以测试设置下最大值测试比较稳妥
* VM-args: -Xms256m -Xmx256m -Xmn128m -XX:MetaspaceSize=5m -XX:MaxMetaspaceSize=9m -XX:+PrintGCDetails
*/
public class MetaspaceOOM {
public static void main(String[] args) {
int i = 0;
try {
while (true) {
i++;
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(MetaspaceOOMObj.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(obj, args);
}
});
enhancer.create();
}
} catch (Exception e) {
System.out.println("第" + i + "次时发生异常");
e.printStackTrace();
}
}
static class MetaspaceOOMObj {
// public static final String test = "test";
}
}
1.4 栈区参数控制
- -Xss 栈空间大小
栈空间大小示例:
/**
* 栈溢出示例
* 栈存储的方法的局部变量、操作栈、动态链接、返回地址。想要它溢出,使用一个没有结束的递归调用即可。
* VM-args: -Xss128k -XX:+PrintGCDetails -Xloggc:log/gc.log
* 修改Xss的值,查看调用的栈的深度
*/
public class JavaVMStackSOF {
private int stackLength = 1;
public void staticLeak() {
stackLength++;
staticLeak();
}
public static void main(String[] args) {
JavaVMStackSOF oom = new JavaVMStackSOF();
try {
oom.staticLeak();
}catch(Throwable e){
System.out.println("static length: "+ oom.stackLength);
// throw e;
}
}
}
2 参数配置估算(内存估算)
2.1 参数基本策略
各分区的大小对GC的性能影响很大。如何将各分区调整到合适的大小,分析活跃数据的大小是很好的切入点。
活跃数据的大小是指,应用程序稳定运行时长期存活对象在堆中占用的空间大小,也就是Full GC后堆中老年代占用空间的大小。
可以通过GC日志中Full GC之后老年代数据大小得出,比较准确的方法是在程序稳定后,多次获取GC数据,通过取平均值的方式计算活跃数据的大小。活跃数据和各分区之间的比例关系如下:
空间 倍数 总大小 3-4 倍活跃数据的大小 新生代 1-1.5 活跃数据的大小 老年代 2-3 倍活跃数据的大小 永久代 1.2-1.5 倍Full GC后的永久代空间占用
例如,根据GC日志获得老年代的活跃数据大小为300M,那么各分区大小可以设为:
- 总堆:1200MB = 300MB × 4*
- 新生代:450MB = 300MB × 1.5*
- 老年代: 750MB = 1200MB - 450MB*
这部分设置仅仅是堆大小的初始值,后面的优化中,可能会调整这些值,具体情况取决于应用程序的特性和需求。
2.2 参数估算示例
参考示例:每天百万交易的支付系统,生产环境该怎么设置JVM堆内存大小
3 问题排查
通常我们在线上遇到JVM的问题大概有两种: 内存溢出(内存占用过大)、频繁GC或者FullGC。
3.1 JVM常见问题排查思路
分析工具
-
ps
,查看进程id -
top -p [pid] -H
,top命令:Linux命令。可以查看实时的内存使用情况。 -
jstack -l [pid] > stack.log
,jstack跟踪堆栈信息。 -
jmap -histo:live [pid]
,然后分析具体的对象数目和占用内存大小,从而定位代码。 -
jmap -dump:live,format=b,file=xxx.xxx [pid]
,然后利用MAT工具分析是否存在内存泄漏等等。 - 使用VisualVm 进行分析
pmap -x [pid]
查看进程的内存映射情况
总结
- 如果有OOM的话,根据分析dump的情况,结合代码看是或否是代码的漏洞,如果有代码漏洞,修复代码(如:不小心写了产生死循环的代码)
- 如果代码没有问题,可能是之前JVM设置的内存过小,调整JVM参数设置
- 如果没有OOM,而是频繁的GC。分析调整 young old 的占比(-XX:NewRatio),survivor的占比(-XX:SurvivorRatio),考虑年轻代和老年代大小是否合适。
3.2 JVM常见问题排查示例
参考资料
网友评论