当Java虚拟机遇上Linux Arena内存池
2017-10-27 00:06 来源:数据和云
<article class="article" id="mp-editor">
作者简介
刘韬,云和恩墨中间件服务交付团队专家
Java开发出身,10年WebLogic相关开发、运维工作经验,熟悉SOA、现代业务系统架构中各层组件,尤其擅长故障处理、性能优化等工作。
故障案例一
系统环境:
RHEL 6.8 64-bit(glibc 2.12)、Sun JDK 6u45 64-bit、WLS 10.3.6
故障现象:
这里引用一下客户当时发邮件时提出的问题描述吧。
下面pid 6287 weblogic进程占用7.6G的物理内存,之前只占用5G内存。我发现只有系统有空余的内存,就会被java给吃掉,为什么内存占用越来越多?
通过jmap -histo:live 6287 查看内存只占用800多MB。
Total 12415620 891690640
image此时,操作系统内存几乎耗尽,而且用了很多Swap交换分区内存,系统性能并不是很好。
故障分析:
刚开始看到这个问题时,首先考虑可能是Native Memory Leak或JDK的Bug,然后看了下那些WebLogic进程的命令行参数:
/data/jdk1.6.0_45/bin/java -server -Xms2560m -Xmx2560m .....
从JDK入手
一看,已经是6u45了,Sun Java SE Public版的最终版本了,找来找去也没找到匹配的Bug(当时还真找到一个看着很像的,JDK-2172773 : JVM sometimes would suddenly consume significant amount of memory,但人家是在6u14b01、5u16rev这两个版本开始,都已经修复了),看来不能从JDK Bug这个方向入手分析了。
从Native Memory Leak入手
但是这个JDK版本也比较尴尬,没有提供Native Memory Trace的功能参数或命令支持(from 7u40版本开始提供),要知道Sun Java SE内部的内存区域很复杂,常见或不常见的很多区域,下面拿JDK 8版本(6版本大同小异)的内存区域为例展示一下:
image(图片来源于SAP公司某技术专家在OOW演讲时的一篇文章)
没有直接的诊断工具的情况下,只能通过一些操作系统命令对这些RES、VIRT内存占用都高的JVM进程的内存使用输出结果做比较,以从中找出一些蛛丝马迹。最终,确定使用pmap这个命令(程序),结果看到如下的输出结果:
image这里发现一个规律,65484 + 52 = 65536 KB, 65420 + 116 = 65536 KB, 65036 + 500 = 65536 KB .... ,进程内有大量的这种64MB大小的连续内存块。
然后,就是需要知道这是什么东东,Google一把,得知anon是Anonymous memory段的缩写。
Anonymous memory is a memory mapping with no file or device backing it.
This is how programs allocate memory from the operating system for use
by things like the stack and heap.
Anonymous memory的使用会使虚拟内存(VIRT)、物理内存(RSS)使用率上升。
而且,找到两篇讲的很清晰的文档了:
JAVA 进程在64位LINUX下占用巨大内存的分析
文章链接 :https://blog.chou.it/2014/03/java-consume-huge-memory-on-64bit-linux
Linux glibc >= 2.11 (RHEL 6) malloc may show excessive virtual memory usage
那这个问题就是Arena内存池数太多,且分配使用的内存较多,不断上涨,导致的WebLogic/Java虚拟机进程RES、VIRT内存使用超高。
这部分内容是有一定的原理的,和故障二里面的理论集中在一起,放在文章下方说明了。
解决办法:
直接想到的解决思路就是限制Arena内存池的个数。考虑到Arena内存池的主要是用来提高glibc内存分配性能的,而且根据Hadoop、Redis等产品的最佳实践建议,尝试设置MALLOC_ARENA_MAX环境变量值为4:
export MALLOC_ARENA_MAX=4
设置完重启WebLogic,然而意外的是,设置完以后Java虚拟机/WebLogic进程RES、VIRT内存使用依然很高:
image后来我查到glibc 2.12版本有几个Arena内存管理的Bug,可能导致参数设置不生效或生效后内存继续往上涨:
Bug 799327 - MALLOC_ARENA_MAX=1 does not work in RHEL 6.2(glibc 2.12)
Bug 20425 - unbalanced and poor utilization of memory in glibc arenas may cause memory bloat and subsequent OOM
Bug 11261 - malloc uses excessive memory for multi-threaded applications
然后,我们考虑到将MALLOC_ARENA_MAX设置为4已经影响了一些Arena内存池管理上的一些性能,要继续使用MALLOC_ARENA_MAX参数,就需要升级glibc的版本,升级完还不确定高版本的glibc与其他包兼容性上有什么影响,毕竟是操作系统底层的包了,所以就直接使用了Google的tcmalloc替代操作系统自带的glibc管理内存。有资料显示,使用tcmalloc以后,Web Server的吞吐量得以提升(先尝试的jemalloc,但是启动后会影响操作系统命令的执行,所以,就用了tcmalloc):
image替换为tcmalloc以后,WebLogic/Java虚拟机进程使用的RES、VIRT内存明显下降到合理值,问题得以解决。
故障案例二
系统环境:
RHEL 6.5 64-bit(glibc 2.12)、Sun JDK 5u22 32-bit、WLS 10.0.2
故障现象:
客户核心系统由于业务的需要,新加了一个节点,沿用原先的相同的操作系统、WebLogic、JDK版本,并且保证所有WebLogic参数配置都是相同的情况下,经常出现Java虚拟机Crash的情况:
file hs_err_pid28384.log :
An unexpected error has been detected by HotSpot Virtual Machine:
SIGSEGV (0xb) at pc=0xf6f8405d, pid=28384, tid=815790960
Java VM: Java HotSpot(TM) Server VM (1.5.0_22-b03 mixed mode)
Problematic frame:
V [libjvm.so+0x24405d]
......
故障分析:
由于这是32-bit的JDK,那就是Native Memory使用过高,超过了寻址空间的限制(4G,默认User Space : Kernel Space = 3 : 1,但在目前的Linux内核版本中,大多数32-bit的进程运行在64-bit操作系统上,几乎都可以用到所有的4G用户空间)。
在做分析之前,为扩大Native Memory空间,我降低了Java Heap Size(-Xms、-Xmx)和 Perm Size(-XX:PermSize、-XX:MaxPermSize)的值,以此来放缓Native Memory上涨的形势(客户不同意使用64-bit JDK)。
接下来,就是分析什么东东造成Native Memory使用持续上涨了。这里,还是用了pmap去看下Native Memory的使用和变化:
image这里也看到了许多984 + 40 = 1024 KB, 1012 + 12 = 1024 KB, 988 + 36 = 1024 KB .... ,进程内有大量的这种1MB大小的连续内存块,而且,通过多次不同时间点的pmap -px输出结果来看,这种1MB大小的内存块还在不断增长。到这里,联想到上面的连续的64MB大小的内存快,迅速找到了当时留的文档链接
Linux glibc >= 2.10 (RHEL 6) malloc may show excessive virtual memory usage
这篇文章里明确提到:
These memory pools are called arenas and the implementation is in arena.c. The first important macro is HEAP_MAX_SIZE which is the maximum size of an arena and it is basically 1MB on 32-bit and 64MB on 64-bit:
HEAP_MAX_SIZE = (2 * DEFAULT_MMAP_THRESHOLD_MAX)
32-bit [DEFAULT_MMAP_THRESHOLD_MAX = (512 * 1024)] = 1,048,576 (1MB)
64-bit [DEFAULT_MMAP_THRESHOLD_MAX = (4 * 1024 * 1024 * sizeof(long))] = 67,108,864 (64MB)
32-bit应用程序Arena的大小最大为1MB,64-bit应用程序最大为64MB,这次终于见识到了。
32-bit应用程序,sizeof(long) = 4 bit,那么这个计算系数就是 2(sizeof(long) == 4 ? 2 : 8)
按照Arena数量最大值的计算公式:
**maximum number of arenas = NUMBER_OF_CPU_CORES * (sizeof(long) == 4 ? 2 : 8) **计算,当前系统80核CPU,那么理论上该Java虚拟机进程最大的Arena值就是 80 * 2 * 1(MB)= 160MB,但实际上,通过pmap观察到这个进程这种1MB大小的匿名内存块都有700多MB,又看了下当前操作系统中glibc的版本是1.12,联想到故障案例一中设置的MALLOC_ARENA_MAX=4在1.12版本都不生效的问题,遇到这种现象就不足为奇了。
目前,RHEL 5.x、6.x、7.3中使用的glibc版本都比较旧(都是2012年及之前的版本了,7.3中使用的glibc版本是2.17,6.x中使用的glibc版本是2.12),可考虑在不是很重要的系统中保持glibc版本始终为最新,然后再观察Arena内存的使用。
image解决办法:
这次直接设置MALLOC_ARENA_MAX=1,只保留main arena,禁用掉per thread arena内存池,使其与RHEL 5.x版本保持一致,问题得以解决,设置完,Java虚拟机不再Crash,pmap监控WebLogic/JVM进程使用的内存增长明显变少、变缓。当然,设置完MALLOC_ARENA_MAX=1,该WebLogic/JVM进程的Native Memory分配、重用、回收等性能多多少少会受到一些影响,也可以使用Google的tcmalloc解决。
总结
通过这两个故障案例可以看出,从glibc 2.11(为应用系统在多核心CPU和多Sockets环境中高伸缩性提供了一个动态内存分配的特性增强)版本开始引入了per thread arena内存池,Native Heap区被打散为sub-pools ,这部分内存池叫做Arena内存池。也就是说,以前只有一个main arena,目前是一个main arena(还是位于Native Heap区) + 多个per thread arena,多个线程之间不再共用一个arena内存区域了,保证每个线程都有一个堆,这样避免内存分配时需要额外的锁来降低性能。main arena主要通过brk/sbrk系统调用去管理,per thread arena主要通过mmap系统调用去分配和管理。
我们来看下线程申请per thread arena内存池的流程:
Unlimited MALLOC_ARENAS_MAX
- Thread asks for an per thread arena
- Thread gets an per thread arena
- Thread fills arena, never frees memory
- Thread asks for an new per thread arena
- ............
- When no more per thread arena will be created, reused_arena function will be called to reuse arena already existed.
我们知道了main arena、per thread arena,那么一个Java虚拟机进程究竟能创建多少个arena、每个arena的大小又是多少那?这部分理论知识比较常见,还不清楚的童鞋,我再啰嗦一下,贴一遍:
一个32位的应用程序进程,最大可创建 2 * CPU总核数个arena内存池(MALLOC_ARENA_MAX),每个arena内存池大小为1MB
一个64位的应用程序进程,最大可创建 8 * CPU总核数个arena内存池(MALLOC_ARENA_MAX),每个arena内存池大小为64MB
理论归理论,glibc 2.12版本(也就是RHEL 6.x中默认自带的)在arena内存分配和管理上,由于不少的Bug或目前我还没完全弄明白的理论的存在,实际上用pmap看到的1MB或64MB的anonymous memory(缩写为anon)并不完全遵循MALLOC_ARENA_MAX个数设置。
除故障案例一中提到的几处bug文章外,还有两个地方的文档显示,MALLOC_ARENA_MAX个数并不是按照设计那样的工作,多线程应用经常遇到RSS、VIRT内存持续升高的情况,尤其是CPU核数多的系统环境。
glibc incorrectly allocated too much memory due to a race condition
within its own malloc routines. This could cause a multi-threaded
application to allocate more memory than was expected.
RHSA-2012:0058 - Security Advisory
文章地址:https://access.redhat.com/errata/RHSA-2012:0058
Linux check M_ARENA_TEST, and M_ARENA_MAX ?
文章地址:https://www.zhihu.com/question/64733296
如果不考虑内存分配的性能,遇到这样的问题时,可使用export MALLOC_ARENA_MAX=1禁用per thread arena,只用main arena,多个线程共用一个arena内存池。如果考虑到性能,可使用tcmalloc或jemalloc替代操作系统自带的glibc管理内存。
上面两个故障案例都是Sun HotSpot JVM的,另外,IBM JDK和Oracle JRockit在RHEL 6.x操作系统环境运行时,也会遇到Arena内存使用上的问题,详见:
IBM JDK虚拟内存使用量过高
Linux glibc >= 2.10 (RHEL 6) malloc may show excessive virtual memory usage(文章开头部分)
Oracle JRockit虚拟机print_memusage输出的other内存使用过高
"Other" Allocation Reported By JRCMD print_memusage Is Too High And Causing An OutOfMemory Issue (Doc ID 2073773.1) (请在My Oracle Support站点搜索文章号)
转型之路,《未来数据库的发展方向与DBA转型之路的解读分析》系列文档
mobike,摩拜物联网架构演进之路ppt返回搜狐,查看更多
责任编辑:
</article>
声明:该文观点仅代表作者本人,搜狐号系信息发布平台,搜狐仅提供信息存储空间服务。
阅读 ()
大家都在看
推荐阅读
网友评论