learn.lianglianglee.com/专栏/Java 业务开发常见错误 100 例/35 加餐5:分析定位Java问题,一定要用好这些工具(二).md.html
2022-05-11 18:57:05 +08:00

1333 lines
39 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>35 加餐5分析定位Java问题一定要用好这些工具.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/00 开篇词 业务代码真的会有这么多坑?.md.html">00 开篇词 业务代码真的会有这么多坑?.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/01 使用了并发工具类库,线程安全就高枕无忧了吗?.md.html">01 使用了并发工具类库,线程安全就高枕无忧了吗?.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/02 代码加锁:不要让“锁”事成为烦心事.md.html">02 代码加锁:不要让“锁”事成为烦心事.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/03 线程池:业务代码最常用也最容易犯错的组件.md.html">03 线程池:业务代码最常用也最容易犯错的组件.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/04 连接池:别让连接池帮了倒忙.md.html">04 连接池:别让连接池帮了倒忙.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/05 HTTP调用你考虑到超时、重试、并发了吗.md.html">05 HTTP调用你考虑到超时、重试、并发了吗.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/06 2成的业务代码的Spring声明式事务可能都没处理正确.md.html">06 2成的业务代码的Spring声明式事务可能都没处理正确.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/07 数据库索引:索引并不是万能药.md.html">07 数据库索引:索引并不是万能药.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/08 判等问题:程序里如何确定你就是你?.md.html">08 判等问题:程序里如何确定你就是你?.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/09 数值计算:注意精度、舍入和溢出问题.md.html">09 数值计算:注意精度、舍入和溢出问题.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/10 集合类坑满地的List列表操作.md.html">10 集合类坑满地的List列表操作.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/11 空值处理分不清楚的null和恼人的空指针.md.html">11 空值处理分不清楚的null和恼人的空指针.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/12 异常处理:别让自己在出问题的时候变为瞎子.md.html">12 异常处理:别让自己在出问题的时候变为瞎子.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/13 日志:日志记录真没你想象的那么简单.md.html">13 日志:日志记录真没你想象的那么简单.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/14 文件IO实现高效正确的文件读写并非易事.md.html">14 文件IO实现高效正确的文件读写并非易事.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/15 序列化:一来一回你还是原来的你吗?.md.html">15 序列化:一来一回你还是原来的你吗?.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/16 用好Java 8的日期时间类少踩一些“老三样”的坑.md.html">16 用好Java 8的日期时间类少踩一些“老三样”的坑.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/17 别以为“自动挡”就不可能出现OOM.md.html">17 别以为“自动挡”就不可能出现OOM.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/18 当反射、注解和泛型遇到OOP时会有哪些坑.md.html">18 当反射、注解和泛型遇到OOP时会有哪些坑.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/19 Spring框架IoC和AOP是扩展的核心.md.html">19 Spring框架IoC和AOP是扩展的核心.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/20 Spring框架框架帮我们做了很多工作也带来了复杂度.md.html">20 Spring框架框架帮我们做了很多工作也带来了复杂度.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/21 代码重复:搞定代码重复的三个绝招.md.html">21 代码重复:搞定代码重复的三个绝招.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/22 接口设计:系统间对话的语言,一定要统一.md.html">22 接口设计:系统间对话的语言,一定要统一.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/23 缓存设计:缓存可以锦上添花也可以落井下石.md.html">23 缓存设计:缓存可以锦上添花也可以落井下石.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/24 业务代码写完,就意味着生产就绪了?.md.html">24 业务代码写完,就意味着生产就绪了?.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/25 异步处理好用,但非常容易用错.md.html">25 异步处理好用,但非常容易用错.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/26 数据存储NoSQL与RDBMS如何取长补短、相辅相成.md.html">26 数据存储NoSQL与RDBMS如何取长补短、相辅相成.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/27 数据源头:任何客户端的东西都不可信任.md.html">27 数据源头:任何客户端的东西都不可信任.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/28 安全兜底:涉及钱时,必须考虑防刷、限量和防重.md.html">28 安全兜底:涉及钱时,必须考虑防刷、限量和防重.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/29 数据和代码:数据就是数据,代码就是代码.md.html">29 数据和代码:数据就是数据,代码就是代码.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/30 如何正确保存和传输敏感数据?.md.html">30 如何正确保存和传输敏感数据?.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/31 加餐1带你吃透课程中Java 8的那些重要知识点.md.html">31 加餐1带你吃透课程中Java 8的那些重要知识点.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/32 加餐2带你吃透课程中Java 8的那些重要知识点.md.html">32 加餐2带你吃透课程中Java 8的那些重要知识点.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/33 加餐3定位应用问题排错套路很重要.md.html">33 加餐3定位应用问题排错套路很重要.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/34 加餐4分析定位Java问题一定要用好这些工具.md.html">34 加餐4分析定位Java问题一定要用好这些工具.md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/Java 业务开发常见错误 100 例/35 加餐5分析定位Java问题一定要用好这些工具.md.html">35 加餐5分析定位Java问题一定要用好这些工具.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/36 加餐6这15年来我是如何在工作中学习技术和英语的.md.html">36 加餐6这15年来我是如何在工作中学习技术和英语的.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/37 加餐7程序员成长28计.md.html">37 加餐7程序员成长28计.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/38 加餐8Java程序从虚拟机迁移到Kubernetes的一些坑.md.html">38 加餐8Java程序从虚拟机迁移到Kubernetes的一些坑.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/答疑篇:代码篇思考题集锦(一).md.html">答疑篇:代码篇思考题集锦(一).md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/答疑篇:代码篇思考题集锦(三).md.html">答疑篇:代码篇思考题集锦(三).md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/答疑篇:代码篇思考题集锦(二).md.html">答疑篇:代码篇思考题集锦(二).md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/答疑篇:加餐篇思考题答案合集.md.html">答疑篇:加餐篇思考题答案合集.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/答疑篇:安全篇思考题答案合集.md.html">答疑篇:安全篇思考题答案合集.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/答疑篇:设计篇思考题答案合集.md.html">答疑篇:设计篇思考题答案合集.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/结束语 写代码时,如何才能尽量避免踩坑?.md.html">结束语 写代码时,如何才能尽量避免踩坑?.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>35 加餐5分析定位Java问题一定要用好这些工具</h1>
<p>你好,我是朱晔。</p>
<p>在上一篇加餐中,我们介绍了使用 JDK 内置的一些工具、网络抓包工具 Wireshark 去分析、定位 Java 程序的问题。很多同学看完这一讲,留言反馈说是“打开了一片新天地,之前没有关注过 JVM”“利用 JVM 工具发现了生产 OOM 的原因”。</p>
<p>其实,工具正是帮助我们深入到框架和组件内部,了解其运作方式和原理的重要抓手。所以,我们一定要用好它们。</p>
<p>今天,我继续和你介绍如何使用 JVM 堆转储的工具 MAT 来分析 OOM 问题,以及如何使用全能的故障诊断工具 Arthas 来分析、定位高 CPU 问题。</p>
<h2>使用 MAT 分析 OOM 问题</h2>
<p>对于排查 OOM 问题、分析程序堆内存使用情况,最好的方式就是分析堆转储。</p>
<p>堆转储包含了堆现场全貌和线程栈信息Java 6 Update 14 开始包含)。我们在上一篇加餐中看到,使用 jstat 等工具虽然可以观察堆内存使用情况的变化,但是对程序内到底有多少对象、哪些是大对象还一无所知,也就是说只能看到问题但无法定位问题。而堆转储,就好似得到了病人在某个瞬间的全景核磁影像,可以拿着慢慢分析。</p>
<p>Java 的 OutOfMemoryError 是比较严重的问题,需要分析出根因,所以对生产应用一般都会这样设置 JVM 参数,方便发生 OOM 时进行堆转储:</p>
<pre><code>-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=.
</code></pre>
<p>上一篇加餐中我们提到的 jvisualvm 工具,同样可以进行一键堆转储后,直接打开这个 dump 查看。但是jvisualvm 的堆转储分析功能并不是很强大,只能查看类使用内存的直方图,无法有效跟踪内存使用的引用关系,所以我更推荐使用 Eclipse 的 Memory Analyzer也叫做 MAT做堆转储的分析。你可以点击这个链接下载 MAT。</p>
<p>使用 MAT 分析 OOM 问题,一般可以按照以下思路进行:</p>
<p>通过支配树功能或直方图功能查看消耗内存最大的类型,来分析内存泄露的大概原因;</p>
<p>查看那些消耗内存最大的类型、详细的对象明细列表,以及它们的引用链,来定位内存泄露的具体点;</p>
<p>配合查看对象属性的功能,可以脱离源码看到对象的各种属性的值和依赖关系,帮助我们理清程序逻辑和参数;</p>
<p>辅助使用查看线程栈来看 OOM 问题是否和过多线程有关,甚至可以在线程栈看到 OOM 最后一刻出现异常的线程。</p>
<p>比如,我手头有一个 OOM 后得到的转储文件 java_pid29569.hprof现在要使用 MAT 的直方图、支配树、线程栈、OQL 等功能来分析此次 OOM 的原因。</p>
<p>首先,用 MAT 打开后先进入的是概览信息界面,可以看到整个堆是 437.6MB</p>
<p><img src="assets/63ecdaf5ff7ac431f0d05661855b2e61.png" alt="img" /></p>
<p>那么,这 437.6MB 都是什么对象呢?</p>
<p>如图所示工具栏的第二个按钮可以打开直方图直方图按照类型进行分组列出了每个类有多少个实例以及占用的内存。可以看到char[]字节数组占用内存最多,对象数量也很多,结合第二位的 String 类型对象数量也很多大概可以猜出String 使用 char[]作为实际数据存储)程序可能是被字符串占满了内存,导致 OOM。</p>
<p><img src="assets/0b3ca076b31a2d571a47c64d622b0db9.png" alt="img" /></p>
<p>我们继续分析下,到底是不是这样呢。</p>
<p>在 char[]上点击右键,选择 List objects-&gt;with incoming references就可以列出所有的 char[]实例,以及每个 char[]的整个引用关系链:</p>
<p><img src="assets/f162fb9c6505dc9a8f1ea9900437ada3.png" alt="img" /></p>
<p>随机展开一个 char[],如下图所示:</p>
<p><img src="assets/dd4cb44ad54edee3a51f56a646c5f2ac.png" alt="img" /></p>
<p>接下来,我们按照红色框中的引用链来查看,尝试找到这些大 char[]的来源:</p>
<p>在①处看到,这些 char[]几乎都是 10000 个字符、占用 20000 字节左右char 是 UTF-16每一个字符占用 2 字节);</p>
<p>在②处看到char[]被 String 的 value 字段引用,说明 char[]来自字符串;</p>
<p>在③处看到String 被 ArrayList 的 elementData 字段引用,说明这些字符串加入了一个 ArrayList 中;</p>
<p>在④处看到ArrayList 又被 FooService 的 data 字段引用,这个 ArrayList 整个 RetainedHeap 列的值是 431MB。</p>
<p>Retained Heap深堆代表对象本身和对象关联的对象占用的内存Shallow Heap浅堆代表对象本身占用的内存。比如我们的 FooService 中的 data 这个 ArrayList 对象本身只有 16 字节,但是其所有关联的对象占用了 431MB 内存。这些就可以说明,肯定有哪里在不断向这个 List 中添加 String 数据,导致了 OOM。</p>
<p>左侧的蓝色框可以查看每一个实例的内部属性,图中显示 FooService 有一个 data 属性,类型是 ArrayList。</p>
<p>如果我们希望看到字符串完整内容的话,可以右键选择 Copy-&gt;Value把值复制到剪贴板或保存到文件中</p>
<p><img src="assets/cc1d53eb9570582da415c1aec5cc228f.png" alt="img" /></p>
<p>这里,我们复制出的是 10000 个字符 a下图红色部分可以看到。对于真实案例查看大字符串、大数据的实际内容对于识别数据来源有很大意义</p>
<p><img src="assets/7b3198574113fecdd2a7de8cde8994a0.png" alt="img" /></p>
<p>看到这些,我们已经基本可以还原出真实的代码是怎样的了。</p>
<p>其实,我们之前使用直方图定位 FooService已经走了些弯路。你可以点击工具栏中第三个按钮下图左上角的红框所示进入支配树界面有关支配树的具体概念参考这里。这个界面会按照对象保留的 Retained Heap 倒序直接列出占用内存最大的对象。</p>
<p>可以看到,第一位就是 FooService整个路径是 FooSerice-&gt;ArrayList-&gt;Object[]-&gt;String-&gt;char[](蓝色框部分),一共有 21523 个字符串(绿色方框部分):</p>
<p><img src="assets/7adafa4178a4c72f8621b7eb49ee2757.png" alt="img" /></p>
<p>这样,我们就从内存角度定位到 FooService 是根源了。那么OOM 的时候FooService 是在执行什么逻辑呢?</p>
<p>为解决这个问题,我们可以点击工具栏的第五个按钮(下图红色框所示)。打开线程视图,首先看到的就是一个名为 main 的线程Name 列),展开后果然发现了 FooService</p>
<p><img src="assets/3a2c3d159e1599d906cc428d812cccce.png" alt="img" /></p>
<p>先执行的方法先入栈,所以线程栈最上面是线程当前执行的方法,逐一往下看能看到整个调用路径。因为我们希望了解 FooService.oom() 方法,看看是谁在调用它,它的内部又调用了谁,所以选择以 FooService.oom() 方法(蓝色框)为起点来分析这个调用栈。</p>
<p>往下看整个绿色框部分oom() 方法被 OOMApplication 的 run 方法调用,而这个 run 方法又被 SpringAppliction.callRunner 方法调用。看到参数中的 CommandLineRunner 你应该能想到OOMApplication 其实是实现了 CommandLineRunner 接口,所以是 SpringBoot 应用程序启动后执行的。</p>
<p>以 FooService 为起点往上看,从紫色框中的 Collectors 和 IntPipeline你大概也可以猜出这些字符串是由 Stream 操作产生的。再往上看,可以发现在 StringBuilder 的 append 操作的时候,出现了 OutOfMemoryError 异常(黑色框部分),说明这这个线程抛出了 OOM 异常。</p>
<p>我们看到,整个程序是 Spring Boot 应用程序,那么 FooService 是不是 Spring 的 Bean 呢,又是不是单例呢?如果能分析出这点的话,就更能确认是因为反复调用同一个 FooService 的 oom 方法,然后导致其内部的 ArrayList 不断增加数据的。</p>
<p>点击工具栏的第四个按钮(如下图红框所示),来到 OQL 界面。在这个界面,我们可以使用类似 SQL 的语法,在 dump 中搜索数据(你可以直接在 MAT 帮助菜单搜索 OQL Syntax来查看 OQL 的详细语法)。</p>
<p>比如,输入如下语句搜索 FooService 的实例:</p>
<pre><code>SELECT * FROM org.geekbang.time.commonmistakes.troubleshootingtools.oom.FooService
</code></pre>
<p>可以看到只有一个实例,然后我们通过 List objects 功能搜索引用 FooService 的对象:</p>
<p><img src="assets/1973846815bd9d78f85bef05b499e843.png" alt="img" /></p>
<p>得到以下结果:</p>
<p><img src="assets/07e1216a6cc93bd146535b5809649ea8.png" alt="img" /></p>
<p>可以看到,一共两处引用:</p>
<p>第一处是OOMApplication 使用了 FooService这个我们已经知道了。</p>
<p>第二处是一个 ConcurrentHashMap。可以看到这个 HashMap 是 DefaultListableBeanFactory 的 singletonObjects 字段,可以证实 FooService 是 Spring 容器管理的单例的 Bean。</p>
<p>你甚至可以在这个 HashMap 上点击右键,选择 Java Collections-&gt;Hash Entries 功能,来查看其内容:</p>
<p><img src="assets/ce4020b8f63db060a94fd039314b2d5f.png" alt="img" /></p>
<p>这样就列出了所有的 Bean可以在 Value 上的 Regex 进一步过滤。输入 FooService 后可以看到,类型为 FooService 的 Bean 只有一个,其名字是 fooService</p>
<p><img src="assets/023141fb717704cde9a57c5be6118d1a.png" alt="img" /></p>
<p>到现在为止,我们虽然没看程序代码,但是已经大概知道程序出现 OOM 的原因和大概的调用栈了。我们再贴出程序来对比一下,果然和我们看到得一模一样:</p>
<pre><code>@SpringBootApplication
public class OOMApplication implements CommandLineRunner {
@Autowired
FooService fooService;
public static void main(String[] args) {
SpringApplication.run(OOMApplication.class, args);
}
@Override
public void run(String... args) throws Exception {
//程序启动后不断调用Fooservice.oom()方法
while (true) {
fooService.oom();
}
}
}
@Component
public class FooService {
List&lt;String&gt; data = new ArrayList&lt;&gt;();
public void oom() {
//往同一个ArrayList中不断加入大小为10KB的字符串
data.add(IntStream.rangeClosed(1, 10_000)
.mapToObj(__ -&gt; &quot;a&quot;)
.collect(Collectors.joining(&quot;&quot;)));
}
}
</code></pre>
<p>到这里,我们使用 MAT 工具从对象清单、大对象、线程栈等视角,分析了一个 OOM 程序的堆转储。可以发现,有了堆转储,几乎相当于拿到了应用程序的源码 + 当时那一刻的快照OOM 的问题无从遁形。</p>
<h2>使用 Arthas 分析高 CPU 问题</h2>
<p>Arthas是阿里开源的 Java 诊断工具,相比 JDK 内置的诊断工具,要更人性化,并且功能强大,可以实现许多问题的一键定位,而且可以一键反编译类查看源码,甚至是直接进行生产代码热修复,实现在一个工具内快速定位和修复问题的一站式服务。今天,我就带你使用 Arthas 定位一个 CPU 使用高的问题,系统学习下这个工具的使用。</p>
<p>首先,下载并启动 Arthas</p>
<pre><code>curl -O https://alibaba.github.io/arthas/arthas-boot.jar
java -jar arthas-boot.jar
</code></pre>
<p>启动后,直接找到我们要排查的 JVM 进程,然后可以看到 Arthas 附加进程成功:</p>
<pre><code>[INFO] arthas-boot version: 3.1.7
[INFO] Found existing java process, please choose one and hit RETURN.
\* [1]: 12707
[2]: 30724 org.jetbrains.jps.cmdline.Launcher
[3]: 30725 org.geekbang.time.commonmistakes.troubleshootingtools.highcpu.HighCPUApplication
[4]: 24312 sun.tools.jconsole.JConsole
[5]: 26328 org.jetbrains.jps.cmdline.Launcher
[6]: 24106 org.netbeans.lib.profiler.server.ProfilerServer
3
[INFO] arthas home: /Users/zhuye/.arthas/lib/3.1.7/arthas
[INFO] Try to attach process 30725
[INFO] Attach process 30725 success.
[INFO] arthas-client connect 127.0.0.1 3658
,---. ,------. ,--------.,--. ,--. ,---. ,---.
/ O \ | .--. ''--. .--'| '--' | / O \ ' .-'
| .-. || '--'.' | | | .--. || .-. |`. `-.
| | | || |\ \ | | | | | || | | |.-' |
`--' `--'`--' '--' `--' `--' `--'`--' `--'`-----'
wiki https://alibaba.github.io/arthas
tutorials https://alibaba.github.io/arthas/arthas-tutorials
version 3.1.7
pid 30725
time 2020-01-30 15:48:33
</code></pre>
<p>输出 help 命令,可以看到所有支持的命令列表。今天,我们会用到 dashboard、thread、jad、watch、ognl 命令,来定位这个 HighCPUApplication 进程。你可以通过官方文档,查看这些命令的完整介绍:</p>
<p><img src="assets/47b2abc1c3a8c0670a60c6ed74761873.png" alt="img" /></p>
<p>dashboard 命令用于整体展示进程所有线程、内存、GC 等情况,其输出如下:</p>
<p><img src="assets/ce59c22389ba95104531e46edd9afa4c.png" alt="img" /></p>
<p>可以看到CPU 高并不是 GC 引起的,占用 CPU 较多的线程有 8 个,其中 7 个是 ForkJoinPool.commonPool。学习过加餐 1的话你应该就知道了ForkJoinPool.commonPool 是并行流默认使用的线程池。所以,此次 CPU 高的问题,应该出现在某段并行流的代码上。</p>
<p>接下来,要查看最繁忙的线程在执行的线程栈,可以使用 thread -n 命令。这里,我们查看下最忙的 8 个线程:</p>
<pre><code>thread -n 8
</code></pre>
<p>输出如下:</p>
<p><img src="assets/96cca0708e211ea7f7de413d40c72c00.png" alt="img" /></p>
<p>可以看到,由于这些线程都在处理 MD5 的操作,所以占用了大量 CPU 资源。我们希望分析出代码中哪些逻辑可能会执行这个操作,所以需要从方法栈上找出我们自己写的类,并重点关注。</p>
<p>由于主线程也参与了 ForkJoinPool 的任务处理,因此我们可以通过主线程的栈看到需要重点关注 org.geekbang.time.commonmistakes.troubleshootingtools.highcpu.HighCPUApplication 类的 doTask 方法。</p>
<p>接下来,使用 jad 命令直接对 HighCPUApplication 类反编译:</p>
<pre><code>jad org.geekbang.time.commonmistakes.troubleshootingtools.highcpu.HighCPUApplication
</code></pre>
<p>可以看到,调用路径是 main-&gt;task()-&gt;doTask(),当 doTask 方法接收到的 int 参数等于某个常量的时候,会进行 1 万次的 MD5 操作,这就是耗费 CPU 的来源。那么,这个魔法值到底是多少呢?</p>
<p><img src="assets/4594c58363316d8ff69178d7a341d5e5.png" alt="img" /></p>
<p>你可能想到了,通过 jad 命令继续查看 User 类即可。这里因为是 Demo所以我没有给出很复杂的逻辑。在业务逻辑很复杂的代码中判断逻辑不可能这么直白我们可能还需要分析出 doTask 的“慢”会慢在什么入参上。</p>
<p>这时,我们可以使用 watch 命令来观察方法入参。如下命令,表示需要监控耗时超过 100 毫秒的 doTask 方法的入参,并且输出入参,展开 2 层入参参数:</p>
<pre><code>watch org.geekbang.time.commonmistakes.troubleshootingtools.highcpu.HighCPUApplication doTask '{params}' '#cost&gt;100' -x 2
</code></pre>
<p>可以看到,所有耗时较久的 doTask 方法的入参都是 0意味着 User.ADMN_ID 常量应该是 0。</p>
<p><img src="assets/04e7a4e54c09052ab937f184ab31e03a.png" alt="img" /></p>
<p>最后,我们使用 ognl 命令来运行一个表达式,直接查询 User 类的 ADMIN_ID 静态字段来验证是不是这样,得到的结果果然是 0</p>
<pre><code>[<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="fc9d8e88949d8fbccfcdcdceca">[email&#160;protected]</a>]$ ognl '@org<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="e4ca8381818f86858a83ca908d8981ca878b89898b8a898d9790858f8197ca90968b91868881978c8b8b908d8a83908b8b8897ca8c8d838c879491cab1978196a4">[email&#160;protected]</a>ADMIN_ID'
@Integer[0]
</code></pre>
<p>需要额外说明的是,由于 monitor、trace、watch 等命令是通过字节码增强技术来实现的,会在指定类的方法中插入一些切面来实现数据统计和观测,因此诊断结束要执行 shutdown 来还原类或方法字节码,然后退出 Arthas。</p>
<p>在这个案例中,我们通过 Arthas 工具排查了高 CPU 的问题:</p>
<p>首先,通过 dashboard + thread 命令,基本可以在几秒钟内一键定位问题,找出消耗 CPU 最多的线程和方法栈;</p>
<p>然后,直接 jad 反编译相关代码,来确认根因;</p>
<p>此外,如果调用入参不明确的话,可以使用 watch 观察方法入参,并根据方法执行时间来过滤慢请求的入参。</p>
<p>可见,使用 Arthas 来定位生产问题根本用不着原始代码,也用不着通过增加日志来帮助我们分析入参,一个工具即可完成定位问题、分析问题的全套流程。</p>
<p>对于应用故障分析,除了阿里 Arthas 之外还可以关注去哪儿的Bistoury 工具,其提供了可视化界面,并且可以针对多台机器进行管理,甚至提供了在线断点调试等功能,模拟 IDE 的调试体验。</p>
<h2>重点回顾</h2>
<p>最后,我再和你分享一个案例吧。</p>
<p>有一次开发同学遇到一个 OOM 问题通过查监控、查日志、查调用链路排查了数小时也无法定位问题但我拿到堆转储文件后直接打开支配树图一眼就看到了可疑点。Mybatis 每次查询都查询出了几百万条数据,通过查看线程栈马上可以定位到出现 Bug 的方法名,然后来到代码果然发现因为参数条件为 null 导致了全表查询,整个定位过程不足 5 分钟。</p>
<p>从这个案例我们看到,使用正确的工具、正确的方法来分析问题,几乎可以在几分钟内定位到问题根因。今天,我和你介绍的 MAT 正是分析 Java 堆内存问题的利器,而 Arthas 是快速定位分析 Java 程序生产 Bug 的利器。利用好这两个工具,就可以帮助我们在分钟级定位生产故障。</p>
<h2>思考与讨论</h2>
<p>在介绍线程池的时候,我们模拟了两种可能的 OOM 情况,一种是使用 Executors.newFixedThreadPool一种是使用 Executors.newCachedThreadPool你能回忆起 OOM 的原因吗?假设并不知道 OOM 的原因,拿到了这两种 OOM 后的堆转储,你能否尝试使用 MAT 分析堆转储来定位问题呢?</p>
<p>Arthas 还有一个强大的热修复功能。比如,遇到高 CPU 问题时,我们定位出是管理员用户会执行很多次 MD5消耗大量 CPU 资源。这时我们可以直接在服务器上进行热修复步骤是jad 命令反编译代码 -&gt; 使用文本编辑器(比如 Vim直接修改代码 -&gt; 使用 sc 命令查找代码所在类的 ClassLoader-&gt; 使用 redefine 命令热更新代码。你可以尝试使用这个流程,直接修复程序(注释 doTask 方法中的相关代码)吗?</p>
<p>在平时工作中,你还会使用什么工具来分析排查 Java 应用程序的问题呢?我是朱晔,欢迎在评论区与我留言分享你的想法,也欢迎你把今天的内容分享给你的朋友或同事,一起交流。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/Java 业务开发常见错误 100 例/34 加餐4分析定位Java问题一定要用好这些工具.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/Java 业务开发常见错误 100 例/36 加餐6这15年来我是如何在工作中学习技术和英语的.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js"></script><script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"709970640ff93d60","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>