mirror of
https://github.com/zhwei820/learn.lianglianglee.com.git
synced 2025-09-26 21:26:41 +08:00
527 lines
34 KiB
HTML
527 lines
34 KiB
HTML
<!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.html">00 开篇词:JVM,一块难啃的骨头</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/深入浅出 Java 虚拟机-完/01 一探究竟:为什么需要 JVM?它处在什么位置?.md.html">01 一探究竟:为什么需要 JVM?它处在什么位置?</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/深入浅出 Java 虚拟机-完/02 大厂面试题:你不得不掌握的 JVM 内存管理.md.html">02 大厂面试题:你不得不掌握的 JVM 内存管理</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/深入浅出 Java 虚拟机-完/03 大厂面试题:从覆盖 JDK 的类开始掌握类的加载机制.md.html">03 大厂面试题:从覆盖 JDK 的类开始掌握类的加载机制</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/深入浅出 Java 虚拟机-完/04 动手实践:从栈帧看字节码是如何在 JVM 中进行流转的.md.html">04 动手实践:从栈帧看字节码是如何在 JVM 中进行流转的</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/深入浅出 Java 虚拟机-完/05 大厂面试题:得心应手应对 OOM 的疑难杂症.md.html">05 大厂面试题:得心应手应对 OOM 的疑难杂症</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/深入浅出 Java 虚拟机-完/06 深入剖析:垃圾回收你真的了解吗?(上).md.html">06 深入剖析:垃圾回收你真的了解吗?(上)</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/深入浅出 Java 虚拟机-完/07 深入剖析:垃圾回收你真的了解吗?(下).md.html">07 深入剖析:垃圾回收你真的了解吗?(下)</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/深入浅出 Java 虚拟机-完/08 大厂面试题:有了 G1 还需要其他垃圾回收器吗?.md.html">08 大厂面试题:有了 G1 还需要其他垃圾回收器吗?</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/深入浅出 Java 虚拟机-完/09 案例实战:亿级流量高并发下如何进行估算和调优.md.html">09 案例实战:亿级流量高并发下如何进行估算和调优</a>
|
||
</li>
|
||
<li>
|
||
<a class="current-tab" href="/专栏/深入浅出 Java 虚拟机-完/10 第09讲:案例实战:面对突如其来的 GC 问题如何下手解决.md.html">10 第09讲:案例实战:面对突如其来的 GC 问题如何下手解决</a>
|
||
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/深入浅出 Java 虚拟机-完/11 第10讲:动手实践:自己模拟 JVM 内存溢出场景.md.html">11 第10讲:动手实践:自己模拟 JVM 内存溢出场景</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/深入浅出 Java 虚拟机-完/12 第11讲:动手实践:遇到问题不要慌,轻松搞定内存泄漏.md.html">12 第11讲:动手实践:遇到问题不要慌,轻松搞定内存泄漏</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/深入浅出 Java 虚拟机-完/13 工具进阶:如何利用 MAT 找到问题发生的根本原因.md.html">13 工具进阶:如何利用 MAT 找到问题发生的根本原因</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/深入浅出 Java 虚拟机-完/14 动手实践:让面试官刮目相看的堆外内存排查.md.html">14 动手实践:让面试官刮目相看的堆外内存排查</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/深入浅出 Java 虚拟机-完/15 预警与解决:深入浅出 GC 监控与调优.md.html">15 预警与解决:深入浅出 GC 监控与调优</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/深入浅出 Java 虚拟机-完/16 案例分析:一个高死亡率的报表系统的优化之路.md.html">16 案例分析:一个高死亡率的报表系统的优化之路</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/深入浅出 Java 虚拟机-完/17 案例分析:分库分表后,我的应用崩溃了.md.html">17 案例分析:分库分表后,我的应用崩溃了</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/深入浅出 Java 虚拟机-完/18 动手实践:从字节码看方法调用的底层实现.md.html">18 动手实践:从字节码看方法调用的底层实现</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/深入浅出 Java 虚拟机-完/19 大厂面试题:不要搞混 JMM 与 JVM.md.html">19 大厂面试题:不要搞混 JMM 与 JVM</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/深入浅出 Java 虚拟机-完/20 动手实践:从字节码看并发编程的底层实现.md.html">20 动手实践:从字节码看并发编程的底层实现</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/深入浅出 Java 虚拟机-完/21 动手实践:不为人熟知的字节码指令.md.html">21 动手实践:不为人熟知的字节码指令</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/深入浅出 Java 虚拟机-完/22 深入剖析:如何使用 Java Agent 技术对字节码进行修改.md.html">22 深入剖析:如何使用 Java Agent 技术对字节码进行修改</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/深入浅出 Java 虚拟机-完/23 动手实践:JIT 参数配置如何影响程序运行?.md.html">23 动手实践:JIT 参数配置如何影响程序运行?</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/深入浅出 Java 虚拟机-完/24 案例分析:大型项目如何进行性能瓶颈调优?.md.html">24 案例分析:大型项目如何进行性能瓶颈调优?</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/深入浅出 Java 虚拟机-完/25 未来:JVM 的历史与展望.md.html">25 未来:JVM 的历史与展望</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/深入浅出 Java 虚拟机-完/26 福利:常见 JVM 面试题补充.md.html">26 福利:常见 JVM 面试题补充</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="/tmp/logs"
|
||
JAVA_OPT_LOG=" -verbose:gc"
|
||
JAVA_OPT_LOG="${JAVA_OPT_LOG} -XX:+PrintGCDetails"
|
||
JAVA_OPT_LOG="${JAVA_OPT_LOG} -XX:+PrintGCDateStamps"
|
||
JAVA_OPT_LOG="${JAVA_OPT_LOG} -XX:+PrintGCApplicationStoppedTime"
|
||
JAVA_OPT_LOG="${JAVA_OPT_LOG} -XX:+PrintTenuringDistribution"
|
||
JAVA_OPT_LOG="${JAVA_OPT_LOG} -Xloggc:${LOG_DIR}/gc_%p.log"
|
||
JAVA_OPT_OOM=" -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${LOG_DIR} -XX:ErrorFile=${LOG_DIR}/hs_error_pid%p.log "
|
||
JAVA_OPT="${JAVA_OPT_LOG} ${JAVA_OPT_OOM}"
|
||
JAVA_OPT="${JAVA_OPT} -XX:-OmitStackTraceInFastThrow"
|
||
</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="/tmp/logs"
|
||
JAVA_OPT_LOG=" -verbose:gc"
|
||
JAVA_OPT_LOG="${JAVA_OPT_LOG} -Xlog:gc,gc+ref=debug,gc+heap=debug,gc+age=trace:file=${LOG_DIR}/gc_%p.log:tags,uptime,time,level"
|
||
JAVA_OPT_LOG="${JAVA_OPT_LOG} -Xlog:safepoint:file=${LOG_DIR}/safepoint_%p.log:tags,uptime,time,level"
|
||
JAVA_OPT_OOM=" -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${LOG_DIR} -XX:ErrorFile=${LOG_DIR}/hs_error_pid%p.log "
|
||
JAVA_OPT="${JAVA_OPT_LOG} ${JAVA_OPT_OOM}"
|
||
JAVA_OPT="${JAVA_OPT} -XX:-OmitStackTraceInFastThrow"
|
||
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 < 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>(4)G1 的时间耗时</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"=| " '{ if($8>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 对应的是 Surivor0,S1 对应的是 Surivor1,O 代表的是 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>1) {print $0 "\t" ($12-pre) "\t" $12*100/$1 ; pre=$12 } else { print $0 "\tGCT_INC\tRate"} }'
|
||
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.html">上一页</a>
|
||
</div>
|
||
<div style="float: right">
|
||
<a href="/专栏/深入浅出 Java 虚拟机-完/11 第10讲:动手实践:自己模拟 JVM 内存溢出场景.md.html">下一页</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>
|