堆外内存泄露排查经历
优质博文:IT-BLOG-CN
一、问题描述
淘宝后台应用从今年某个时间开始docker oom
的量突然变多,确定为堆外内存泄露。
后面继续按照上一篇对外内存分析方法的进行排查(jemalloc
、pmap
、malloc+pmap/maps+NMT+jstack+gdb
),但都没有定位到问题。至于为什么没有定位到问题,后面会根据问题的特点进行分析。
至此,回到原点。其实也不是原点,最起码已经确定了是堆外内存off-heap
,而不是Native Memory
(JVM
自身所用内存)泄露。所以你看,很多时候问题排查其实是排除法(苦涩的笑)。
二、排查过程
堆外内存off-heap
的泄露,不外乎有以下几个原因:
【1】流没有关闭;
【2】Unsafe.allocateMemory
内存没释放;
【3】jni
内存没有释放;
其中流没有关闭是最常见的,而2
和3
出现的概率是比较低的,所以先排查流没有关闭的可能。
2.1 走寻常路
对于流没有关闭导致泄露的定位,一般来说有以下4
种方式:
【1】看代码;
【2】用jemalloc
分析;
【3】分析堆内存找小尾巴;
2.1.1 看代码
流未关闭的话,一般来说都是因为没有显式地调用close()
方法或没有使用try-with-resource
的方式管理流。比如下面这段代码就存在流未关闭的情形:
像下面的这段代码,流会被try-with-resource
机制去关闭,正常情况下不会出现内存泄露。而存在泄露的应用,恰恰就是用try-with-resource
机制去管理流,所以排除这里的嫌疑。
2.1.2 用jemalloc分析
这里用到jemalloc
主要是利用它的heap dump
以及它的jeprof
命令来分析java
进程的内存分配情况。注意这里的heap
不是jvm
堆内存,而是操作系统视角的内存布局,比如heap
、stack
、BSS
、数据段、代码段,这里不是本文的重点,就不展开描述了……
使用jemalloc
分析内存分配的过程很多文章都有描述,这里也不展开了,结果是通过jeprof
生成的pdf
文件,依然没有发现导致流未关闭的场景,只能作罢。
2.1.3 分析堆内存
通过看代码的方式以及jemalloc
都没法定位到流未关闭的情形,考虑代码走查难免有遗漏,同时应用使用了大量的第三方组件,第三方组件会不会存在流未关闭的可能呢?但很显然,如果去分析第三方组件的代码会累吐血。联想到流没有关闭的情形,一般会在堆内存里面留一些引用的痕迹,于是开始dump java
堆内存。
内存dump
下来后,通过MAT
查找java.lang.ref.Finalizer
、InputStream
、OutputStream
相关的对象,依然一无所获,这时开始怀疑内存的泄露跟流未关闭没有关系。
2.1.4 内部工具分析
公司内部提供了一个跟踪内存分配的工具,通过扩展malloc
方法获取到分配内存的调用线程和内存地址,通过jstack
打印线程栈,结合gdb
、pmap
等方式获取可疑内存段,以定位内存泄露源头。通过这种方式依然没有找到任何线索,同时jstack
的方式会导致应用出现短暂的停顿safepoint
而影响性能,所以这种方式也放弃了。
2.2 走了弯路
在暂时排除了流未关闭的嫌疑后,这时转向分析直接用Unsafe.allocateMemory
分配的内存。有些组件不会基于java.nio.DirectByteBuffer(int cap)
申请堆外内存,而是直接用unsafe.allocateMemory
方法申请内存,这时候MaxDirectMemorySize
是限制不住堆外内存的用量的,当然基于DirectByteBuffer
申请的堆外内存,最终也是基于unsafe.allocateMemory
方法申请内存,所以这里只要分析unsafe.allocateMemory
申请的内存即可。到这里,前面提到的神器async-profiler
就粉墨登场了。
async-profiler
的安装步骤这里就不介绍了,可以自行安装。安装完毕使用以下脚本就可以分析Unsafe_AllocateMemory0
的内存分配情况了。
sudo -u deploy /tmp/async-profiler-2.9-linux-x64/profiler.sh -e Unsafe_AllocateMemory0 -d 1200 -f /tmp/unsafe_alloc-$(pgrep java)-$(date +'%y%m%d%H%M').html $(pgrep java)
这里-e
代表要分析的事件,-d
代表分析的时长,以秒为单位。生成的结果是一张火焰图,你可以下载下来在浏览器上查看哪块用到了Unsafe_AllocateMemory0
来分配内存。
比较悲催的是,通过Unsafe_AllocateMemory0
分配的内存比较少,所以这里的嫌疑也被排除了。所以分析Unsafe_AllocateMemory0
这一步算是走了弯路。
2.3 柳暗花明
前面所有的手段都用尽之后,已经快一个星期过去了。在前面的手段都用尽之后,尝试分析jni
的内存分配情况。其实这时候有点死马当作活马医的味道了。
jni(Java Native Interface)
,简单说就是Java
调用c/c++
写的程序,实现更强的功能。c
写的程序,要分配内存,一般是通过malloc()
方法向操作系统申请内存。在malloc
的实现中,一般分配大块内存 128KB
会使用mmap
分配内存空间。而async-profiler
可以通过分析linux perf_event
中的perf_event_mmap_page
来追踪内存分配情况的。想到这里,便尝试通过下面的命令来追踪系统层面malloc
情况:
sudo -u deploy /tmp/async-profiler-2.9-linux-x64/profiler.sh --loop 1h -e malloc -f /tmp/malloc-$(pgrep java)-%t.html $(pgrep java)
这个命令中的–loop
参数是能够以1
个小时间隔不间断跟踪内存分配情况,如果你想长时间进行问题定位,可以尝试使用一下这个参数,profiler
会每隔1
个小时生成一个html
文件,是不是很方便?
-e malloc
就是告诉async-profiler
去追踪perf_event_mmap_page
的内存分配。
运行了1
个小时后,就得到了下面的这个内存分配火焰图:
从图中可以看出,zstd-jni
这个组件分配了大量的内存。因为在之前我们通过review
代码排查流没有关闭的场景时,是看过这段代码的,但当时没有发现什么问题。但从火焰图中看到分配的内存量,总感觉不对劲。这时候忽然想到,能不能从日志中找到什么蛛丝马迹呢?于是开始扒日志,这时,一个broken pipe
的异常引起了我的注意:
这种broken pipe
的异常其实蛮常见的,尤其在有一方断开连接时,很容易就出现这种异常。但顺着调用栈往下看,顿时眼前一亮,其中有ZstdOutputStream
的调用。流里面的异常那是很容易泄露的,于是进入到ZstdOutputStream.java 178
行看代码,发现了zstd-jni 1.3.x
版本存在的bug
:当ZstdOutputStream
关闭流的时候,会尝试把剩余的数据发送出去。但这时候如果连接已经关闭了,它就咯咯了,导致流关闭不掉,jni
的内存也释放不掉。
这个bug
,在1.4.4-11
版本中就修复了,我们可以看到作者用try-finally
捕获了out.write
的异常,这样不管zstd
依赖的流的状态如何,它最终都会释放自己使用的资源。
定位到问题之后就好办多了,将zstd-jni
的版本升级到1.4.9-5
之后的版本,这个问题就不存在了,下面是修复后RSS
的情况,可以看到RSS
很平稳了:
三、总结
这个case
从开始排查到最终定位到问题,花费了一个星期的时间,成本巨大,回过头看看排查的步骤,貌似也没什么问题,但终究是走了一些弯路:
3.1 忽略了异常信息
如果最开始就重视异常信息的话,那么这个问题可能很早就定位到了。但这个应用自己不是直接责任人,而且在看到broken pipe
的时候犯了经验性错误,没有往影响流关闭的角度想,导致方向错误,浪费了大量的时间。
所以,系统中任何的异常,都要重视起来,避免产生更严重的问题。
3.2 jemalloc失效
jemalloc
在分析内存持续泄露方面比较方便,但对于非稳定复现的场景,如果采样间隔过久,有可能会导致错过问题点。而如果你将采样间隔调短,又会造成生成大量的dump
文件,在用jeprof
生成分析报告的时候,可能会导致too many arguments
的错误而无法生成分析报告。
3.3 内部工具失效
内部工具,能够把可疑的内存段内容用strings
命令查看,某些场景是能够发现蛛丝马迹的,为什么这个case
就不行了呢?这里猜测是因为zstd
对数据做了压缩,用strings
看到的全是乱码,没法发现数据的特征;
综上,问题排查很多时候真的像排雷一样,一个个的去排除。这需要的是耐心和毅力,当你最终定位到问题的时候,那种如释重负的感觉会让自己觉得一切都是值得的。
相关文章:

堆外内存泄露排查经历
优质博文:IT-BLOG-CN 一、问题描述 淘宝后台应用从今年某个时间开始docker oom的量突然变多,确定为堆外内存泄露。 后面继续按照上一篇对外内存分析方法的进行排查(jemalloc、pmap、mallocpmap/mapsNMTjstackgdb),但都没有定位到问题。至于…...

SpringBoot Task
相关文章链接 定时任务工具类(Cron Util)SpringBoot Task 参数详解 Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) Retention(RetentionPolicy.RUNTIME) Documented Repeatable(Schedules.class) public interface Scheduled {String C…...

学习路之压力测试--jmeter安装教程
Jmeter安装 0、先安装jdk:这里是安装jdk-8u211-windows-x64 1、百度网盘上下载 jdk和jmeter 链接: https://pan.baidu.com/s/1qqqaQdNj1ABT1PnH4hfeCw?pwdkwrr 提取码: kwrr 复制这段内容后打开百度网盘手机App,操作更方便哦 官网:Apache JMeter - D…...
大模型部署,运维,测试所需掌握的知识点
python环境部署: python3 -m site --user-base 返回用户级别的Python安装基础目录 sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.8 1 将python3的默认路径/usr/bin/python3替…...

ECharts柱状图-带圆角的堆积柱状图,附视频讲解与代码下载
引言: 在数据可视化的世界里,ECharts凭借其丰富的图表类型和强大的配置能力,成为了众多开发者的首选。今天,我将带大家一起实现一个柱状图图表,通过该图表我们可以直观地展示和分析数据。此外,我还将提供…...
java 并发编程 (2)Thread 类和 Runnable 接口详解
目录 1. Thread 类和 Runnable 接口的设计目的 1.1 为什么有 Thread 类和 Runnable 接口? 2. Thread 类实现的详细分析 2.1 Thread 类的构造方法 2.2 start() 方法的工作原理 2.3 run() 方法 2.4 join() 方法 3. Runnable 接口的实现和作用 3.1 Runnable 接…...

人工智能之数学基础:线性代数在人工智能中的地位
本文重点 从本文开始,我们将开启线性代数的学习,在线性代数中有向量、矩阵,以及各种性质,那么这些数学知识究竟和人工智能有什么关系呢? 重要性 机器学习和深度学习的本质就是训练模型,要想训练模型需要使…...
PostgreSQL WITH 子句:提高查询效率和可读性
PostgreSQL WITH 子句:提高查询效率和可读性 PostgreSQL 是一种功能强大的开源关系数据库管理系统,它以其稳定性、可靠性和高级功能而闻名。在 PostgreSQL 中,WITH 子句(也称为公用表表达式,CTE)是一种非常有用的特性,它允许用户在一个大的查询中创建一个临时的结果集,…...

TransFormer--解码器:前馈网络层、叠加和归一组件
TransFormer--解码器:前馈网络层、叠加和归一组件 解码器的下一个子层是前馈网络层,如下图所示。 解码器的前馈网络层的工作原理与我们在编码器中学到的完全相同 叠加和归一组件 和在编码器部分学到的一样,叠加和归一组件连接子层的输入和输…...

2024亚太杯国际赛C题参考文章50页+完整解题思路+数据处理+最终结果
中国宠物食品行业的发展趋势与汇率情景分析:基于多模型的量化预测与决策分析 一 、 摘要 本文针对宠物产业及相关产业的发展分析问题,采用多种数学建模方法和数据 分析技术,构建了一系列预测和评估模型。从宠物数量预测、全球市场分析、产业 …...

Kafka 分区分配及再平衡策略深度解析与消费者事务和数据积压的简单介绍
Kafka:分布式消息系统的核心原理与安装部署-CSDN博客 自定义 Kafka 脚本 kf-use.sh 的解析与功能与应用示例-CSDN博客 Kafka 生产者全面解析:从基础原理到高级实践-CSDN博客 Kafka 生产者优化与数据处理经验-CSDN博客 Kafka 工作流程解析:…...
useEffect、useCallback、useMemo和memo的区别
前言 在构建现代 React 应用时,性能优化是一个关键考虑因素。随着组件的复杂性增加,合理管理状态和副作用变得尤为重要。React 提供了多个工具来帮助开发者优化组件性能,其中最常用的包括 useEffect、useCallback、useMemo 和 React.memo。这…...

layui树形组件点击树节点后高亮的解决方案
效果显示: 代码 //节点高亮var nodes document.getElementsByClassName("layui-tree-txt");for (var i 0; i < nodes.length; i) {if (nodes[i].innerHTML obj.data.title){nodes[i].style.color "#006BF9";nodes[i].style.fontWeight …...

大语言模型(LLM)安全:十大风险、影响和防御措施
一、什么是大语言模型(LLM)安全? 大语言模型(LLM)安全侧重于保护大型语言模型免受各种威胁,这些威胁可能会损害其功能、完整性和所处理的数据。这涉及实施措施来保护模型本身、它使用的数据以及支持它的基…...

02 —— Webpack 修改入口和出口
概念 | webpack 中文文档 | webpack中文文档 | webpack中文网 修改入口 webpack.config.js (放在项目根目录下) module.exports {//entry设置入口起点的文件路径entry: ./path/to/my/entry/file.js, }; 修改出口 webpack.config.js const path r…...

Go语言进阶依赖管理
1. Go语言进阶 1.1 Goroutine package mainimport ("fmt""time" )func hello(i int) {println("hello goroutine : " fmt.Sprint(i)) }func main() {for i : 0; i < 5; i {go func(j int) { hello(j) }(i) // 启动一个新的 goroutine&…...

集成了高性能ARM Cortex-M0+处理器的一款SimpleLink 2.4 GHz无线模块-RF-BM-2340B1
蓝牙模组 - RF-BM-2340B1是基于美国TI的CC2340R5为核心设计的一款SimpleLink 2.4 GHz 无线模块。支持Bluetooth 5.3 Low Energy、Zigbee 、IEEE 802.15.4g、TI 15.4-Stack (2.4 GHz)及私有协议。集成了高性能ARM Cortex-M0处理器,具有512 KB Flash、32 KB超低泄漏SR…...

ffmpeg本地编译不容易发现的问题 — Error:xxxxx not found!
这里区分电脑CPU架构 本次编译是在Mac笔记本,M1芯片上进行! 前面大致流程:分为两种(1.仅适用,直接下载编译好的本地安装即可;2.使用并查看源码,自己修改编译运行)。这里介绍的是第…...
mybatis——Mapper代理方式
一、原始DAO开发问题 Dao接口实现类方法中存在大量模板方法,设想能否将这些代码提取出来,大大减轻程序员的工作 量。 调用sqlSession的数据库操作方法需要指定statement的id,这里存在硬编码,不利于开发维护。 调用SqlSession方…...

FreeRTOS——消息队列
目录 一、概念及其作用 1.1概念 1.2特点 1.3工作原理 二、相关API 2.1创建队列 2.2任务中写队列 2.3任务中读队列 2.4中断中写队列 2.5中断中读队列 三、实现原理 3.1消息队列控制块 3.2消息队列的创建 3.3消息的发送 3.3.1任务中发送 3.3.2中断中发送 3.4消息的…...
Python爬虫实战:研究MechanicalSoup库相关技术
一、MechanicalSoup 库概述 1.1 库简介 MechanicalSoup 是一个 Python 库,专为自动化交互网站而设计。它结合了 requests 的 HTTP 请求能力和 BeautifulSoup 的 HTML 解析能力,提供了直观的 API,让我们可以像人类用户一样浏览网页、填写表单和提交请求。 1.2 主要功能特点…...
Python|GIF 解析与构建(5):手搓截屏和帧率控制
目录 Python|GIF 解析与构建(5):手搓截屏和帧率控制 一、引言 二、技术实现:手搓截屏模块 2.1 核心原理 2.2 代码解析:ScreenshotData类 2.2.1 截图函数:capture_screen 三、技术实现&…...
内存分配函数malloc kmalloc vmalloc
内存分配函数malloc kmalloc vmalloc malloc实现步骤: 1)请求大小调整:首先,malloc 需要调整用户请求的大小,以适应内部数据结构(例如,可能需要存储额外的元数据)。通常,这包括对齐调整,确保分配的内存地址满足特定硬件要求(如对齐到8字节或16字节边界)。 2)空闲…...

7.4.分块查找
一.分块查找的算法思想: 1.实例: 以上述图片的顺序表为例, 该顺序表的数据元素从整体来看是乱序的,但如果把这些数据元素分成一块一块的小区间, 第一个区间[0,1]索引上的数据元素都是小于等于10的, 第二…...
Ubuntu系统下交叉编译openssl
一、参考资料 OpenSSL&&libcurl库的交叉编译 - hesetone - 博客园 二、准备工作 1. 编译环境 宿主机:Ubuntu 20.04.6 LTSHost:ARM32位交叉编译器:arm-linux-gnueabihf-gcc-11.1.0 2. 设置交叉编译工具链 在交叉编译之前&#x…...
聊一聊接口测试的意义有哪些?
目录 一、隔离性 & 早期测试 二、保障系统集成质量 三、验证业务逻辑的核心层 四、提升测试效率与覆盖度 五、系统稳定性的守护者 六、驱动团队协作与契约管理 七、性能与扩展性的前置评估 八、持续交付的核心支撑 接口测试的意义可以从四个维度展开,首…...

初学 pytest 记录
安装 pip install pytest用例可以是函数也可以是类中的方法 def test_func():print()class TestAdd: # def __init__(self): 在 pytest 中不可以使用__init__方法 # self.cc 12345 pytest.mark.api def test_str(self):res add(1, 2)assert res 12def test_int(self):r…...

【笔记】WSL 中 Rust 安装与测试完整记录
#工作记录 WSL 中 Rust 安装与测试完整记录 1. 运行环境 系统:Ubuntu 24.04 LTS (WSL2)架构:x86_64 (GNU/Linux)Rust 版本:rustc 1.87.0 (2025-05-09)Cargo 版本:cargo 1.87.0 (2025-05-06) 2. 安装 Rust 2.1 使用 Rust 官方安…...

【JVM面试篇】高频八股汇总——类加载和类加载器
目录 1. 讲一下类加载过程? 2. Java创建对象的过程? 3. 对象的生命周期? 4. 类加载器有哪些? 5. 双亲委派模型的作用(好处)? 6. 讲一下类的加载和双亲委派原则? 7. 双亲委派模…...
快刀集(1): 一刀斩断视频片头广告
一刀流:用一个简单脚本,秒杀视频片头广告,还你清爽观影体验。 1. 引子 作为一个爱生活、爱学习、爱收藏高清资源的老码农,平时写代码之余看看电影、补补片,是再正常不过的事。 电影嘛,要沉浸,…...