当前位置: 首页 > 网络应用技术

春季靴子引起的“外部记忆泄漏”的调查和经验摘要

时间:2023-03-09 11:51:23 网络应用技术

  为了更好地实现项目的管理,我们将小组中的项目迁移到MDP框架(基于Springboot),然后我们发现该系统将经常报告交换区域的异常使用。

  作者被要求帮助检查原因,并发现4G内存已配置,但是实际使用的物理内存高达7G,这确实是异常的。

  JVM参数配置为:

  实际的物理记忆如下图所示:

  顶部命令显示的内存状态

  检查过程

  封装内存,代码区域或Unsafe.AlocateMemory和DirectByteBuffer应用程序。

  作者添加-xx:nativeMemoryTracking = lidetjvm参数以重新启动项目,并使用命令JCMD pid vm.native_memory细节查看内存分布如下:

  JCMD显示的内存

  显示的命令显示的内存小于物理内存的内存,因为JCMD命令显示的内存包括堆Intranet的内存,unsafe.alocatememory和DirectByteBuffer的内存,但不包括其他本机代码(C代码)外存储器的应用。

  因此,猜测是使用本机代码申请内存引起的问题。

  为了防止错误判断,作者使用PMAP查看内存分布并找到大量64m的地址;这些地址空间不在JCMD命令给出的地址空间中,这基本上确定为这64m内存。

  PMAP显示的内存

  由于作者基本上已经确定它是由本机代码引起的,并且Java级别的工具不方便检查此类问题,因此只能使用系统级工具来找到问题。

  首先,使用gperftools找到问题。Gperftools的使用可能是指gperftools:

  Gperftools的监视如下:

  Gperftools监视

  从上图可以看出,在Malloc应用的存储器释放高达3G之后,它将发布,然后始终保持在700m-800m。

  作者的第一个反应是:没有在本机代码中应用Malloc应用程序,而是直接申请MMAP/BRK应用程序?(Gperftools原理使用动态链接替换操作系统的默认内存分布(GLIBC)。

  然后使用strace跟踪系统调用。由于Gperftools没有跟踪这些内存,因此命令“ strace -f -e” brk,mmap,munmap” -p pid直接使用该命令申请OS的内存请求,但没有发现可疑的内存应用。

  Strace监视如下图:

  Strace监视

  然后,使用GDB去倾倒可疑内存,因为使用Strace没有跟踪可疑的内存应用;因此,我想看看记忆中的情况。

  它是使用命令GDP -PID PID输入GDB,然后使用命令转储记忆内存内存内存内存内存内存内存内存存储器内存存储器内存存储器内存存储器内存存储器内存存储器内存存储器内存mem。其中,可以从/proc/pid/smaps找到jintaddress和endaddress。

  然后使用字符串mem.bin查看转储的内容,如下所示:

  Gperftools监视

  从内容的角度来看,像解剖的JAR软件包信息一样。阅读JAR软件包信息应该是在项目启动时,因此在项目开始后使用strace并不是很大。因此,当项目启动时使用strace而不是启动。

  第三,该项目用于在启动期间跟踪系统调用。该项目开始使用Strace跟踪系统调用,发现应用了许多64m内存空间。

  下面的屏幕截图:

  Strace监视

  应用于MMAP的地址空间如下:PMAP如下:

  PMAP地址空间与Strace应用程序内容相对应

  最后,使用JSTACK检查相应的线程,因为应用程序内存的线程ID已显示在Strace命令中。

  使用命令JSTACK PID检查线程堆栈并找到相应的线程堆栈(请注意10个prace和十六进制转换),如下:

  Strace应用程序空间线程堆栈

  基本上,可以在此处看到问题:MCC(Meituan统一配置中心)使用反射进行包装,Springboot用于在底部加载JAR。

  由于罐子使用膨胀者类,因此需要用于外部内存,然后使用Btrace来跟踪此类。堆栈如下:

  Btrace跟踪堆栈

  然后查看使用MCC的地方,发现包装路径没有配置,默认值是扫描所有袋子。。

  尽管问题已经解决,但有几个问题:

  毫无疑问,作者直接研究了Springboot Loader的源代码。发现Springboot填充了Java JDK的ActistRatoDputStream并使用了充气机,并且充气器本身用于解压缩JAR包装以使用一堆外部内存。

  固定的ZipinflatorInputStream没有释放充气器持有的堆积记忆。因此,我认为找到了原因,并立即喂回Springboot社区。

  但是在反馈之后,作者发现充气机本身实现了最终化方法,该方法具有呼叫和释放外部内存的逻辑。

  当我使用JMAP查看内部对象时,我发现基本上没有充气对象。因此,当我怀疑GC时,我没有调用最终化。

  毫无疑问,作者将充气器包裹在Springboot装载机中,以用他包装的充气机代替它,并将其中一些人最终确定。结果,确实调用了最终确定方法。

  因此,我去看了与Afterater相对应的C代码,发现Malloc应用程序内存已初始化。当结束时,FreeE也称为免费发布内存。

  目前,作者只能怀疑,当Free并没有真正释放内存时,他就取代了Springboot包装的ActricinputStream to wife Java JDK。找到替换后,还解决了内存问题。

  目前,返回查看Gperftools的内存分布,并发现使用Springboot时,内存使用的使用率一直在增加。突然,某个内存点的使用降低了很多(使用量从3G减少到约700m)。

  这一点应由GC引起,并且应该释放内存,但是在操作系统级别看不到内存更改。是否尚未发布到操作系统并持有内存分配器?

  继续探索,发现系统的默认内存分配器(GLIBC 2.12版本)和Gperftools内存地址的内存地址的分布很明显。2.5g地址使用SMAPS发现其属于本机堆栈。

  内存地址分布如下:

  Gperftools显示的内存地址分布

  在这一点上,基本上可以确定内存分配者正在敲击。我搜索了64m的GLIBC,发现GLIBC将内存池引入了2.11(64位计算机的大小为64m内存)。

  原始文本如下:

  GLIB内存池描述

  修改文本中提到的malloc_arena_max环境变量,发现没有效果。查看TCMALLOC(GPERFTOOLS使用的内存分配器)也使用内存池方法。

  为了验证它是内存池的幽灵,作者只需编写一个没有内存池的内存分布式即可。

  使用命令GCC ZJBMALLOC.C -FPIC -SHAARD -O ZJBMALLOC.SO生成动态库,然后用导出LD_PRELOAD = ZJBMALLOC.SO替换GLIBC内存分配器。

  代码演示如下:

  通过将点埋在定制的分布中,您可以发现实际上,在程序启动后应用于实际应用程序的一堆外部内存在700m-800m之间,而Gperftools监视内存使用的显示也约为700m-800m。

  作者进行了测试,并使用不同的分销商进行不同程度的包装。记忆如下:

  内存测试比较

  为什么定制的Malloc适用于800m,最终职业为1.7G?

  由于自定义内存分配使用MMAP分配内存,并且根据需要将内存的MMAP分配带到整个页面上,因此存在巨大的空间浪费。

  通过监视,发现最终应用程序的页数约为536K,然后应用于系统的内存等效于512K * 4K(PAGESIZE)= 2G。

  为什么此数据大于1.7g?因为操作系统通过MMAP应用于系统时采用延迟分布的方法,系统仅返回内存地址,并且不分配真实的物理内存。

  只有在真正使用的情况下,系统才会生成缺乏页面中断,然后分配实际的物理页面。

  总结

  流程图

  整个内存分配的过程如上图所示。MCC软件包的默认配置是扫描所有JAR软件包。当扫描软件包时,Springboot不会主动释放一堆外部内存,从而在扫描阶段,导致扫描阶段,外部记忆职业的数量继续飙升。

  当GC发生时,Springboot依靠最终的机制释放记忆堆。但是,对于性能考虑,GLIBC并没有真正将内存返回操作系统。“内存泄漏”。因此,将MCC的配置路径作为特定的JAR软件包进行修改,解决了问题。

  作者发表本文时,我发现最新版本的Springboot(2.0.5.Release)已修改,在ZipinflatorInputStream中,它积极地释放了一堆外部内存,并且不再取决于GC;因此,Springboot升级到了最新版本。

  PS:防止本文被发现无法找到本文。

  原始:https://juejin.cn/post/7097144187241365535