learn.lianglianglee.com/专栏/Java并发编程实战/24 内存持续上升,我该如何排查问题?.md.html
2022-05-11 18:57:05 +08:00

1281 lines
35 KiB
HTML
Raw Permalink 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>24 内存持续上升,我该如何排查问题?.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 开篇词你为什么需要学习并发编程?.md.html">00 开篇词你为什么需要学习并发编程?.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/01 如何制定性能调优标准?.md.html">01 如何制定性能调优标准?.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/02 如何制定性能调优策略?.md.html">02 如何制定性能调优策略?.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/03 字符串性能优化不容小觑百M内存轻松存储几十G数据.md.html">03 字符串性能优化不容小觑百M内存轻松存储几十G数据.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/04 慎重使用正则表达式.md.html">04 慎重使用正则表达式.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/05 ArrayList还是LinkedList使用不当性能差千倍.md.html">05 ArrayList还是LinkedList使用不当性能差千倍.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/06 Stream如何提高遍历集合效率.md.html">06 Stream如何提高遍历集合效率.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/07 深入浅出HashMap的设计与优化.md.html">07 深入浅出HashMap的设计与优化.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/08 网络通信优化之IO模型如何解决高并发下IO瓶颈.md.html">08 网络通信优化之IO模型如何解决高并发下IO瓶颈.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/09 网络通信优化之序列化避免使用Java序列化.md.html">09 网络通信优化之序列化避免使用Java序列化.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/10 网络通信优化之通信协议如何优化RPC网络通信.md.html">10 网络通信优化之通信协议如何优化RPC网络通信.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/11 答疑课堂深入了解NIO的优化实现原理.md.html">11 答疑课堂深入了解NIO的优化实现原理.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/12 多线程之锁优化深入了解Synchronized同步锁的优化方法.md.html">12 多线程之锁优化深入了解Synchronized同步锁的优化方法.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/13 多线程之锁优化深入了解Lock同步锁的优化方法.md.html">13 多线程之锁优化深入了解Lock同步锁的优化方法.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/14 多线程之锁优化(下):使用乐观锁优化并行操作.md.html">14 多线程之锁优化(下):使用乐观锁优化并行操作.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/15 多线程调优(上):哪些操作导致了上下文切换?.md.html">15 多线程调优(上):哪些操作导致了上下文切换?.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/16 多线程调优(下):如何优化多线程上下文切换?.md.html">16 多线程调优(下):如何优化多线程上下文切换?.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/17 并发容器的使用:识别不同场景下最优容器.md.html">17 并发容器的使用:识别不同场景下最优容器.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/18 如何设置线程池大小?.md.html">18 如何设置线程池大小?.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/19 如何用协程来优化多线程业务?.md.html">19 如何用协程来优化多线程业务?.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/20 磨刀不误砍柴工欲知JVM调优先了解JVM内存模型.md.html">20 磨刀不误砍柴工欲知JVM调优先了解JVM内存模型.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/21 深入JVM即时编译器JIT优化Java编译.md.html">21 深入JVM即时编译器JIT优化Java编译.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/22 如何优化垃圾回收机制?.md.html">22 如何优化垃圾回收机制?.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/23 如何优化JVM内存分配.md.html">23 如何优化JVM内存分配.md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/Java并发编程实战/24 内存持续上升,我该如何排查问题?.md.html">24 内存持续上升,我该如何排查问题?.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/25 答疑课堂:模块四热点问题解答.md.html">25 答疑课堂:模块四热点问题解答.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/26 单例模式:如何创建单一对象优化系统性能?.md.html">26 单例模式:如何创建单一对象优化系统性能?.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/27 原型模式与享元模式:提升系统性能的利器.md.html">27 原型模式与享元模式:提升系统性能的利器.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/28 如何使用设计模式优化并发编程?.md.html">28 如何使用设计模式优化并发编程?.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/29 生产者消费者模式:电商库存设计优化.md.html">29 生产者消费者模式:电商库存设计优化.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/30 装饰器模式:如何优化电商系统中复杂的商品价格策略?.md.html">30 装饰器模式:如何优化电商系统中复杂的商品价格策略?.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/31 答疑课堂:模块五思考题集锦.md.html">31 答疑课堂:模块五思考题集锦.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/32 MySQL调优之SQL语句如何写出高性能SQL语句.md.html">32 MySQL调优之SQL语句如何写出高性能SQL语句.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/33 MySQL调优之事务高并发场景下的数据库事务调优.md.html">33 MySQL调优之事务高并发场景下的数据库事务调优.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/34 MySQL调优之索引索引的失效与优化.md.html">34 MySQL调优之索引索引的失效与优化.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/35 记一次线上SQL死锁事故如何避免死锁.md.html">35 记一次线上SQL死锁事故如何避免死锁.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/36 什么时候需要分表分库?.md.html">36 什么时候需要分表分库?.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/37 电商系统表设计优化案例分析.md.html">37 电商系统表设计优化案例分析.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/38 数据库参数设置优化,失之毫厘差之千里.md.html">38 数据库参数设置优化,失之毫厘差之千里.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/39 答疑课堂MySQL中InnoDB的知识点串讲.md.html">39 答疑课堂MySQL中InnoDB的知识点串讲.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/41 如何设计更优的分布式锁?.md.html">41 如何设计更优的分布式锁?.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/42 电商系统的分布式事务调优.md.html">42 电商系统的分布式事务调优.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/43 如何使用缓存优化系统性能?.md.html">43 如何使用缓存优化系统性能?.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/44 记一次双十一抢购性能瓶颈调优.md.html">44 记一次双十一抢购性能瓶颈调优.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/加餐 什么是数据的强、弱一致性?.md.html">加餐 什么是数据的强、弱一致性?.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/加餐 推荐几款常用的性能测试工具.md.html">加餐 推荐几款常用的性能测试工具.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/答疑课堂:模块三热点问题解答.md.html">答疑课堂:模块三热点问题解答.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/结束语 栉风沐雨,砥砺前行!.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>24 内存持续上升,我该如何排查问题?</h1>
<p>你好,我是刘超。</p>
<p>我想你肯定遇到过内存溢出,或是内存使用率过高的问题。碰到内存持续上升的情况,其实我们很难从业务日志中查看到具体的问题,那么面对多个进程以及大量业务线程,我们该如何精准地找到背后的原因呢?</p>
<h2>常用的监控和诊断内存工具</h2>
<p>工欲善其事,必先利其器。平时排查内存性能瓶颈时,我们往往需要用到一些 Linux 命令行或者 JDK 工具来辅助我们监测系统或者虚拟机内存的使用情况,下面我就来介绍几种好用且常用的工具。</p>
<h3>Linux 命令行工具之 top 命令</h3>
<p>top 命令是我们在 Linux 下最常用的命令之一,它可以实时显示正在执行进程的 CPU 使用率、内存使用率以及系统负载等信息。其中上半部分显示的是系统的统计信息,下半部分显示的是进程的使用率统计信息。</p>
<p><img src="assets/3633095ed54d1ef22fc08310497d6b49.jpg" alt="img" /></p>
<p>除了简单的 top 之外,我们还可以通过 top -Hp pid 查看具体线程使用系统资源情况:</p>
<p><img src="assets/1e4429a9785ae4e6c0884655ee8b5747.jpg" alt="img" /></p>
<h3>Linux 命令行工具之 vmstat 命令</h3>
<p>vmstat 是一款指定采样周期和次数的功能性监测工具,我们可以看到,它不仅可以统计内存的使用情况,还可以观测到 CPU 的使用率、swap 的使用情况。但 vmstat 一般很少用来查看内存的使用情况,而是经常被用来观察进程的上下文切换。</p>
<p><img src="assets/31a79622cdcadda4e9003b075378dc62.jpg" alt="img" /></p>
<ul>
<li>r等待运行的进程数</li>
<li>b处于非中断睡眠状态的进程数</li>
<li>swpd虚拟内存使用情况</li>
<li>free空闲的内存</li>
<li>buff用来作为缓冲的内存数</li>
<li>si从磁盘交换到内存的交换页数量</li>
<li>so从内存交换到磁盘的交换页数量</li>
<li>bi发送到块设备的块数</li>
<li>bo从块设备接收到的块数</li>
<li>in每秒中断数</li>
<li>cs每秒上下文切换次数</li>
<li>us用户 CPU 使用时间;</li>
<li>sy内核 CPU 系统使用时间;</li>
<li>id空闲时间</li>
<li>wa等待 I/O 时间;</li>
<li>st运行虚拟机窃取的时间。</li>
</ul>
<h3>Linux 命令行工具之 pidstat 命令</h3>
<p>pidstat 是 Sysstat 中的一个组件也是一款功能强大的性能监测工具我们可以通过命令yum install sysstat 安装该监控组件。之前的 top 和 vmstat 两个命令都是监测进程的内存、CPU 以及 I/O 使用情况,而 pidstat 命令则是深入到线程级别。</p>
<p>通过 pidstat -help 命令,我们可以查看到有以下几个常用的参数来监测线程的性能:</p>
<p><img src="assets/90d26ef49ad94510062ac3f36727a346.jpg" alt="img" /></p>
<p>常用参数:</p>
<ul>
<li>-u默认的参数显示各个进程的 cpu 使用情况;</li>
<li>-r显示各个进程的内存使用情况</li>
<li>-d显示各个进程的 I/O 使用情况;</li>
<li>-w显示每个进程的上下文切换情况</li>
<li>-p指定进程号</li>
<li>-t显示进程中线程的统计信息。</li>
</ul>
<p>我们可以通过相关命令(例如 ps 或 jps查询到相关进程 ID再运行以下命令来监测该进程的内存使用情况</p>
<p><img src="assets/184df3ee5ab0a920f506b3daa6250a61.jpg" alt="img" /></p>
<p>其中 pidstat 的参数 -p 用于指定进程 ID-r 表示监控内存的使用情况1 表示每秒的意思3 则表示采样次数。</p>
<p>其中显示的几个关键指标的含义是:</p>
<ul>
<li>Minflt/s任务每秒发生的次要错误不需要从磁盘中加载页</li>
<li>Majflt/s任务每秒发生的主要错误需要从磁盘中加载页</li>
<li>VSZ虚拟地址大小虚拟内存使用 KB</li>
<li>RSS常驻集合大小非交换区内存使用 KB。</li>
</ul>
<p>如果我们需要继续查看该进程下的线程内存使用率,则在后面添加 -t 指令即可:</p>
<p><img src="assets/3c9072c659a91b5f83cbc1a112ddcc72.jpg" alt="img" /></p>
<p>我们知道Java 是基于 JVM 上运行的,大部分内存都是在 JVM 的用户内存中创建的,所以除了通过以上 Linux 命令来监控整个服务器内存的使用情况之外,我们更需要知道 JVM 中的内存使用情况。JDK 中就自带了很多命令工具可以监测到 JVM 的内存分配以及使用情况。</p>
<h3>JDK 工具之 jstat 命令</h3>
<p>jstat 可以监测 Java 应用程序的实时运行情况,包括堆内存信息以及垃圾回收信息。我们可以运行 jstat -help 查看一些关键参数信息:</p>
<p><img src="assets/42880a93eb63ae6854a7920e73a751e8.jpg" alt="img" /></p>
<p>再通过 jstat -option 查看 jstat 有哪些操作:</p>
<p><img src="assets/7af697d9cfd6002a49063ab2464d5f7d.jpg" alt="img" /></p>
<ul>
<li>-class显示 ClassLoad 的相关信息;</li>
<li>-compiler显示 JIT 编译的相关信息;</li>
<li>-gc显示和 gc 相关的堆信息;</li>
<li>-gccapacity显示各个代的容量以及使用情况</li>
<li>-gcmetacapacity显示 Metaspace 的大小;</li>
<li>-gcnew显示新生代信息</li>
<li>-gcnewcapacity显示新生代大小和使用情况</li>
<li>-gcold显示老年代和永久代的信息</li>
<li>-gcoldcapacity :显示老年代的大小;</li>
<li>-gcutil显示垃圾收集信息</li>
<li>-gccause显示垃圾回收的相关信息通 -gcutil同时显示最后一次或当前正在发生的垃圾回收的诱因</li>
<li>-printcompilation输出 JIT 编译的方法信息。</li>
</ul>
<p>它的功能比较多,在这里我例举一个常用功能,如何使用 jstat 查看堆内存的使用情况。我们可以用 jstat -gc pid 查看:</p>
<p><img src="assets/e59188982cf5b75243a8c333bfead068.jpg" alt="img" /></p>
<ul>
<li>S0C年轻代中 To Survivor 的容量(单位 KB</li>
<li>S1C年轻代中 From Survivor 的容量(单位 KB</li>
<li>S0U年轻代中 To Survivor 目前已使用空间(单位 KB</li>
<li>S1U年轻代中 From Survivor 目前已使用空间(单位 KB</li>
<li>EC年轻代中 Eden 的容量(单位 KB</li>
<li>EU年轻代中 Eden 目前已使用空间(单位 KB</li>
<li>OCOld 代的容量(单位 KB</li>
<li>OUOld 代目前已使用空间(单位 KB</li>
<li>MCMetaspace 的容量(单位 KB</li>
<li>MUMetaspace 目前已使用空间(单位 KB</li>
<li>YGC从应用程序启动到采样时年轻代中 gc 次数;</li>
<li>YGCT从应用程序启动到采样时年轻代中 gc 所用时间 (s)</li>
<li>FGC从应用程序启动到采样时 old 代(全 gcgc 次数;</li>
<li>FGCT从应用程序启动到采样时 old 代(全 gcgc 所用时间 (s)</li>
<li>GCT从应用程序启动到采样时 gc 用的总时间 (s)。</li>
</ul>
<h3>JDK 工具之 jstack 命令</h3>
<p>这个工具在模块三的[答疑课堂]中介绍过,它是一种线程堆栈分析工具,最常用的功能就是使用 jstack pid 命令查看线程的堆栈信息,通常会结合 top -Hp pid 或 pidstat -p pid -t 一起查看具体线程的状态,也经常用来排查一些死锁的异常。</p>
<p><img src="assets/2869503e8d5460e36b3fd3e1a52a8888.jpg" alt="img" /></p>
<p>每个线程堆栈的信息中,都可以查看到线程 ID、线程的状态wait、sleep、running 等状态)以及是否持有锁等。</p>
<h3>JDK 工具之 jmap 命令</h3>
<p>在[第 23 讲]中我们使用过 jmap 查看堆内存初始化配置信息以及堆内存的使用情况。那么除了这个功能,我们其实还可以使用 jmap 输出堆内存中的对象信息,包括产生了哪些对象,对象数量多少等。</p>
<p>我们可以用 jmap 来查看堆内存初始化配置信息以及堆内存的使用情况:</p>
<p><img src="assets/808870b42f5f6525d79f70fd287a293f.jpg" alt="img" /></p>
<p>我们可以使用 jmap -histo[:live] pid 查看堆内存中的对象数目、大小统计直方图,如果带上 live 则只统计活对象:</p>
<p><img src="assets/74f42fa2b48ceaff869472f6061c1c7b.jpg" alt="img" /></p>
<p>我们可以通过 jmap 命令把堆内存的使用情况 dump 到文件中:</p>
<p><img src="assets/f3c17fd9bb436599fb48cf151ee7ba17.jpg" alt="img" /></p>
<p>我们可以将文件下载下来,使用 <a href="https://www.eclipse.org/mat/">MAT</a> 工具打开文件进行分析:</p>
<p><img src="assets/3cc14844625cebcc1cdb836e5ccbfc43.jpg" alt="img" /></p>
<p>下面我们用一个实战案例来综合使用下刚刚介绍的几种工具,具体操作一下如何分析一个内存泄漏问题。</p>
<h2>实战演练</h2>
<p>我们平时遇到的内存溢出问题一般分为两种,一种是由于大峰值下没有限流,瞬间创建大量对象而导致的内存溢出;另一种则是由于内存泄漏而导致的内存溢出。</p>
<p>使用限流,我们一般就可以解决第一种内存溢出问题,但其实很多时候,内存溢出往往是内存泄漏导致的,这种问题就是程序的 BUG我们需要及时找到问题代码。</p>
<p><strong>下面我模拟了一个内存泄漏导致的内存溢出案例,我们来实践一下。</strong></p>
<p>我们知道ThreadLocal 的作用是提供线程的私有变量,这种变量可以在一个线程的整个生命周期中传递,可以减少一个线程在多个函数或类中创建公共变量来传递信息,避免了复杂度。但在使用时,如果 ThreadLocal 使用不恰当,就可能导致内存泄漏。</p>
<p>这个案例的场景就是 ThreadLocal下面我们创建 100 个线程。运行以下代码,系统一会儿就发送了内存溢出异常:</p>
<pre><code>final static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(100, 100, 1, TimeUnit.MINUTES,
new LinkedBlockingQueue&lt;&gt;());// 创建线程池,通过线程池,保证创建的线程存活
final static ThreadLocal&lt;Byte[]&gt; localVariable = new ThreadLocal&lt;Byte[]&gt;();// 声明本地变量
@RequestMapping(value = &quot;/test0&quot;)
public String test0(HttpServletRequest request) {
poolExecutor.execute(new Runnable() {
public void run() {
Byte[] c = new Byte[4096*1024];
localVariable.set(c);// 为线程添加变量
}
});
return &quot;success&quot;;
}
@RequestMapping(value = &quot;/test1&quot;)
public String test1(HttpServletRequest request) {
List&lt;Byte[]&gt; temp1 = new ArrayList&lt;Byte[]&gt;();
Byte[] b = new Byte[1024*20];
temp1.add(b);// 添加局部变量
return &quot;success&quot;;
}
</code></pre>
<p>在启动应用程序之前,我们可以通过 HeapDumpOnOutOfMemoryError 和 HeapDumpPath 这两个参数开启堆内存异常日志,通过以下命令启动应用程序:</p>
<pre><code>java -jar -Xms1000m -Xmx4000m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof -Xms1g -Xmx1g -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:/tmp/heapTest.log heapTest-0.0.1-SNAPSHOT.jar
</code></pre>
<p>首先,请求 test0 链接 10000 次,之后再请求 test1 链接 10000 次,这个时候我们请求 test1 的接口报异常了。</p>
<p><img src="assets/60ab8d7847a55a9bcf84d17ecd11ebdc.jpg" alt="img" /></p>
<p>通过日志,我们很好分辨这是一个内存溢出异常。我们首先通过 Linux 系统命令查看进程在整个系统中内存的使用率是多少,最简单就是 top 命令了。</p>
<p><img src="assets/d2ad570e1fff2a64a1924c2852f93e37.jpg" alt="img" /></p>
<p>从 top 命令查看进程的内存使用情况,可以发现在机器只有 8G 内存且只分配了 4G 内存给 Java 进程的情况下Java 进程内存使用率已经达到了 55%,再通过 top -Hp pid 查看具体线程占用系统资源情况。</p>
<p><img src="assets/6fdea40b5ff4f2f0744e019c3bef79a7.jpg" alt="img" /></p>
<p>再通过 jstack pid 查看具体线程的堆栈信息,可以发现该线程一直处于 TIMED_WAITING 状态,此时 CPU 使用率和负载并没有出现异常,我们可以排除死锁或 I/O 阻塞的异常问题了。</p>
<p><img src="assets/4bfb58d626f988260e016a2bdf0e8687.jpg" alt="img" /></p>
<p>我们再通过 jmap 查看堆内存的使用情况,可以发现,老年代的使用率几乎快占满了,而且内存一直得不到释放:</p>
<p><img src="assets/feb358259ea8b3ed2b67e868c101d271.jpg" alt="img" /></p>
<p>通过以上堆内存的情况,我们基本可以判断系统发生了内存泄漏。下面我们就需要找到具体是什么对象一直无法回收,什么原因导致了内存泄漏。</p>
<p>我们需要查看具体的堆内存对象,看看是哪个对象占用了堆内存,可以通过 jstat 查看存活对象的数量:</p>
<p><img src="assets/c5b89deb306a2c470e606fa9c49dd0d9.jpg" alt="img" /></p>
<p>Byte 对象占用内存明显异常,说明代码中 Byte 对象存在内存泄漏,我们在启动时,已经设置了 dump 文件,通过 MAT 打开 dump 的内存日志文件,我们可以发现 MAT 已经提示了 byte 内存异常:</p>
<p><img src="assets/4ceb91714afa77b54d1112a0e1f0c863.jpg" alt="img" /></p>
<p>再点击进入到 Histogram 页面,可以查看到对象数量排序,我们可以看到 Byte[] 数组排在了第一位,选中对象后右击选择 with incomming reference 功能,可以查看到具体哪个对象引用了这个对象。</p>
<p><img src="assets/5a651a2f52dfed72712543f7680de091.jpg" alt="img" /></p>
<p>在这里我们就可以很明显地查看到是 ThreadLocal 这块的代码出现了问题。</p>
<p><img src="assets/2bed3871097249d64ccf4c79d68109a2.jpg" alt="img" /></p>
<h2>总结</h2>
<p>在一些比较简单的业务场景下,排查系统性能问题相对来说简单,且容易找到具体原因。但在一些复杂的业务场景下,或是一些开源框架下的源码问题,相对来说就很难排查了,有时候通过工具只能猜测到可能是某些地方出现了问题,而实际排查则要结合源码做具体分析。</p>
<p>可以说没有捷径,排查线上的性能问题本身就不是一件很简单的事情,除了将今天介绍的这些工具融会贯通,还需要我们不断地去累积经验,真正做到性能调优。</p>
<h2>思考题</h2>
<p>除了以上我讲到的那些排查内存性能瓶颈的工具之外,你知道要在代码中对 JVM 的内存进行监控,常用的方法是什么?</p>
<p><strong>问题:</strong></p>
<p>老师是否可以讲下如何避免threadLocal内存泄漏呢</p>
<blockquote>
<p>我们知道ThreadLocal是基于ThreadLocalMap实现的这个Map的Entry继承了WeakReference而Entry对象中的key使用了WeakReference封装也就是说Entry中的key是一个弱引用类型而弱引用类型只能存活在下次GC之前。</p>
<p>如果一个线程调用ThreadLocal的set设置变量当前ThreadLocalMap则新增一条记录但发生一次垃圾回收此时key值被回收而value值依然存在内存中由于当前线程一直存在所以value值将一直被引用。.</p>
<p>这些被垃圾回收掉的key就存在一条引用链的关系一直存在Thread --&gt; ThreadLocalMap--&gt;Entry--&gt;Value这条引用链会导致Entry不会回收Value也不会回收但Entry中的Key却已经被回收的情况造成内存泄漏。</p>
<p>我们只需要在使用完该key值之后通过remove方法remove掉就可以防止内存泄漏了</p>
</blockquote>
<p>老师内存泄露和内存溢出具体有啥区别</p>
<blockquote>
<p>内存泄漏是指不再使用的对象无法得到及时的回收持续占用内存空间从而造成内存空间的浪费。例如我们之前在第3讲中聊到的在Java6中substring方法可能会导致内存泄漏情况发生。当调用substring方法时会调用new string构造函数此时会复用原来字符串的char数组而如果我们仅仅是用substring获取一小段字符而原本string字符串非常大的情况下substring的对象如果一直被引用由于substring的里面的char数组仍然指向原字符串此时string字符串也无法回收从而导致内存泄露。</p>
<p>内存溢出则是发生了OutOfMemoryException内存溢出的情况有很多例如堆内存空间不足栈空间不足以及方法区空间不足都会发生内存溢出异常。</p>
<p>内存泄漏与内存溢出的关系:内存泄漏很容易导致内存溢出,但内存溢出不一定是内存泄漏导致的。</p>
</blockquote>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/Java并发编程实战/23 如何优化JVM内存分配.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/Java并发编程实战/25 答疑课堂:模块四热点问题解答.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":"70997190afae3d60","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>