learn.lianglianglee.com/专栏/深入浅出 Java 虚拟机-完/10 第09讲:案例实战:面对突如其来的 GC 问题如何下手解决.md.html
2022-05-11 18:52:13 +08:00

1203 lines
35 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>10 第09讲案例实战面对突如其来的 GC 问题如何下手解决.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 虚拟机-完/00 开篇词JVM一块难啃的骨头.md">00 开篇词JVM一块难啃的骨头.md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/01 一探究竟:为什么需要 JVM它处在什么位置.md">01 一探究竟:为什么需要 JVM它处在什么位置.md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/02 大厂面试题:你不得不掌握的 JVM 内存管理.md">02 大厂面试题:你不得不掌握的 JVM 内存管理.md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/03 大厂面试题:从覆盖 JDK 的类开始掌握类的加载机制.md">03 大厂面试题:从覆盖 JDK 的类开始掌握类的加载机制.md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/04 动手实践:从栈帧看字节码是如何在 JVM 中进行流转的.md">04 动手实践:从栈帧看字节码是如何在 JVM 中进行流转的.md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/05 大厂面试题:得心应手应对 OOM 的疑难杂症.md">05 大厂面试题:得心应手应对 OOM 的疑难杂症.md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/06 深入剖析:垃圾回收你真的了解吗?(上).md">06 深入剖析:垃圾回收你真的了解吗?(上).md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/07 深入剖析:垃圾回收你真的了解吗?(下).md">07 深入剖析:垃圾回收你真的了解吗?(下).md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/08 大厂面试题:有了 G1 还需要其他垃圾回收器吗?.md">08 大厂面试题:有了 G1 还需要其他垃圾回收器吗?.md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/09 案例实战:亿级流量高并发下如何进行估算和调优.md">09 案例实战:亿级流量高并发下如何进行估算和调优.md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/深入浅出 Java 虚拟机-完/10 第09讲案例实战面对突如其来的 GC 问题如何下手解决.md">10 第09讲案例实战面对突如其来的 GC 问题如何下手解决.md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/11 第10讲动手实践自己模拟 JVM 内存溢出场景.md">11 第10讲动手实践自己模拟 JVM 内存溢出场景.md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/12 第11讲动手实践遇到问题不要慌轻松搞定内存泄漏.md">12 第11讲动手实践遇到问题不要慌轻松搞定内存泄漏.md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/13 工具进阶:如何利用 MAT 找到问题发生的根本原因.md">13 工具进阶:如何利用 MAT 找到问题发生的根本原因.md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/14 动手实践:让面试官刮目相看的堆外内存排查.md">14 动手实践:让面试官刮目相看的堆外内存排查.md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/15 预警与解决:深入浅出 GC 监控与调优.md">15 预警与解决:深入浅出 GC 监控与调优.md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/16 案例分析:一个高死亡率的报表系统的优化之路.md">16 案例分析:一个高死亡率的报表系统的优化之路.md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/17 案例分析:分库分表后,我的应用崩溃了.md">17 案例分析:分库分表后,我的应用崩溃了.md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/18 动手实践:从字节码看方法调用的底层实现.md">18 动手实践:从字节码看方法调用的底层实现.md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/19 大厂面试题:不要搞混 JMM 与 JVM.md">19 大厂面试题:不要搞混 JMM 与 JVM.md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/20 动手实践:从字节码看并发编程的底层实现.md">20 动手实践:从字节码看并发编程的底层实现.md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/21 动手实践:不为人熟知的字节码指令.md">21 动手实践:不为人熟知的字节码指令.md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/22 深入剖析:如何使用 Java Agent 技术对字节码进行修改.md">22 深入剖析:如何使用 Java Agent 技术对字节码进行修改.md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/23 动手实践JIT 参数配置如何影响程序运行?.md">23 动手实践JIT 参数配置如何影响程序运行?.md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/24 案例分析:大型项目如何进行性能瓶颈调优?.md">24 案例分析:大型项目如何进行性能瓶颈调优?.md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/25 未来JVM 的历史与展望.md">25 未来JVM 的历史与展望.md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/26 福利:常见 JVM 面试题补充.md">26 福利:常见 JVM 面试题补充.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>10 第09讲案例实战面对突如其来的 GC 问题如何下手解决</h1>
<p>本课时我们主要从一个实战案例入手分析面对突如其来的 GC 问题该如何下手解决。</p>
<p>想要下手解决 GC 问题,我们首先需要掌握下面这三种问题。</p>
<ul>
<li>如何使用 jstat 命令查看 JVM 的 GC 情况?</li>
<li>面对海量 GC 日志参数,如何快速抓住问题根源?</li>
<li>你不得不掌握的日志分析工具。</li>
</ul>
<p>工欲善其事,必先利其器。我们前面课时讲到的优化手段,包括代码优化、扩容、参数优化,甚至我们的估算,都需要一些支撑信息加以判断。</p>
<p><img src="assets/CgpOIF49MRaAYy5hAAAxaKab220724.jpg" alt="img" /></p>
<p>对于 JVM 来说,一种情况是 GC 时间过长,会影响用户的体验,这个时候就需要调整某些 JVM 参数、观察日志。</p>
<p>另外一种情况就比较严重了,发生了 OOM或者操作系统的内存溢出。服务直接宕机我们要寻找背后的原因。</p>
<p>这时GC 日志能够帮我们找到问题的根源。本课时,我们就简要介绍一下如何输出这些日志,以及如何使用这些日志的支撑工具解决问题。</p>
<h2>GC 日志输出</h2>
<p>你可能感受到,最近几年 Java 的版本更新速度是很快的JVM 的参数配置其实变化也很大。就拿 GC 日志这一块来说Java 9 几乎是推翻重来。网络上的一些文章,把这些参数写的乱七八糟,根本不能投入生产。如果你碰到不能被识别的参数,先确认一下自己的 Java 版本。</p>
<p>在事故出现的时候,通常并不是那么温柔。你可能在半夜里就能接到报警电话,这是因为很多定时任务都设定在夜深人静的时候执行。</p>
<p>这个时候,再去看 jstat 已经来不及了,我们需要保留现场。这个便是看门狗的工作,看门狗可以通过设置一些 JVM 参数进行配置。</p>
<p>那在实践中,要怎么用呢?请看下面命令行。</p>
<h3>Java 8</h3>
<p>我们先看一下 JDK8 中的使用。</p>
<pre><code>#!/bin/sh
LOG_DIR=&quot;/tmp/logs&quot;
JAVA_OPT_LOG=&quot; -verbose:gc&quot;
JAVA_OPT_LOG=&quot;${JAVA_OPT_LOG} -XX:+PrintGCDetails&quot;
JAVA_OPT_LOG=&quot;${JAVA_OPT_LOG} -XX:+PrintGCDateStamps&quot;
JAVA_OPT_LOG=&quot;${JAVA_OPT_LOG} -XX:+PrintGCApplicationStoppedTime&quot;
JAVA_OPT_LOG=&quot;${JAVA_OPT_LOG} -XX:+PrintTenuringDistribution&quot;
JAVA_OPT_LOG=&quot;${JAVA_OPT_LOG} -Xloggc:${LOG_DIR}/gc_%p.log&quot;
JAVA_OPT_OOM=&quot; -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${LOG_DIR} -XX:ErrorFile=${LOG_DIR}/hs_error_pid%p.log &quot;
JAVA_OPT=&quot;${JAVA_OPT_LOG} ${JAVA_OPT_OOM}&quot;
JAVA_OPT=&quot;${JAVA_OPT} -XX:-OmitStackTraceInFastThrow&quot;
</code></pre>
<p>合成一行。</p>
<pre><code>-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps
-XX:+PrintGCApplicationStoppedTime -XX:+PrintTenuringDistribution
-Xloggc:/tmp/logs/gc_%p.log -XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/tmp/logs -XX:ErrorFile=/tmp/logs/hs_error_pid%p.log
-XX:-OmitStackTraceInFastThrow
</code></pre>
<p>然后我们来解释一下这些参数:</p>
<table>
<thead>
<tr>
<th>参数</th>
<th>意义</th>
</tr>
</thead>
<tbody>
<tr>
<td>-verbose:gc</td>
<td>打印 GC 日志</td>
</tr>
<tr>
<td>PrintGCDetails</td>
<td>打印详细 GC 日志</td>
</tr>
<tr>
<td>PrintGCDateStamps</td>
<td>系统时间更加可读PrintGCTimeStamps 是 JVM 启动时间</td>
</tr>
<tr>
<td>PrintGCApplicationStoppedTime</td>
<td>打印 STW 时间</td>
</tr>
<tr>
<td>PrintTenuringDistribution</td>
<td>打印对象年龄分布,对调优 MaxTenuringThreshold 参数帮助很大</td>
</tr>
<tr>
<td>loggc</td>
<td>将以上 GC 内容输出到文件中</td>
</tr>
</tbody>
</table>
<p>再来看下 OOM 时的参数:</p>
<table>
<thead>
<tr>
<th>参数</th>
<th>意义</th>
</tr>
</thead>
<tbody>
<tr>
<td>HeapDumpOnOutOfMemoryError</td>
<td>OOM 时 Dump 信息,非常有用</td>
</tr>
<tr>
<td>HeapDumpPath</td>
<td>Dump 文件保存路径</td>
</tr>
<tr>
<td>ErrorFile</td>
<td>错误日志存放路径</td>
</tr>
</tbody>
</table>
<p>注意到我们还设置了一个参数 OmitStackTraceInFastThrow这是 JVM 用来缩简日志输出的。</p>
<p>开启这个参数之后,如果你多次发生了空指针异常,将会打印以下信息。</p>
<pre><code>java.lang.NullPointerException
java.lang.NullPointerException
java.lang.NullPointerException
java.lang.NullPointerException
</code></pre>
<p>在实际生产中,这个参数是默认开启的,这样就导致有时候排查问题非常不方便(很多研发对此无能为力),我们这里把它关闭,但这样它会输出所有的异常堆栈,日志会多很多。</p>
<h3>Java 13</h3>
<p>再看下 JDK 13 中的使用。</p>
<p>从 Java 9 开始,移除了 40 多个 GC 日志相关的参数。具体参见 JEP 158。所以这部分的日志配置有很大的变化。</p>
<p>我们同样看一下它的生成脚本。</p>
<pre><code>#!/bin/sh
LOG_DIR=&quot;/tmp/logs&quot;
JAVA_OPT_LOG=&quot; -verbose:gc&quot;
JAVA_OPT_LOG=&quot;${JAVA_OPT_LOG} -Xlog:gc,gc+ref=debug,gc+heap=debug,gc+age=trace:file=${LOG_DIR}/gc_%p.log:tags,uptime,time,level&quot;
JAVA_OPT_LOG=&quot;${JAVA_OPT_LOG} -Xlog:safepoint:file=${LOG_DIR}/safepoint_%p.log:tags,uptime,time,level&quot;
JAVA_OPT_OOM=&quot; -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${LOG_DIR} -XX:ErrorFile=${LOG_DIR}/hs_error_pid%p.log &quot;
JAVA_OPT=&quot;${JAVA_OPT_LOG} ${JAVA_OPT_OOM}&quot;
JAVA_OPT=&quot;${JAVA_OPT} -XX:-OmitStackTraceInFastThrow&quot;
echo $JAVA_OPT
</code></pre>
<p>合成一行展示。</p>
<pre><code>-verbose:gc -Xlog:gc,gc+ref=debug,gc+heap=debug,gc+age=trace:file
=/tmp/logs/gc_%p.log:tags,uptime,time,level -Xlog:safepoint:file=/tmp
/logs/safepoint_%p.log:tags,uptime,time,level -XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/tmp/logs -XX:ErrorFile=/tmp/logs/hs_error_pid%p.log
-XX:-OmitStackTraceInFastThrow
</code></pre>
<p>可以看到 GC 日志的打印方式,已经完全不一样,但是比以前的日志参数规整了许多。</p>
<p>我们除了输出 GC 日志,还输出了 safepoint 的日志。这个日志对我们分析问题也很重要,那什么叫 safepoint 呢?</p>
<p>safepoint 是 JVM 中非常重要的一个概念,指的是可以安全地暂停线程的点。</p>
<p>当发生 GC 时,用户线程必须全部停下来,才可以进行垃圾回收,这个状态我们可以认为 JVM 是安全的safe整个堆的状态是稳定的。</p>
<p><img src="assets/Cgq2xl49MRaAZaFKAABeSE1hLTg491.jpg" alt="img" /></p>
<p>如果在 GC 前,有线程迟迟进入不了 safepoint那么整个 JVM 都在等待这个阻塞的线程,会造成了整体 GC 的时间变长。</p>
<p>所以呢,并不是只有 GC 会挂起 JVM进入 safepoint 的过程也会。这个概念,如果你有兴趣可以自行深挖一下,一般是不会出问题的。</p>
<p>如果面试官问起你在项目中都使用了哪些打印 GC 日志的参数,上面这些信息肯定是不很好记忆。你需要进行以下总结。比如:</p>
<p>“我一般在项目中输出详细的 GC 日志,并加上可读性强的 GC 日志的时间戳。特别情况下我还会追加一些反映对象晋升情况和堆详细信息的日志用来排查问题。另外OOM 时自动 Dump 堆栈,我一般也会进行配置”。</p>
<h2>GC 日志的意义</h2>
<p>我们首先看一段日志,然后简要看一下各个阶段的意义。</p>
<p><img src="assets/CgpOIF49MRaAUrVfAAFWihJ6jwk874.jpg" alt="img" /></p>
<ul>
<li>1 表示 GC 发生的时间,一般使用可读的方式打印;</li>
<li>2 表示日志表明是 G1 的“转移暂停: 混合模式”,停顿了约 223ms</li>
<li>3 表明由 8 个 Worker 线程并行执行,消耗了 214ms</li>
<li>4 表示 Diff 越小越好,说明每个工作线程的速度都很均匀;</li>
<li>5 表示外部根区扫描外部根是堆外区。JNI 引用JVM 系统目录Classloaders 等;</li>
<li>6 表示更新 RSet 的时间信息;</li>
<li>7 表示该任务主要是对 CSet 中存活对象进行转移(复制);</li>
<li>8 表示花在 GC 之外的工作线程的时间;</li>
<li>9 表示并行阶段的 GC 总时间;</li>
<li>10 表示其他清理活动;</li>
<li>11表示收集结果统计</li>
<li>12 表示时间花费统计。</li>
</ul>
<p>可以看到 GC 日志描述了垃圾回收器过程中的几乎每一个阶段。但即使你了解了这些数值的意义,在分析问题时,也会感到吃力,我们一般使用图形化的分析工具进行分析。</p>
<p>尤其注意的是最后一行日志,需要详细描述。可以看到 G C花费的时间竟然有 3 个数值。这个数值你可能在多个地方见过。如果你手头有 Linux 机器,可以执行以下命令:</p>
<p>time ls /</p>
<p><img src="assets/Cgq2xl49MRaAWpU-AAAUzFIkYlk730.jpg" alt="img" /></p>
<p>可以看到一段命令的执行,同样有三种纬度的时间统计。接下来解释一下这三个字段的意思。</p>
<ul>
<li>real 实际花费的时间,指的是从开始到结束所花费的时间。比如进程在等待 I/O 完成,这个阻塞时间也会被计算在内;</li>
<li>user 指的是进程在用户态User Mode所花费的时间只统计本进程所使用的时间注意是指多核</li>
<li>sys 指的是进程在核心态Kernel Mode花费的 CPU 时间量,指的是内核中的系统调用所花费的时间,只统计本进程所使用的时间。</li>
</ul>
<p>在上面的 GC 日志中real &lt; user + sys因为我们使用了多核进行垃圾收集所以实际发生的时间比 (user + sys) 少很多。在多核机器上,这很常见。</p>
<p>[Times: user=1.64 sys=0.00, real=0.23 secs]</p>
<p>下面是一个串行垃圾收集器收集的 GC 时间的示例。由于串行垃圾收集器始终仅使用一个线程,因此实际使用的时间等于用户和系统时间的总和:</p>
<p>[Times: user=0.29 sys=0.00, real=0.29 secs]</p>
<p>那我们统计 GC 以哪个时间为准呢?一般来说,用户只关心系统停顿了多少秒,对实际的影响时间非常感兴趣。至于背后是怎么实现的,是多核还是单核,是用户态还是内核态,它们都不关心。所以我们直接使用 real 字段。</p>
<h2>GC日志可视化</h2>
<p>肉眼可见的这些日志信息,让人非常头晕,尤其是日志文件特别大的时候。所幸现在有一些在线分析平台,可以帮助我们分析这个过程。下面我们拿常用的 gceasy 来看一下。</p>
<p>以下是一个使用了 G1 垃圾回收器,堆内存为 6GB 的服务,运行 5 天的 GC 日志。</p>
<p>1堆信息</p>
<p><img src="assets/CgpOIF49MRaACSuzAABDthdVxTk570.jpg" alt="img" /></p>
<p>我们可以从图中看到堆的使用情况。</p>
<p>2关键信息</p>
<p>从图中我们可以看到一些性能的关键信息。</p>
<p>吞吐量98.6%(一般超过 95% 就 ok 了);</p>
<p>最大延迟230ms平均延迟42.8ms</p>
<p>延迟要看服务的接受程度,比如 SLA 定义 50ms 返回数据,上面的最大延迟就会有一点问题。本服务接近 99% 的停顿在 100ms 以下,可以说算是非常优秀了。</p>
<p><img src="assets/Cgq2xl49MReAI98WAABRPqHhDjE672.jpg" alt="img" /></p>
<p>你在看这些信息的时候,一定要结合宿主服务器的监控去看。比如 GC 发生期间CPU 会突然出现尖锋,就证明 GC 对 CPU 资源使用的有点多。但多数情况下,如果吞吐量和延迟在可接受的范围内,这些对 CPU 的超额使用是可以忍受的。</p>
<p>3交互式图表</p>
<p><img src="assets/CgpOIF49MReAcKfGAABakc1dRtA053.jpg" alt="img" /></p>
<p>可以对有问题的区域进行放大查看,图中表示垃圾回收后的空间释放,可以看到效果是比较好的。</p>
<p>4G1 的时间耗时</p>
<p><img src="assets/Cgq2xl49MReAf_DdAACM8OnUC_I541.jpg" alt="img" /></p>
<p>如图展示了 GC 的每个阶段花费的时间。可以看到平均耗时最长的阶段,就是 Concurrent Mark 阶段但由于是并发的影响并不大。随着时间的推移YoungGC 竟然达到了 136485 次。运行 5 天,光花在 GC 上的时间就有 2 个多小时,还是比较可观的。</p>
<p>5其他</p>
<p><img src="assets/CgpOIF49MReAL2goAAB6BiE3imA217.jpg" alt="img" /></p>
<p>如图所示,整个 JVM 创建了 100 多 T 的数据,其中有 2.4TB 被 promoted 到老年代。</p>
<p>另外,还有一些 safepoint 的信息等,你可以自行探索。</p>
<p>那到底什么样的数据才是有问题的呢gceasy 提供了几个案例。比如下面这个就是停顿时间明显超长的 GC 问题。</p>
<p><img src="assets/Cgq2xl49MReAQIQ_AABZPnGfj9s030.jpg" alt="img" /></p>
<p>下面这个是典型的内存泄漏。</p>
<p><img src="assets/CgpOIF49MReAQHswAABgRmmPU5k549.jpg" alt="img" /></p>
<p>上面这些问题都是非常明显的。但大多数情况下,问题是偶发的。从基本的衡量指标,就能考量到整体的服务水准。如果这些都没有问题,就要看曲线的尖峰。</p>
<p>一般来说,任何不平滑的曲线,都是值得怀疑的,那就需要看一下当时的业务情况具体是什么样子的。是用户请求突增引起的,还是执行了一个批量的定时任务,再或者查询了大批量的数据,这要和一些服务的监控一起看才能定位出根本问题。</p>
<p>只靠 GC 来定位问题是比较困难的,我们只需要知道它有问题就可以了。后面,会介绍更多的支持工具进行问题的排解。</p>
<p>为了方便你调试使用,我在 GitHub 上上传了两个 GC 日志。其中 gc01.tar.gz 就是我们现在正在看的,解压后有 200 多兆;另外一个 gc02.tar.gz 是一个堆空间为 1GB 的日志文件,你也可以下载下来体验一下。</p>
<blockquote>
<p>GitHub 地址:</p>
</blockquote>
<blockquote>
<p><a href="https://gitee.com/xjjdog/jvm-lagou-res">https://gitee.com/xjjdog/jvm-lagou-res</a></p>
</blockquote>
<p>另外GCViewer 这个工具也是常用的,可以下载到本地,以 jar 包的方式运行。</p>
<p>在一些极端情况下,也可以使用脚本简单过滤一下。比如下面行命令,就是筛选停顿超过 100ms 的 GC 日志和它的行数G1</p>
<pre><code># grep -n real gc.log | awk -F&quot;=| &quot; '{ if($8&gt;0.1){ print }}'
1975: [Times: user=2.03 sys=0.93, real=0.75 secs]
2915: [Times: user=1.82 sys=0.65, real=0.64 secs]
16492: [Times: user=0.47 sys=0.89, real=0.35 secs]
16627: [Times: user=0.71 sys=0.76, real=0.39 secs]
16801: [Times: user=1.41 sys=0.48, real=0.49 secs]
17045: [Times: user=0.35 sys=1.25, real=0.41 secs]
</code></pre>
<h2>jstat</h2>
<p>上面的可视化工具,必须经历导出、上传、分析三个阶段,这种速度太慢了。有没有可以实时看堆内存的工具?</p>
<p>你可能会第一时间想到 jstat 命令。第一次接触这个命令,我也是很迷惑的,主要是输出的字段太多,不了解什么意义。</p>
<p>但其实了解我们在前几节课时所讲到内存区域划分和堆划分之后,再看这些名词就非常简单了。</p>
<p><img src="assets/Cgq2xl49MReAUqsJAABSeq9EGOY088.jpg" alt="img" /></p>
<p>我们拿 -gcutil 参数来说明一下。</p>
<p>jstat -gcutil $pid 1000</p>
<p>只需要提供一个 Java 进程的 ID然后指定间隔时间毫秒就 OK 了。</p>
<pre><code>S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 0.00 72.03 0.35 54.12 55.72 11122 16.019 0 0.000 16.019
0.00 0.00 95.39 0.35 54.12 55.72 11123 16.024 0 0.000 16.024
0.00 0.00 25.32 0.35 54.12 55.72 11125 16.025 0 0.000 16.025
0.00 0.00 37.00 0.35 54.12 55.72 11126 16.028 0 0.000 16.028
0.00 0.00 60.35 0.35 54.12 55.72 11127 16.028 0 0.000 16.028
</code></pre>
<p>可以看到E 其实是 Eden 的缩写S0 对应的是 Surivor0S1 对应的是 Surivor1O 代表的是 Old而 M 代表的是 Metaspace。</p>
<p>YGC 代表的是年轻代的回收次数YGC T对应的是年轻代的回收耗时。那么 FGC 肯定代表的是 Full GC 的次数。</p>
<p>你在看日志的时候,一定要注意其中的规律。-gcutil 位置的参数可以有很多种。我们最常用的有 gc、gcutil、gccause、gcnew 等,其他的了解一下即可。</p>
<ul>
<li>gc: 显示和 GC 相关的 <strong>堆信息</strong></li>
<li>gcutil: 显示 <strong>垃圾回收信息</strong></li>
<li>gccause: 显示<strong>垃圾回收</strong> 的相关信息(同 -gcutil同时显示 <strong>最后一次</strong><strong>当前</strong> 正在发生的垃圾回收的 <strong>诱因</strong></li>
<li>gcnew: 显示 <strong>新生代</strong> 信息;</li>
<li>gccapacity: 显示 <strong>各个代</strong><strong>容量</strong> 以及 <strong>使用情况</strong></li>
<li>gcmetacapacity: 显示 <strong>元空间</strong> metaspace 的大小;</li>
<li>gcnewcapacity: 显示 <strong>新生代大小</strong><strong>使用情况</strong></li>
<li>gcold: 显示 <strong>老年代</strong><strong>永久代</strong> 的信息;</li>
<li>gcoldcapacity: 显示 <strong>老年代</strong> 的大小;</li>
<li>printcompilation: 输出 JIT <strong>编译</strong> 的方法信息;</li>
<li>class: 显示 <strong>类加载</strong> ClassLoader 的相关信息;</li>
<li>compiler: 显示 JIT <strong>编译</strong> 的相关信息;</li>
</ul>
<p>如果 GC 问题特别明显,通过 jstat 可以快速发现。我们在启动命令行中加上参数 -t可以输出从程序启动到现在的时间。如果 FGC 和启动时间的比值太大就证明系统的吞吐量比较小GC 花费的时间太多了。另外,如果老年代在 Full GC 之后,没有明显的下降,那可能内存已经达到了瓶颈,或者有内存泄漏问题。</p>
<p>下面这行命令,就追加了 GC 时间的增量和 GC 时间比率两列。</p>
<pre><code>jstat -gcutil -t 90542 1000 | awk 'BEGIN{pre=0}{if(NR&gt;1) {print $0 &quot;\t&quot; ($12-pre) &quot;\t&quot; $12*100/$1 ; pre=$12 } else { print $0 &quot;\tGCT_INC\tRate&quot;} }'
Timestamp S0 S1 E O M CCS YGC YGCT FGC FGCT GCT GCT_INC Rate
18.7 0.00 100.00 6.02 1.45 84.81 76.09 1 0.002 0 0.000 0.002 0.002 0.0106952
19.7 0.00 100.00 6.02 1.45 84.81 76.09 1 0.002 0 0.000 0.002 0 0.0101523
</code></pre>
<h2>GC 日志也会搞鬼</h2>
<p>顺便给你介绍一个实际发生的故障。</p>
<p>你知道 ElasticSearch 的速度是非常快的,我们为了压榨它的性能,对磁盘的读写几乎是全速的。它在后台做了很多 Merge 动作,将小块的索引合并成大块的索引。还有 TransLog 等预写动作,都是 I/O 大户。</p>
<p>使用 iostat -x 1 可以看到具体的 I/O 使用状况。</p>
<p>问题是,我们有一套 ES 集群,在访问高峰时,有多个 ES 节点发生了严重的 STW 问题。有的节点竟停顿了足足有 7~8 秒。</p>
<p>[Times: user=0.42 sys=0.03, real=7.62 secs]</p>
<p>从日志可以看到在 GC 时用户态只停顿了 420ms但真实的停顿时间却有 7.62 秒。</p>
<p>盘点一下资源,唯一超额利用的可能就是 I/O 资源了(%util 保持在 90 以上GC 可能在等待 I/O。</p>
<p>通过搜索,发现已经有人出现过这个问题,这里直接说原因和结果。</p>
<p>原因就在于,写 GC 日志的 write 动作,是统计在 STW 的时间里的。在我们的场景中,由于 ES 的索引数据,和 GC 日志放在了一个磁盘GC 时写日志的动作,就和写数据文件的动作产生了资源争用。</p>
<p><img src="assets/CgpOIF49MReAa5QEAAAoiL0gjR4706.jpg" alt="img" /></p>
<p>解决方式也是比较容易的,把 ES 的日志文件,单独放在一块普通 HDD 磁盘上就可以了。</p>
<h2>小结</h2>
<p>本课时,我们主要介绍了比较重要的 GC 日志,以及怎么输出它,并简要的介绍了一段 G1 日志的意义。对于这些日志的信息,能够帮助我们理解整个 GC 的过程,专门去记忆它投入和产出并不成正比,可以多看下 G1 垃圾回收器原理方面的东西。</p>
<p>接下来我们介绍了几个图形化分析 GC 的工具,这也是现在主流的使用方式,因为动辄几百 MB 的 GC 日志,是无法肉眼分辨的。如果机器的 I/O 问题很突出,就要考虑把 GC 日志移动到单独的磁盘。</p>
<p>我们尤其介绍了在线分析工具 gceasy你也可以下载 gcviewer 的 jar 包本地体验一下。</p>
<p>最后我们看了一个命令行的 GC 回收工具 jstat它的格式比较规整可以重定向到一个日志文件里后续使用 sed、awk 等工具进行分析。关于相关的两个命令,可以参考我以前写的两篇文章。</p>
<p><a href="https://mp.weixin.qq.com/s/wP9_wvoTARRrlszsOmvMgQ">《Linux生产环境上最常用的一套“Sed“技巧》</a></p>
<p><a href="https://mp.weixin.qq.com/s/aRy3QlMUpSNOKf2pyN6Uuw">《Linux生产环境上最常用的一套“AWK“技巧》</a></p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/深入浅出 Java 虚拟机-完/09 案例实战:亿级流量高并发下如何进行估算和调优.md">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/深入浅出 Java 虚拟机-完/11 第10讲动手实践自己模拟 JVM 内存溢出场景.md">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"70997a5ab92d3cfa","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>