性能文章>【全网首发】一次jstack 的排查之旅>

【全网首发】一次jstack 的排查之旅原创

2年前
617249

前言

今天阳光明媚,天气真好,奈何是工作日,不然大睡一场,岂不美哉。

还残留在昨天的美梦中,突然群内的告警把我拉回了现实,奈何还在车上,不能拿上电脑,疯狂输出。

群内告警,接口频繁超时,运维查看了机器情况,超时的接口的应用的CPU,居高不下,如下图

好家伙,一大早就给我喂饭,其实这个问题也没什么,jstack打印下,再下来根据日志分析分析,多半能找到原因。

但是奇怪的问题是,运维在使用jstack打印的时候出现了问题,如下图,这就有点意思了,让我深入的分析了下jstack。

是的,出现了connection refused导致jstack打印没成功,为了不影响客户的使用,紧急重启,此时我还在骑马的路上。

  • 系统环境:centos6
  • java版本:1.6.0_24

分析

jstack打印出现connection refused我倒是第一次见,不知道什么原因,那就只有谷歌了。不知道是不是我检索的方式有问题,没有找到有用的信息。那既然谷歌找不到有用的信息,那就只能看源代码了

  • 下载jdk1.6的源代码

找到sun.tools.jstack.JStack这个类,这里面的代码并不复杂。

  • 这里是方法的入口,大概意思,就是解析jstack -xx后面的参数,需要注意点就是使用-F -m 和不使用-F -m的时候走的逻辑是不一样的。-F -m的时候useSA =true
   if (args.length == 0) {
            usage(); // no arguments
        }

        boolean useSA = false;
        boolean mixed = false;
        boolean locks = false;

        // Parse the options (arguments starting with "-" )
        int optionCount = 0;
        while (optionCount < args.length) {
            String arg = args[optionCount];
            if (!arg.startsWith("-")) {
                break;
            }
            if (arg.equals("-F")) {
                useSA = true;
            } else {
                if (arg.equals("-m")) {
                    mixed = true;
                } else {
                    if (arg.equals("-l")) {
                       locks = true;
                    } else {
                        usage();
                    }
                }
            }
            optionCount++;
        }
  • 由于上面是没有使用任何参数,所以直接看不带-F的逻辑
      if (useSA) {
            // parameters (<pid> or <exe> <core>
            String params[] = new String[paramCount];
            for (int i=optionCount; i<args.length; i++ ){
                params[i-optionCount] = args[i];
            }
            runJStackTool(mixed, locks, params);
        } else {
            // pass -l to thread dump operation to get extra lock info
            String pid = args[optionCount];
            String params[];
            if (locks) {
                params = new String[] { "-l" };
            } else {
                params = new String[0];
            }
            runThreadDump(pid, params);
        }
  • 查看runThreadDump的方法,这里的注意逻辑在于remoteDataDump方法
  private static void runThreadDump(String pid, String args[]) throws Exception {
        VirtualMachine vm = null;
        try {
            // 1.attach 传入进来的pid
            vm = VirtualMachine.attach(pid);
        } catch (Exception x) {
            String msg = x.getMessage();
            if (msg != null) {
                System.err.println(pid + ": " + msg);
            } else {
                x.printStackTrace();
            }
            if ((x instanceof AttachNotSupportedException) &&
                (loadSAClass() != null)) {
                System.err.println("The -F option can be used when the target " +
                    "process is not responding");
            }
            System.exit(1);
        }

        // Cast to HotSpotVirtualMachine as this is implementation specific
        // method.
        // 
        2. remote dump
        InputStream in = ((HotSpotVirtualMachine)vm).remoteDataDump((Object[])args);

        // read to EOF and just print output
        3. 输出dump的信息
        byte b[] = new byte[256];
        int n;
        do {
            n = in.read(b);
            if (n > 0) {
                String s = new String(b, 0, n, "UTF-8");
                System.out.print(s);
            }
        } while (n > 0);
        in.close();
        vm.detach();
    }
  • 调用remoteDataDump
  public InputStream remoteDataDump(Object ... args) throws IOException {
        return executeCommand("threaddump", args);
    }
  • 调用executeCommand
   private InputStream executeCommand(String cmd, Object ... args) throws IOException {
        try {
            return execute(cmd, args);
        } catch (AgentLoadException x) {
            throw new InternalError("Should not get here", x);
        }
    }
  • 这里的execute有几种实现,我们查看linux的实现

  • linux平台的execute的实现

 InputStream execute(String cmd, Object ... args) throws AgentLoadException, IOException {
        assert args.length <= 3;                // includes null

        // did we detach?
        String p;
        synchronized (this) {
            if (this.path == null) {
                throw new IOException("Detached from target VM");
            }
            p = this.path;
        }

        // create UNIX socket
        // 创建unix socket
        int s = socket();

        // connect to target VM
        try {

            connect(s, p);
        } catch (IOException x) {
            close(s);
            throw x;
        }

        IOException ioe = null;

        // connected - write request
        // <ver> <cmd> <args...>
        try {
            writeString(s, PROTOCOL_VERSION);
            writeString(s, cmd);

            for (int i=0; i<3; i++) {
                if (i < args.length && args[i] != null) {
                    writeString(s, (String)args[i]);
                } else {
                    writeString(s, "");
                }
            }
        } catch (IOException x) {
            ioe = x;
        }


        // Create an input stream to read reply
        SocketInputStream sis = new SocketInputStream(s);

        // Read the command completion status
        int completionStatus;
        try {
            completionStatus = readInt(sis);
        } catch (IOException x) {
            sis.close();
            if (ioe != null) {
                throw ioe;
            } else {
                throw x;
            }
        }

        if (completionStatus != 0) {
            sis.close();

            // In the event of a protocol mi**atch then the target VM
            // returns a known error so that we can throw a reasonable
            // error.
            if (completionStatus == ATTACH_ERROR_BADVERSION) {
                throw new IOException("Protocol mi**atch with target VM");
            }

            // Special-case the "load" command so that the right exception is
            // thrown.
            if (cmd.equals("load")) {
                throw new AgentLoadException("Failed to load agent library");
            } else {
                throw new IOException("Command failed in target VM");
            }
        }

        // Return the input stream so that the command output can be read
        return sis;
    }
  • 在这里我们已经可以看到前面出现的connection refused大概是因为连接的时候失败了,然后出现的异常。

思考

通过上面的分析,我们都已经知道了为什么出现connection refused,但是我们怎么在出现connection refused的时候,获取到更多的java信息,以便后续分析问题呢。

上面的源代码有提到,使用jstack 带-F 和不带-F的时候走的是不同的逻辑,那我们就看看带-F的逻辑吧

  • -F的时候useSA为true,所以这里会执行runJStackTool
    1.mixed 使用-F的时候有没有带-m,有为true,没有是false

2.locks 使用-F的时候有没有带-l,有为true,没有是false

3.params 使用-标志后的其他参数,通常是pid

使用jstack -h的时候,可以看到jstack的选项说明


Options:
    -F  to force a thread dump. Use when jstack <pid> does not respond (process is hung)
    -m  to print both java and native frames (mixed mode)
    -l  long listing. Prints additional information about locks
    -h or -help to print this help message
if (useSA) {
            // parameters (<pid> or <exe> <core>
            String params[] = new String[paramCount];
            for (int i=optionCount; i<args.length; i++ ){
                params[i-optionCount] = args[i];
            }
            
            runJStackTool(mixed, locks, params);
        } else {
            // pass -l to thread dump operation to get extra lock info
            String pid = args[optionCount];
            String params[];
            if (locks) {
                params = new String[] { "-l" };
            } else {
                params = new String[0];
            }
            runThreadDump(pid, params);
        }
  • runJStackTool方法,使用反射去执行sun.jvm.hotspot.tools.JStackmain方法进行dump
    private static void runJStackTool(boolean mixed, boolean locks, String args[]) throws Exception {
        Class<?> cl = loadSAClass();
        if (cl == null) {
            usage();            // SA not available
        }

        // JStack tool also takes -m and -l arguments
        if (mixed) {
            args = prepend("-m", args);
        }
        if (locks) {
            args = prepend("-l", args);
        }

        Class[] argTypes = { String[].class };
        Method m = cl.getDeclaredMethod("main", argTypes);

        Object[] invokeArgs = { args };
        m.invoke(null, invokeArgs);
    }
  • 查看sun.jvm.hotspot.tools.JStackmain方法
 public static void main(String[] args) {
        boolean mixedMode = false;
        boolean concurrentLocks = false;
        int used = 0;
        for (int i = 0; i < args.length; i++) {
            if (args[i].equals("-m")) {
                mixedMode = true;
                used++;
            } else if (args[i].equals("-l")) {
                concurrentLocks = true;
                used++;
            }
        }

        if (used != 0) {
            String[] newArgs = new String[args.length - used];
            for (int i = 0; i < newArgs.length; i++) {
                newArgs[i] = args[i + used];
            }
            args = newArgs;
        }

        JStack jstack = new JStack(mixedMode, concurrentLocks);
        jstack.start(args);
        jstack.stop();
    }
  • 查看run方法
    public void run() {
        Tool tool = null;
        if (mixedMode) {
            tool = new PStack(false, concurrentLocks);
        } else {
            tool = new StackTrace(false, concurrentLocks);
        }
        tool.setAgent(getAgent());
        tool.setDebugeeType(getDebugeeType());
        tool.run();
    }

在这里我们理清了jstack大概的逻辑,下面来实践

实践

  • jstack -m 打印混合模式(Java 和本机 C/C++ 帧)堆栈跟踪。
  • jstack -F 当没有响应时强制打印堆栈

在jdk1.6.0_24的时候使用jstack -F 会有问题,这是一个已知的jdk的bug,

hread 26724: (state = BLOCKED)
Error occurred during stack walking:
sun.jvm.hotspot.debugger.DebuggerException: sun.jvm.hotspot.debugger.DebuggerException: get_thread_regs failed for a lwp
        at sun.jvm.hotspot.debugger.linux.LinuxDebuggerLocal$LinuxDebuggerLocalWorkerThread.execute(LinuxDebuggerLocal.java:152)
        at sun.jvm.hotspot.debugger.....

设置export LIBSAPROC_DEBUG=1,再次使用jstack -F pid,会发现多了一行

Thread 26724: (state = BLOCKED)
libsaproc DEBUG: ptrace(PTRACE_GETREGS, ...) not supported

这里是因为缺少libsaproc.so文件,jdk1.6.30已经修复

我本来想下载个jdk1.6.30的新版本,但是oracle下载jdk居然要登录,然后直接把jdk1.8下的拷贝到jdk.1.6下来也能用

cp jdk1.8.0_77/jre/lib/amd64/libsaproc.so jdk1.6/jre/lib/amd64/libsaproc.so

总结

在出现以上的情况下,我们可以使用

  • jstack -F pid
  • jstack -m pid
  • pstack pid
点赞收藏
分类:标签:
少放盐
请先登录,查看4条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

JDBC PreparedStatement 字段值为null导致TBase带宽飙升的案例分析

JDBC PreparedStatement 字段值为null导致TBase带宽飙升的案例分析

9
4