Linux进程内存用量分析之堆内存篇( 三 )


每个子堆的信息从前到后依次为起止地址,共占用大小,使用中的chunk数量和占用大小,空闲的chunk数量和占用大小 。
每个线程都会有自己私有的分配区,在分配空间时会先检查该私有分配区是否被加锁,如果没有则加锁分配,否则遍历循环链表直到找到可用的分配区分配 。极端情况可能遍历完整个环形链表也找不到一个可用分配区,这种情况下就分配一个新的分配区链到循环链表中 。可以认为分配区数量等于线程数量 。于是我们就可以大致猜测这些分配区都对应什么线程 。例如上图中这些只有一个子堆的分配区可能就对应着空间占用较小的查询线程或者一些控制线程,而下图中的这个分配区有多个子堆,可能就对应着需要更多内存的文档线程 。

Linux进程内存用量分析之堆内存篇

文章插图
 
含有多个子堆的分配区
注意看这个线程的子堆大小,明显第一个子堆的总大小是小于64M的,而下边的子堆大小接近64M,这是因为每个分配区下的子堆虽然是链表结构,但是在逻辑上仍然看作是一块地址连续的堆空间,第一个子堆对应着堆顶,而堆的收缩就是通过模拟移动堆顶指针(实际上是由top_chunk管理)实现的 。也就是说只有第一个子堆的堆顶释放了,下边的内存才可以释放,就像下图这样 。当第一个子堆完全释放了,才可以释放第二个和下边的子堆 。这就会造成如果下边的子堆上的内存已经释放,但是堆顶一直不释放,ptmalloc就无法将释放的内存归还给操作系统,也就是会造成内存空洞现象 。
Linux进程内存用量分析之堆内存篇

文章插图
 
堆顶未释放而造成的内存空洞
就上边的例子来看,这个分配区大约有3M左右的内存空洞,比较正常,不是内存膨胀的主要来源 。我们继续往后看,后边列出了所有的mmap段上申请的块
Linux进程内存用量分析之堆内存篇

文章插图
 
进程申请的mmap内存块
注意这里的块有些是调用malloc申请大于mmap_threashold的块时由ptmalloc分配的,而有些则是代码中直接使用mmap申请的空间 。Core_analyzer无法对这两类空间进行区分,就比如继续看接下来打印的块:
Linux进程内存用量分析之堆内存篇

文章插图
 
含有较大的mmap块
还记得上文中提到的n_mmaps和mmaped_mem两个值吗?这两个值就对应了通过ptmalloc申请的mmap段空间,总大小为3G+,那我们至少我们能知道上图中大于3G的块都不是通过malloc接口申请的,而是通过mmap调用申请的 。实际上这些较大的mmap块就是打开的索引文件 。
最后在打印的结尾处有一个汇总,说明了进程占用的总内存大小,以及使用中和空闲的内存大小,从这里也可以判断内存空洞现象是否严重 。在这个例子中,空闲内存占用为775MB,占总内存的2%左右,因为空闲内存可以被复用,所以不算太大 。
Linux进程内存用量分析之堆内存篇

文章插图
 
Ptmalloc层的内存统计
通过上边的方法,我们便可将进程的内存布局一览无余,并且可以判断是否存在大量的内存空洞 。接下来就需要继续深究这些in-use内存都是由哪些对象在占用 。我们可以先做个假设,所有大小相同的chunk极有可能是同一个类的实例,这个假设是接下来定位对象类型的前提,虽然这个假设不一定正确,但是大部分情况下是合理的 。
执行Sort by Type,这个功能按照chunk大小给所有的chunk分类,并且按照占用总大小排序 。如果core文件较大可能需要较长的时间,需要耐心等待,但是会记录结果,也就是说只要跑出来一次,再次使用这个功能就直接输出结果而不用再跑一遍了 。打印出如下图所示:
Linux进程内存用量分析之堆内存篇

文章插图
 
内存块分类排序
三列分别代表块大小,该大小的块个数和该大小块占用的总空间 。按照占用总空间是升序排列 。可以看到较大的几个块数量都很少,前边分析过这部分可能是索引文件,我们来加以验证 。执行Get All Blocks,这个功能会打印出所有的chunk的大小和起始地址,注意会打印在当前目录下的out.txt中 。
Linux进程内存用量分析之堆内存篇

文章插图
 
所有地址块的大小和起始地址
可以看到大小为4292967280的块共有三个,拿其中一个起始地址(十进制表示),使用vertical search,该功能会遍历所有的chunk,向上寻找引用到这个chunk的chunk,一直向上直到找到一个在符号表中有调试信息的对象 。注意该过程可能耗费较长时间,需要耐心等待 。


推荐阅读