常见问题排查
OOM
概述
OOM,全称“Out Of Memory”,翻译成中文就是“内存用完了”,来源于java.lang.OutOfMemoryError。看下关于的官方说明: Thrown when the Java Virtual Machine cannot allocate an object because it is out of memory, and no more memory could be made available by the garbage collector. 意思就是说,当JVM因为没有足够的内存来为对象分配空间并且垃圾回收器也已经没有空间可回收时,就会抛出这个error(注:非exception,因为这个问题已经严重到不足以被应用处理)。
为什么会OOM
为什么会没有内存了呢?原因不外乎有两点:
1)分配的少了:比如虚拟机本身可使用的内存(一般通过启动时的VM参数指定)太少。
2)应用用的太多,并且用完没释放,浪费了。此时就会造成内存泄露或者内存溢出。
内存泄露:申请使用完的内存没有释放,导致虚拟机不能再次使用该内存,此时这段内存就泄露了,因为申请者不用了,而又不能被虚拟机分配给别人用。
内存溢出:申请的内存超出了JVM能提供的内存大小,此时称之为溢出。
在之前没有垃圾自动回收的日子里,比如C语言和C++语言,我们必须亲自负责内存的申请与释放操作,如果申请了内存,用完后又忘记了释放,比如C++中的new了但是没有delete,那么就可能造成内存泄露。偶尔的内存泄露可能不会造成问题,而大量的内存泄露可能会导致内存溢出。
而在Java语言中,由于存在了垃圾自动回收机制,所以,我们一般不用去主动释放不用的对象所占的内存,也就是理论上来说,是不会存在“内存泄露”的。但是,如果编码不当,比如,将某个对象的引用放到了全局的Map中,虽然方法结束了,但是由于垃圾回收器会根据对象的引用情况来回收内存,导致该对象不能被及时的回收。如果该种情况出现次数多了,就会导致内存溢出,比如系统中经常使用的缓存机制。Java中的内存泄露,不同于C++中的忘了delete,往往是逻辑上的原因泄露。
排查思路
导出堆栈信息
-
使用-dump:live,format=b,file=java_pid64220.hprof 生成堆转储快照。慎用,容易挂起java线程或者宕机
-
配置jvm参数,当发生OOM时打印堆栈信息(推荐)
-server
-XX:+PrintGCDetails
-Xloggc:./gc.log
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=./
- 使用mat 导出jvm heap 信息。当发生垃圾回收时,导出效率很慢
分析大对象
使用mat,idea等工具找出占用内存特别大的对象
- 使用mat
排查示例
1.模拟OOM代码
@RequestMapping("/moreObj")
public ResponseDto gcTest() {
Map<String, Hanson> map = new HashMap<String, Hanson>();
int counter = 1;
while (true) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
Hanson h = new Hanson();
String[] friends = new String[counter];
for (int i = 0; i < friends.length; i++) {
friends[i] = "friends" + i;
}
h.setAge(counter);
h.setName("hanson" + counter);
h.setFriends(friends);
map.put(h.getName(), h);
if (counter % 100 == 0)
System.out.println("put" + counter);
counter++;
}
}
- 准备堆栈信息
自行导出
3.使用mat分析
使用依赖树分析
排查出cpu 消耗过高的代码
分析
-
非计算密集型任务cpu占用过高:有用户线程cpu过高、gc线程cpu过高。用户线程cpu过高一般是出现了死循环,需要查看线程堆栈找出问题根源。gc线程过高一般是Full GC频繁,Full GC频繁一般是因为老年代或方法区(元空间)内存不足。需要对gc日志分析、对堆转储文件分析,确定是哪些对象实例占用了过多内存、反射的类是否过多、被多个类加载器加载的类是否过多等,决定是增加内存大小还是对源码进行处理等。
-
老年代已使用空间大于70%:有可能发生了内存泄漏、有可能是设置的堆大小无法满足正常业务的需求。需要对堆转储文件分析,确定是哪些对象实例占用了过多内存,并检查是否发生了内存泄漏。对堆大小无法满足正常业务的需求,应调大堆大小,通过-Xms和-Xmx来设置。
-
Full GC频繁:Full GC频繁一般是因为老年代或方法区(元空间)内存不足。需要对gc日志分析、对堆转储文件分析,确定是哪些对象实例占用了过多内存、反射的类是否过多、被多个类加载器加载的类是否过多等,决定是增加内存大小还是对源码进行处理等。
排查思路
- 找出服务进程
- 找出当前进程下有问题的线程
- 查看线程堆栈信息,找出有问题的代码
排查示例(jdk 工具类)
- 模拟消耗cpu代码
@RequestMapping("/cpuLoad")
public ResponseDto cpuLoad() {
Runnable target;
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
long i = 0;
while (true) {
i++;
}
}
});
thread.setName("rumenz");
thread.start();
return new ResponseDto().data(null);
}
- 使用jsp 找出相关服务进程id
- 找出消耗过高的线程 linux : top -Hp [pid]
windows: 没有好用的命令,推荐使用微软的资源管理器
- 使用jstack [pid] |grep [线程id 16进制] 消耗cpu 过高的线程id:
转成16进制 :4b18
- 使用阿里开源arthas