本文主要是以常见的线上问题进行模拟,然后介绍定位问题的方法。
1.环境准备:
基础环境 jdk1.8,采用 SpringBoot 框架来写几个接口来触发模拟场景,首先是模拟 CPU 占满情况
2.问题列举
2.1 cpu占用率较高
模拟cpu占用率较高,实现方法较简单,用一个死循环占用cpu计算即可。
代码模拟:
/**
* 模拟CPU占满
*/
@GetMapping("/cpu/loop")
public void testCPULoop() throws InterruptedException {
int num = 0;
while (true) {
num++;
if (num == Integer.MAX_VALUE) {
System.out.println("reset");
}
num = 0;
}
}
模拟cpu占用过高的场景:
curl localhost:80/cpu/loop
通过执行top命令查看各个进程cpu的占用率和内存使用情况:
通过执行top -Hp 429855
查看 Java 线程情况
执行 printf '%x' 429873
获取 16 进制的线程 id,用于dump
信息查询,结果为 68f31
。
最后我们执行jstack 429855 |grep -A 20 68f31
来查看下详细的dump
信息.可以直接定位出问题方法和代码行
2.2 内存泄露
模拟内存泄露场景,使用ThreadLocal。ThreadLocal 是一个线程私有变量,可以绑定到线程上,在整个线程的生命周期都会存在,但是由于 ThreadLocal 的特殊性,ThreadLocal 是基于 ThreadLocalMap 实现的,ThreadLocalMap 的 Entry 继承 WeakReference,而 Entry 的 Key 是 WeakReference 的封装,换句话说 Key 就是弱引用,弱引用在下次 GC 之后就会被回收,如果 ThreadLocal 在 set 之后不进行后续的操作,因为 GC 会把 Key 清除掉,但是 Value 由于线程还在存活,所以 Value 一直不会被回收,最后就会发生内存泄漏。
代码模拟:
/**
* 模拟内存泄漏
*/
@GetMapping(value = "/memory/leak")
public void leak() {
System.out.println("模拟内存泄漏");
ThreadLocal<Byte[]> localVariable = new ThreadLocal<Byte[]>();
localVariable.set(new Byte[4096 * 1024]);// 为线程添加变量
}
我们给启动加上堆内存大小限制,同时设置内存溢出的时候输出堆栈快照并输出日志。
java -jar -Xms500m -Xmx500m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:/tmp/heaplog.log analysis-demo-0.0.1-SNAPSHOT.jar
测试方法:
1.循环触发memory/leak方法,直到出现java.lang.OutOfMemoryError异常
2.使用命令jstat -gc pid(进程号)查看程序的gc情况。
很明显,内存溢出了,堆内存经过42次 Full Gc 之后都没释放出可用内存,这说明当前堆内存中的对象都是存活的,有GC Roots引用,无法回收。我们可以使用Leak Suspects Report工具对保存的dump文件进行检测排查。工具会直接给你列出问题报告:
然后会列出可疑的内存泄漏的问题,然后我们可以按照右侧的问题进行逐个的问题排查和分析。