JVM 调优之 glibc 引发的内存泄露

但是 RSS,确实就是一直在增长,期间也利用 Native Memory Tracking 追踪过 JVM 内部内存的使用情况,具体是这样做的

由于我们开启了 NMT -XX:NativeMemoryTracking=detail

先设置一个基线:

jcmd 1 VM.native_memory baseline

复制代码

然后过一段时间执行:

jcmd 1 VM.native_memory summary.diff

复制代码

对比地看一下统计信息。下图只做示例,具体数字不做参考,因为是我临时执行出来的,数字不对。

真实环境中,增长最多的就是 class 中的 malloc

malloc ? 这是申请内存的函数啊,为什么要申请这么多呢?难道没有释放? 于是想到用 pmap 命令看一下内存映射情况。

Pmap 提供了进程的内存映射,pmap 命令用于显示一个或多个进程的内存状态。其报告进程的地址空间和内存状态信息

执行了以下命令:

pmap -x 1 | sort -n -k3

复制代码

发现了一些端倪:

有一些 64M 左右的内存分配,且越来越多。

glibc

搞不懂了,于是 google 了一下。发现是有这一类问题由于涉及许多底层基础知识,这里就大概解析一下,有兴趣的读者可以查询更多资料了解:

目前大部分服务端程序使用 glibc 提供的 malloc/free 系列函数来进行内存的分配。

Linux 中 malloc 的早期版本是由 Doug Lea 实现的,它有一个严重问题是内存分配只有一个分配区(arena),每次分配内存都要对分配区加锁,分配完释放锁,导致多线程下并发申请释放内存锁的竞争激烈。arena 单词的字面意思是「舞台;竞技场」

于是修修补补又一个版本,你不是多线程锁竞争厉害吗,那我多开几个 arena,锁竞争的情况自然会好转。

Wolfram Gloger 在 Doug Lea 的基础上改进使得 Glibc 的 malloc 可以支持多线程,这就是 ptmalloc2。在只有一个分配区的基础上,增加了非主分配区 (non main arena),主分配区只有一个,非主分配可以有很多个

当调用 malloc 分配内存的时候,会先查看当前线程私有变量中是否已经存在一个分配区 arena。如果存在,则尝试会对这个 arena 加锁如果加锁成功,则会使用这个分配区分配内存

如果加锁失败,说明有其它线程正在使用,则遍历 arena 列表寻找没有加锁的 arena 区域,如果找到则用这个 arena 区域分配内存。

主分配区可以使用 brk 和 mmap 两种方式申请虚拟内存,非主分配区只能 mmap。glibc 每次申请的虚拟内存区块大小是 64MB,glibc 再根据应用需要切割为小块零售。

这就是 linux 进程内存分布中典型的 64M 问题,那有多少个这样的区域呢?在 64 位系统下,这个值等于 8 * number of cores,如果是 4 核,则最多有 32 个 64M 大小的内存区域

glibc 从 2.11 开始对每个线程引入内存池,而我们使用的版本是 2.17,可以通过下面的命令查询版本号

查看 glibc 版本

ldd --version

复制代码

问题解决


通过服务器上一个参数 MALLOC_ARENA_MAX 可以控制最大的 arena 数量

export MALLOC_ARENA_MAX=1

复制代码

由于我们使用的是 docker 容器,于是是在 docker 的启动参数上添加的。

容器重启后发现果然没有了 64M 的内存分配。

but RSS 依然还在增长,虽然这次的增长好像更慢了。于是再次 google 。(事后在其他环境拉长时间观察,其实是有效的,短期内虽然有增长,但后面还会有回落)

查询到可能是因为 glibc 的内存分配策略导致的碎片化内存回收问题,导致看起来像是内存泄露。那有没有更好一点的对碎片化内存的 malloc 库呢?业界常见的有 google 家的 tcmalloc 和 facebook 家的 jemalloc。

tcmalloc

安装

yum install gperftools-libs.x86_64

复制代码

使用 LD_PRELOAD 挂载

export LD_PRELOAD=“/usr/lib64/libtcmalloc.so.4.4.5”

复制代码

注意 java 应用要重启,经过我的测试使用 tcmalloc RSS 内存依然在涨,对我无效。

jemalloc

安装

yum install epel-release -y

yum install jemalloc -y

复制代码

使用 LD_PRELOAD 挂载

export LD_PRELOAD=“/usr/lib64/libjemalloc.so.1”

复制代码

使用 jemalloc 后,RSS 内存呈周期性波动,波动范围约 2 个百分点以内,基本控制住了。

jemalloc 原理

与 tcmalloc 类似,每个线程同样在<32KB 的时候无锁使用线程本地 cache。

Jemalloc 在 64bits 系统上使用下面的 size-class 分类:

  • Small: [8], [16, 32, 48, …, 128], [192, 256, 320, …, 512], [768, 1024, 1280, …, 3840]

  • Large: [4 KiB, 8 KiB, 12 KiB, …, 4072 KiB]

  • Huge: [4 MiB, 8 MiB, 12 MiB, …]

small/large 对象查找 metadata 需要常量时间, huge 对象通过全局红黑树在对数时间内查找。

虚拟内存被逻辑上分割成 chunks(默认是 4MB,1024 个 4k 页),应用线程通过 round-robin 算法在第一次 malloc 的时候分配 arena, 每个 arena 都是相互独立的,维护自己的 chunks, chunk 切割 pages 到 small/large 对象。free() 的内存总是返回到所属的 arena 中,而不管是哪个线程调用 free()。

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,不论你是刚入门Java开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
-1715824399401)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,不论你是刚入门Java开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

<think>我们正在解决Kubernetes环境中容器化应用的Native内存泄漏问题。Native内存泄漏指的是由JVM以外的部分(如通过JNI用的本地库、直接分配的堆外内存等)引起的内存持续增长问题。这类问题在容器环境中尤为棘手,因为常规的JVM监控工具无法直接观测到。 根据引用[2]提到的挑战,我们知道在容器环境中试Native内存泄漏需要特殊工具和方法。下面将系统性地介绍解决方案。 一、诊断工具准备 1. **基础镜像增强**:在构建应用镜像时预装诊断工具 ```dockerfile # 基于标准镜像添加试工具 FROM openjdk:11-jdk RUN apt-get update && apt-get install -y \ procps \ # 提供ps、top等命令 gdb \ # GNU试器 lsof \ # 查看进程打开文件 tcpdump \ # 网络抓包 strace \ # 系统用追踪 valgrind \ # 内存试工具 perf \ # Linux性能分析工具 && rm -rf /var/lib/apt/lists/* ``` 2. **临时试容器**:通过Ephemeral Containers附加到运行中的Pod(k8s v1.23+) ```bash kubectl debug -it <pod-name> --image=nicolaka/netshoot --target=<container-name> ``` 二、内存泄漏定位步骤 1. **监控容器内存变化** 使用`kubectl top pod`观察内存增长趋势: ```bash # 每5秒采集一次,持续监控 watch -n 5 kubectl top pod <pod-name> ``` 2. **进入容器分析进程** 通过`kubectl exec`进入容器: ```bash kubectl exec -it <pod-name> -- /bin/bash ``` 3. **使用工具分析** - **pmap**:查看进程内存映射 ```bash # 获取Java进程PID pid=$(jps -q) pmap -x $pid ``` 重点观察`[anon]`段的大小变化(匿名映射内存) - **valgrind**:检测内存泄漏(性能开销大,仅测试环境使用) ```bash valgrind --tool=memcheck --leak-check=full java -jar app.jar ``` - **tcmalloc/heap profiler**:对使用glibc的应用,通过gperftools分析 ```bash LD_PRELOAD=/usr/lib/libtcmalloc.so HEAPPROFILE=/tmp/heap_profile java -jar app.jar # 生成heap文件后使用pprof分析 pprof --text java /tmp/heap_profile.0001.heap ``` 4. **Native内存追踪(NMT)** 虽然NMT主要跟踪JVM内部Native内存,但可帮助区分: ```bash java -XX:NativeMemoryTracking=detail -XX:+UnlockDiagnosticVMOptions -XX:+PrintNMTStatistics ... # 运行时查看 jcmd <pid> VM.native_memory detail ``` 三、进阶分析方法 1. **eBPF深度追踪** 使用BCC工具集动态追踪内存分配: ```bash # 安装BCC apt install bpfcc-tools # 追踪malloc用 memleak-bpfcc -p <pid> ``` 2. **核心转储分析** 当容器OOM时自动生成核心转储: ```yaml # Pod配置示例 spec: containers: - name: java-app securityContext: capabilities: add: ["SYS_PTRACE"] privileged: false lifecycle: preStop: exec: command: ["/bin/sh", "-c", "jmap -dump:format=b,file=/dumps/heapdump.hprof $1", "jps"] volumeMounts: - name: dumps mountPath: /dumps volumes: - name: dumps emptyDir: {} ``` 分析工具: - `gdb`:分析C/C++核心转储 - `coredumpctl`:系统级转储分析 四、关键指标监控(Prometheus) | 指标 | 作用 | PromQL示例 | |---|---|---| | `process_resident_memory_bytes` | 进程实际物理内存 | `sum(process_resident_memory_bytes{container="java-app"}) by (pod)` | | `container_memory_working_set_bytes` | 容器工作集内存 | `container_memory_working_set_bytes{container="java-app"}` | | `jvm_memory_bytes_used{area="nonheap"}` | JVM非堆内存 | `jvm_memory_bytes_used{area="nonheap", id="CompressedClassSpace"}` | 五、防御性编程建议 1. **JNI代码规范**: - 所有`GetByteArrayElements`必须配对`ReleaseByteArrayElements` - 使用`NewGlobalRef`创建全局引用后必须显式用`DeleteGlobalRef` 2. **堆外内存限制**: ```java // 通过JVM参数限制直接内存 -XX:MaxDirectMemorySize=256m ``` 3. **安全释放资源**: ```java try (ByteBuffer buf = ByteBuffer.allocateDirect(1024)) { // 使用直接缓冲区 } // 自动用Cleaner ``` 六、典型案例分析 案例:Elasticsearch节点频繁OOM 原因:Lucene的MMapDirectory使用大量Native内存未被监控 解决方案: - 整`mmapfs`目录类型为`niofs` - 设置`-XX:MaxDirectMemorySize`限制堆外内存 - 增加`/proc/sys/vm/max_map_count`(节点级) 七、相关问题 1. 如何在不重启Pod的情况下诊断内存泄漏? 2. Kubernetes中如何安全地整`vm.max_map_count`? 3. 有哪些工具可以监控JNI用的性能开销? 4. 如何区分容器OOM是由JVM堆内存还是Native内存引起?</think>### Kubernetes 中试容器化应用 Native 内存泄漏的方法 #### 一、诊断工具链与配置 1. **启用 Native 内存追踪 (NMT)** 在 Java 启动参数中添加: ```bash -XX:NativeMemoryTracking=detail -XX:+UnlockDiagnosticVMOptions ``` 通过 `kubectl exec` 查看内存分布: ```bash kubectl exec <pod-name> -- jcmd 1 VM.native_memory detail ``` 重点关注 `Internal (malloc)` 和 `Arena` 的持续增长[^2]。 2. **eBPF 深度分析工具** 使用临时试容器附加到目标 Pod: ```bash kubectl debug -it <pod-name> --image=nicolaka/netshoot --target=<container-name> ``` 在试容器中使用: ```bash # 安装 BCC 工具 apt install bpfcc-tools # 追踪 malloc 用 memleak-bpfcc -p $(pgrep -n java) # 监控内存页分配 funccount-bpfcc 't:exceptions:page_fault_*' ``` #### 二、泄漏定位四步法 ```mermaid graph TD A[发现内存持续增长] --> B{是否 JVM 堆内存?} B -->|是| C[JVM 堆转储分析] B -->|否| D[检查 NMT 输出] D --> E{malloc/arena 异常?} E -->|是| F[使用 eBPF 追踪分配点] E -->|否| G[检查 JNI 库] F --> H[定位泄漏代码] G --> I[分析 JNI 引用管理] ``` #### 三、关键场景解决方案 1. **JNI 引用泄漏** - 使用 `jvmti` 工具检测未释放的全局引用: ```bash -agentpath:/path/libjvmti.so=printrefs ``` - 确保 JNI 代码遵循模式: ```c jbyteArray array = (*env)->NewByteArray(env, len); /* 使用后必须释放 */ (*env)->DeleteLocalRef(env, array); ``` 2. **堆外缓存泄漏(如 Netty)** 添加监控指标: ```java // 获取 Netty 的 PooledByteBufAllocator 指标 PlatformDependent.usedDirectMemory(); ``` 在 Prometheus 中告警: ```yaml - alert: DirectMemoryOverflow expr: jvm_memory_bytes_used{area="nonheap", id="direct"} / jvm_memory_bytes_max{area="nonheap", id="direct"} > 0.8 for: 5m ``` #### 四、内核级诊断 1. **核心转储分析** 配置 Pod 自动生成 core dump: ```yaml securityContext: capabilities: add: ["SYS_PTRACE"] lifecycle: preStop: exec: command: ["/bin/sh", "-c", "gcore -o /dump/core $1"] volumeMounts: - name: dump-volume mountPath: /dump ``` 2. **分析核心转储** 使用 GDB 检查 native 堆: ```bash gdb /usr/lib/jvm/java-11-openjdk/bin/java core.123 (gdb) malloc_info (gdb) info proc mappings ``` #### 五、Prometheus 监控关键指标 | 指标名称 | PromQL | 告警阈值 | |---------|--------|---------| | Native 内存使用 | `process_resident_memory_bytes{container="<container-name>"}` | 持续增长 >10%/5min | | JVM Direct Buffer | `jvm_memory_bytes_used{id="direct"}` | >80% of `jvm_memory_bytes_max` | | 内存映射文件 | `process_vm_{rss,swap,hwm}_bytes` | 突增 >30% | | Page Faults | `rate(node_vmstat_pgfault[5m])` | >1000/sec | #### 六、防御性编程实践 1. **资源释放检查表**: ```java try (ByteBuffer buf = ByteBuffer.allocateDirect(1024)) { // 使用直接缓冲区 } // 自动用 Cleaner // JNI 代码必须实现 JNIEXPORT void JNICALL Java_clean(JNIEnv* env, jobject obj) { free(global_ptr); // 释放 native 资源 } ``` 2. **压力测试工具** 使用 JMeter 模拟负载时注入内存追踪: ```bash -javaagent:/path/to/jemalloc-profiler.jar=port=8081 ``` 通过 `http://pod-ip:8081/memprofile` 获取实时分配热图。 --- ### 相关问题 1. 如何在不重启 Pod 的情况下获取 Java 应用的 Native 内存快照? 2. Kubernetes 中如何安全地整 `vm.max_map_count` 以解决内存映射泄漏? 3. 有哪些工具可以监控 JNI 用的性能开销和内存分配? 4. 如何区分容器 OOM 是由 JVM 堆内存还是 Native 内存引起? [^1]: Kubernetes 应用更新与动态扩容机制 [^2]: Kubernetes 中 Java 应用性能指南
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值