但是 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开发知识点,不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!
如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
14861760858)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!
如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!