最新公告
  • 欢迎您光临欧资源网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入我们
  • Java层面使用系统的工具定位和使用内存

    背景

    为了更好的管理项目,我们将群里的一个项目迁移到MDP框架(基于Spring Boot),然后发现系统会频繁报Swap区使用过多的异常。打电话给笔者帮忙查了一下原因,发现配置了4G的堆内内存,但实际使用的物理内存却高达7G,确实不正常。JVM参数配置为“-XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:+AlwaysPreTouch -XX:ReservedCodeCacheSize=128m -XX:InitialCodeCacheSize=128m, -Xss512k -Xmx4g -Xms4g,-XX:+UseG1GC -XX : G1HeapRegionSize=4M”,实际使用的物理内存如下图所示:

    故障排除过程

    1. 使用 Java 级别的工具来定位内存区域(堆上内存、代码区域或使用 unsafe.allocateMemory 和 DirectByteBuffer 请求的堆外内存)

    作者在项目中添加-XX:NativeMemoryTracking=detailJVM参数重启项目,使用命令jcmd pid VM.native_memory detail查看内存分布如下:

    发现该命令显示的committed memory比物理内存小,因为jcmd命令显示的内存包括堆上内存、Code区、unsafe.allocateMemory和DirectByteBuffer请求的内存,但是没有不包括其他本机代码(C 代码)请求的堆外内存。. 所以猜测是这个问题是使用Native Code申请内存造成的。

    为了防止误判,作者使用pmap查看内存分布,发现大量64M地址;这些地址空间不在jcmd命令给出的地址空间内c语言内存泄漏的解决方法,基本可以断定是这64M内存造成的。

    2. 使用系统级工具定位堆外内存

    因为笔者已经基本确定是Native Code引起的,而Java级别的工具不容易排查此类问题,只能使用系统级别的工具来定位问题。

    一、使用gperftools定位问题

    gperftools的使用请参考gperftools。gperftools的监控如下:

    从上图可以看出:malloc申请的内存最多释放3G,之后一直保持在700M-800M。笔者的第一反应是:Native Code里面没有malloc应用,直接mmap/brk应用吗?(gperftools 原理使用动态链接来替换操作系统的默认内存分配器 (glibc)。)

    然后,使用 strace 跟踪系统调用

    由于没有使用gperftools追踪内存,所以直接使用命令“strace -f -e “brk,mmap,munmap” -p pid” 追踪到OS的内存请求,但没有发现可疑的内存请求。strace监控如下图所示:

    接下来,使用 GDB 转储可疑内存

    因为没有使用 strace 跟踪可疑的内存应用程序;所以我想看看记忆中的情况。直接使用命令gdp -pid pid 进入GDB,然后使用命令dump memory mem.bin startAddress endAddressdump memory,其中startAddress 和endAddress 可以从/proc/pid/smaps 中找到。然后使用strings mem.bin查看dump的内容,如下:

    从内容上看,就像解压后的JAR包信息。读取JAR包信息应该是在项目启动的时候,所以在项目启动后使用strace并不是很有用。所以你应该在项目启动时使用 strace,而不是在启动完成之后。

    再次,在项目启动时使用 strace 跟踪系统调用

    项目开始使用strace跟踪系统调用,发现确实申请了很多64M的内存空间。截图如下:

    使用这个mmap申请的地址空间对应pmap中的如下:

    最后使用jstack查看对应的线程

    因为申请内存的线程ID已经显示在strace命令中了。直接使用命令jstack pid查看线程栈,找到对应的线程栈(注意十进制和十六进制的转换)如下:

    这里基本可以看出问题所在:MCC(美团统一配置中心)使用Reflections扫描包,底层使用Spring Boot加载JAR。因为解压JAR使用了Inflater类,所以需要使用堆外内存,然后使用Btrace来追踪这个类。堆栈如下:

    然后查看使用MCC的地方,发现没有配置包扫描路径。默认是扫描所有包。于是修改代码,配置包扫描路径,解决发布后的内存问题。

    3. 为什么堆外内存没有释放?

    虽然问题已经解决了,但是还有几个问题:

    带着疑惑,笔者直接看了一下Spring Boot Loader的源码。发现Spring Boot封装了Java JDK的InflaterInputStream并使用了Inflater,而Inflater本身需要使用堆外内存来解压JAR包。包装类 ZipInflaterInputStream 不会释放 Inflater 持有的堆外内存。所以我以为我找到了原因,立即向 Spring Boot 社区报告了这个 bug。但是经过反馈,作者发现Inflater对象本身实现了finalize方法,并且在这个方法中有一个逻辑可以调用来释放堆外的内存。也就是说,Spring Boot 依赖 GC 来释放堆外内存。

    当我使用jmap查看堆中的对象时,发现基本上没有Inflater对象。因此,当我怀疑 GC 时,不会调用 finalize。带着这样的疑惑,笔者将Spring Boot Loader中打包的Inflater替换为自己打包的Inflaterc语言内存泄漏的解决方法,并在finalize中进行了监控。结果,确实调用了finalize方法。于是查看了Inflater对应的C代码,发现初始化时使用malloc请求内存,结束时也调用free释放内存。

    此时笔者只能怀疑内存空闲时并没有真正释放,于是将Spring Boot打包的InflaterInputStream替换为Java JDK自带的,替换后发现内存问题解决了。

    这时候回去查看gperftools的内存分布,发现在使用Spring Boot的时候,内存使用量一直在增加,突然某个点内存使用量下降了很多(使用量直接从3G到700M左右)。这点应该是GC造成的,应该释放内存,但是在操作系统层面看不到内存变化。它不是释放到操作系统并由内存分配器持有吗?

    继续探索,发现系统默认的内存分配器(glibc2.12版本)和使用gperftools的内存地址分配差别很大。2.5G地址使用smaps发现属于Native Stack。内存地址分布如下:

    至此,基本可以确定内存分配器在耍花招;我搜索了glibc 64M,发现glibc从2.11开始为每个线程引入了一个内存池(64位机器大小为64M内存),原文如下:

    根据文字修改MALLOC_ARENA_MAX环境变量,发现没有效果。查看 tcmalloc(gperftools 使用的内存分配器)也使用内存池方法。

    为了验证是不是内存池的鬼,作者简单写了一个没有内存池的内存分配器。使用命令 gcc zjbmalloc.c -fPIC -shared -o zjbmalloc.so 生成动态库,然后使用 export LD_PRELOAD=zjbmalloc.so 替换 glibc 内存分配器。代码Demo如下:

    #include
    #include
    #include
    #include
    //作者使用的64位机器,sizeof(size_t)也就是sizeof(long) 
    void* malloc ( size_t size )
    {
     long* ptr = mmap( 0, size + sizeof(long), PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, 0, 0 );
     if (ptr == MAP_FAILED) {
     	return NULL;
     }
     *ptr = size; // First 8 bytes contain length.
     return (void*)(&ptr[1]); // Memory that is after length variable
    }
    void *calloc(size_t n, size_t size) {
     void* ptr = malloc(n * size);
     if (ptr == NULL) {
    	return NULL;
     }
     memset(ptr, 0, n * size);
     return ptr;
    }
    void *realloc(void *ptr, size_t size)
    {
     if (size == 0) {
    	free(ptr);
    	return NULL;
     }
     if (ptr == NULL) {
    	return malloc(size);
     }
     long *plen = (long*)ptr;
     plen--; // Reach top of memory
     long len = *plen;
     if (size <= len) {
    	return ptr;
     }
     void* rptr = malloc(size);
     if (rptr == NULL) {
    	free(ptr);
    	return NULL;
     }
     rptr = memcpy(rptr, ptr, len);
     free(ptr);
     return rptr;
    }
    void free (void* ptr )
    {
     if (ptr == NULL) {
    	 return;
     }
     long *plen = (long*)ptr;
     plen--; // Reach top of memory
     long len = *plen; // Read length
     munmap((void*)plen, len + sizeof(long));
    }
    复制代码
    

    通过在自定义分配器中埋点,可以发现程序启动后应用实际申请的堆外内存始终在700M-800M之间,gperftools监控显示内存使用量也在700M-800M左右. 但是从操作系统的角度来看,进程占用的内存变化很大(这里只是监控堆外内存)。

    作者做了一个测试,使用不同的分配器对包进行不同程度的扫描。占用的内存如下:

    为什么自定义malloc申请800M,最终占用的物理内存是1.7G?

    因为自定义内存分配器使用mmap分配内存,mmap分配内存并根据需要四舍五入到整数页,所以存在巨大的空间浪费。通过监控发现最终申请的页数约为536k,实际申请到系统的内存等于512k * 4k(pagesize)=2G。为什么这个数据大于1.7G?

    由于操作系统采用延迟分配的方式,通过mmap向系统申请内存时,系统只返回内存地址,并不分配真正的物理内存。只有在实际使用时,系统才会产生缺页中断,然后分配实际的物理Page。

    总结

    整个内存分配过程如上图所示。MCC 包扫描的默认配置是扫描所有 JAR 包。在扫描包时,Spring Boot 并没有主动释放堆外内存,导致扫描阶段堆外内存使用量持续激增。GC发生时,Spring Boot依靠finalize机制释放堆外内存;但是,出于性能考虑,glibc 并没有真正将内存返回给操作系统,而是将其留在内存池中,导致应用层认为发生了“内存泄漏”。于是修改MCC的配置路径为具体的JAR包,问题就解决了。发表这篇文章的时候,发现Spring Boot最新版本(2.0.5.RELEASE)已经修改,并且ZipInflaterInputStream主动释放堆外内存,不再依赖GC;于是Spring Boot升级到最新版本,也可以解决这个问题。

    站内大部分资源收集于网络,若侵犯了您的合法权益,请联系我们删除!
    欧资源网 » Java层面使用系统的工具定位和使用内存

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    欧资源网
    一个高级程序员模板开发平台

    发表评论