本文主要是以常见的线上问题进行模拟,然后介绍定位问题的方法。

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的占用率和内存使用情况:

java线上故障分享 java线上问题定位_java

 通过执行top -Hp 429855 查看 Java 线程情况

java线上故障分享 java线上问题定位_java_02

执行 printf '%x' 429873 获取 16 进制的线程 id,用于dump信息查询,结果为 68f31

java线上故障分享 java线上问题定位_堆内存_03

最后我们执行jstack 429855 |grep -A 20 68f31来查看下详细的dump信息.可以直接定位出问题方法和代码行

java线上故障分享 java线上问题定位_java_04

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情况。

java线上故障分享 java线上问题定位_线上问题排查_05

很明显,内存溢出了,堆内存经过42次 Full Gc 之后都没释放出可用内存,这说明当前堆内存中的对象都是存活的,有GC Roots引用,无法回收。我们可以使用Leak Suspects Report工具对保存的dump文件进行检测排查。工具会直接给你列出问题报告:

java线上故障分享 java线上问题定位_线上问题排查_06

 然后会列出可疑的内存泄漏的问题,然后我们可以按照右侧的问题进行逐个的问题排查和分析。