最近在做 MySQL 版本升级时( 5.1->5.5 ) , 发现了 mysqld 疑似“内存泄露”现象,但通过 valgrind 等工具检测后,并没发现类似的问题。因此,需要深入学习 Linux 的虚拟内存管理方面的内容来解释这个现象。 Linux 的虚拟内存管理有几个关键概念:1. 每个进程有独立的虚拟地址空间,进程访问的虚拟地址并不是真正的物理地址2. 虚拟地址可通过每个进程上页表与物理地址进行映射,获得真正物理地址3. 如果虚拟地址对应物理地址不在物理内存中,则产生缺页中断,真正分配物理地址,同时更新进程的页表;如果此时物理内存已耗尽,则根据内存替换算法淘汰部分页面至物理磁盘中。基于以上认识,这篇文章通过本人以前对虚拟内存管理的疑惑由浅入深整理了以下十个问题,并通过例子和系统命令尝试进行解答。
1.Linux 虚拟地址空间如何分布? 32 位和 64 位有何不同?2.malloc 是如何分配内存的?3.malloc 分配多大的内存,就占用多大的物理内存空间吗?如何查看进程虚拟地址空间的使用情况?
5.free 的内存真的释放了吗(还给 OS ) ?程序代码中 malloc 的内存都有相应的 free ,就不会出现内存泄露了吗?
既然堆内内存不能直接释放,为什么不全部使用 mmap 来分配?
如何查看进程的缺页中断信息?
如何查看堆内内存的碎片情况?
除了 glibc 的 malloc/free ,还有其他第三方实现吗?
1.Linux 虚拟地址空间如何分布? 32 位和 64位有何不同?
Linux 使用虚拟地址空间,大大增加了进程的寻址空间,由低地址到高地址分别为:
只读段:该部分空间只能读,不可写,包括代码段、 rodata 段( C 常量字符串和 #define定义的常量)数据段:保存全局变量、静态变量的空间堆 :就是平时所说的动态内存, malloc/new 大部分都来源于此。其中堆顶的位置可通过函数 brk 和 sbrk 进行动态调整。文件映射区域 :如动态库、共享内存等映射物理空间的内存,一般是 mmap 函数所分配的虚拟地址空间。栈:用于维护函数调用的上下文空间,一般为 8M ,可通过 ulimit –s 查看。内核虚拟空间:用户代码不可见的内存区域,由内核管理。下图是 32 位系统典型的虚拟地址空间分布(来自《深入理解计算机系统》)。32位系统典型虚拟地址空间分布32 位系统有 4G 的地址空间,其中 0x08048000~0xbfffffff 是用户空间,0xc0000000~0xffffffff 是内核空间,包括内核代码和数据、与进程相关的数据结构(如页表、内核栈)等。另外, %esp 执行栈顶,往低地址方向变化; brk/sbrk 函数控制堆顶往高地址方向变化。可通过以下代码验证进程的地址空间分布,其中 sbrk(0) 函数用于返回栈顶指针。 1: #include <stdlib.h> 2: 3: #include <stdio.h> 4: 5: #include <string.h> 6: 7: #include <unistd.h> 8: 9: 10: int global_num = 0; 11: 12: char global_str_arr [65536] = {'a'}; 13: 14: 15: int main(int argc, char** argv) 16: 17: { 18: 19: char* heap_var = NULL; 20: 21: int local_var = 0; 22: 23: 24: printf("Address of function main 0x%lxn", main); 25: 26: printf("Address of global_num 0x%lxn", &global_num); 27: 28: printf("Address of global_str_arr 0x%lx ~ 0x%lxn", &global_str_arr[0], &global_str_arr[65535]); 29: 30: printf("Top of stack is 0x%lxn", &local_var); 31: 32: printf("Top of heap is 0x%lxn", sbrk(0)); 33: 34: heap_var = malloc(sizeof(char) 127 1024); 35: 36: printf("Address of heap_var is 0x%lxn", heap_var); 37: 38: printf("Top of heap after malloc is 0x%lxn", sbrk(0)); 39: 40: free(heap_var); 41: 42: heap_var = NULL; 43: 44: printf("Top of heap after free is 0x%lxn", sbrk(0)); 45: 46: return 1; 47: }32 位系统的结果如下,与上图的划分保持一致,并且栈顶指针在 mallloc 和 free 一个 127K的存储空间时都发生了变化(增大和缩小)。Address of function main 0x8048474Address of global_num 0x8059904
Address of global_str_arr 0x8049900 ~ 0x80598ff
Top of stack is 0xbfd0886c
Top of heap is 0x805a000
Address of heap_var is 0x805a008
Top of heap after malloc is 0x809a000
Top of heap after free is 0x807b000
但是, 64 位系统结果怎样呢? 64 位系统是否拥有 2^64 的地址空间吗?64 位系统运行结果如下:Address of function main 0x400594Address of global_num 0x610b90
Address of global_str_arr 0x600b80 ~ 0x610b7f
Top of stack is 0x7fff2e9e4994
Top of heap is 0x8f5000
Address of heap_var is 0x8f5010
Top of heap after malloc is 0x935000
Top of heap after free is 0x916000
从结果知,与上图的分布并不一致。而事实上, 64 位系统的虚拟地址空间划分发生了改变:1. 地址空间大小不是 2^32 ,也不是 2^64 ,而一般是 2^48 。因为并不需要 2^64 这么大的寻址空间,过大空间只会导致资源的浪费。 64 位 Linux 一般使用 48 位来表示虚拟地址空间, 40 位表示物理地址,这可通过 /proc/cpuinfo 来查看address sizes : 40 bits physical, 48 bits virtual2. 其中, 0x0000000000000000~0x00007fffffffffff 表示用户空间,0xFFFF800000000000~ 0xFFFFFFFFFFFFFFFF 表示内核空间,共提供 256TB(2^48) 的寻址空间。这两个区间的特点是,第 47 位与 48~63 位相同,若这些位为 0 表示用户空间,否则表示内核空间。3. 用户空间由低地址到高地址仍然是只读段、数据段、堆、文件映射区域和栈2.malloc 是如何分配内存的?malloc 是 glibc 中内存分配函数,也是最常用的动态内存分配函数,其内存必须通过 free进行释放,否则导致内存泄露。关于 malloc 获得虚存空间的实现,与 glibc 的版本有关,但大体逻辑是:1. 若分配内存小于 128k ,调用 sbrk() ,将堆顶指针向高地址移动,获得新的虚存空间。2. 若分配内存大于 128k ,调用 mmap() ,在文件映射区域中分配匿名虚存空间。3. 这里讨论的是简单情况,如果涉及并发可能会复杂一些,不过先不讨论。其中 sbrk 就是修改栈顶指针位置,而 mmap 可用于生成文件的映射以及匿名页面的内存,这里指的是匿名页面。而这个 128k ,是 glibc 的默认配置,可通过函数 mallopt 来设置,可通过以下例子说明。include <stdlib.h>
include <stdio.h>
include <string.h>
include <unistd.h>
include <sys/mman.h>
include <malloc.h>
void print_info(
char* var_name, char* var_ptr, size_t size_in_kb
)
{
printf("Address of %s(%luk) 0x%lx, now heap top is 0x%lx\n", var_name, size_in_kb, var_ptr, sbrk(0));
}
int main(int argc, char** argv)
{
char *heap_var1, *heap_var2, *heap_var3 ; char *mmap_var1, *mmap_var2, *mmap_var3 ; char *maybe_mmap_var;
printf("Orginal heap top is 0x%lx\n", sbrk(0));
heap_var1 = malloc(32*1024); print_info("heap_var1", heap_var1, 32); heap_var2 = malloc(64*1024); print_info("heap_var2", heap_var2, 64); heap_var3 = malloc(127*1024); print_info("heap_var3", heap_var3, 127);
printf("\n"); maybe_mmap_var = malloc(128*1024); print_info("maybe_mmap_var", maybe_mmap_var, 128);
//mmap mmap_var1 = malloc(128*1024); print_info("mmap_var1", mmap_var1, 128);
// set M_MMAP_THRESHOLD to 64k mallopt(M_MMAP_THRESHOLD, 64*1024); printf("set M_MMAP_THRESHOLD to 64k\n");
mmap_var2 = malloc(64*1024); print_info("mmap_var2", mmap_var2, 64); mmap_var3 = malloc(127*1024); print_info("mmap_var3", mmap_var3, 127);
return 1;
}
这个例子很简单,通过 malloc 申请多个不同大小的动态内存,同时通过接口 print_info 打印变量大小和地址等相关信息,其中 sbrk(0) 可返回堆顶指针位置。另外,粗体部分是将 MMAP分配的临界点由 128k 转为 64k ,再打印变量地址的不同。下面是 Linux 64 位机器的执行结果(后文所有例子都是通过 64 位机器上的测试结果)。Orginal heap top is 0x17da000Address of heap_var1(32k) 0x17da010, now heap top is 0x1803000Address of heap_var2(64k) 0x17e2020, now heap top is 0x1803000Address of heap_var3(127k) 0x17f2030, now heap top is 0x1832000Address of maybe_mmap_var(128k) 0x1811c40, now heap top is 0x1832000Address of mmap_var1(128k) 0x7f4a0b1f2010, now heap top is 0x1832000set M_MMAP_THRESHOLD to 64kAddress of mmap_var2(64k) 0x7f4a0b1e1010, now heap top is 0x1832000Address of mmap_var3(127k) 0x7f4a0b1c1010, now heap top is 0x1832000由以上结果知:1. 申请 128k 以内的内存( heap_var1/2/3 ,其内存地址比较小,并且地址 + 长度总小于堆顶 (sbrk(0)) ,即在堆内分配,虚拟地址比较小。2. 第一次申请内存的 heap_var1 与分配前的堆顶地址相邻很近(只差 16 字节),事实上,每次分配的内存地址前 16 字节( 64 位系统, 32 位是 8 字节)是记录该内存块的控制信息(用于 free )。3. 并不是每次 malloc 都会导致堆顶的增大,例如分配 heap_var2 堆顶没有变化,事实上每次 malloc 堆顶至少增大 128k ,如果堆内有足够剩余空间,堆顶不会发生变化。4. 当分配大小大于 128k ,使用 mmap 获得地址空间,地址一般比较大(靠近栈区间),如 mmap_var15. 但当堆顶有足够剩余空间,仍优先使用堆内空间,而不使用 mmap ,如maybe_mmap_var 虽然分配 128k ,但仍是堆内分配。6. 可通过函数 mallopt(M_MMAP_THRESHOLD, 64*1024) 修改使用 mmap 的临界值为64k ,后续大于 64k 的分配就会使用 mmap ,如 mmap_var2/3.3.malloc 分配多大的内存,就占用多大的物理内存空间吗?我们知道, malloc 分配的的内存是虚拟地址空间,而虚拟地址空间和物理地址空间使用进程页表进行映射,那么分配了空间就是占用物理内存空间了吗?首先,进程使用多少内存可通过 ps aux 命令 查看,其中关键的两信息(第五、六列)为:1. VSZ , virtual memory size ,表示进程总共使用的虚拟地址空间大小,包括进程地址空间的代码段、数据段、堆、文件映射区域、栈、内核空间等所有虚拟地址使用的总和,单位是 K2. RSS , resident set size ,表示进程实际使用的物理内存空间, RSS 总小于 VSZ 。可通过一个例子说明这个问题:include <stdlib.h>
include <stdio.h>
include <string.h>
include <unistd.h>
include <sys/mman.h>
include <malloc.h>
char ps_cmd[1024];
void print_info(
char* var_name, char* var_ptr, size_t size_in_kb
)
{
printf("Address of %s(%luk) 0x%lx, now heap top is 0x%lx\n", var_name, size_in_kb, var_ptr, sbrk(0)); system(ps_cmd);
}
int main(int argc, char** argv)
{
char *non_set_var, *set_1k_var, *set_5k_var, *set_7k_var; pid_t pid;
pid = getpid(); sprintf(ps_cmd, "ps aux | grep %lu | grep -v grep", pid);
non_set_var = malloc(32*1024); print_info("non_set_var", non_set_var, 32);
set_1k_var = malloc(64*1024); memset(set_1k_var, 0, 1024); print_info("set_1k_var", set_1k_var, 64);
set_5k_var = malloc(127*1024); memset(set_5k_var, 0, 5*1024); print_info("set_5k_var", set_5k_var, 127);
set_7k_var = malloc(64*1024); memset(set_1k_var, 0, 7*1024); print_info("set_7k_var", set_7k_var, 64);
return 1;
}
该代码扩展了上一个例子 print_info 能力,处理打印变量信息,同时通过 ps aux 命令获得当前进程的 VSZ 和 RSS 值。并且程序 malloc 一块内存后,会 memset 内存的若干 k 内容。执行结果为Address of non_set_var(32k) 0x502010, now heap top is 0x52b000mysql 12183 0.0 0.0 2692 452 pts/3 S+ 20:29 0:00 ./test_vsz
Address of set_1k_var(64k) 0x50a020, now heap top is 0x52b000
mysql 12183 0.0 0.0 2692 456 pts/3 S+ 20:29 0:00 ./test_vsz
Address of set_5k_var(127k) 0x51a030, now heap top is 0x55a000
mysql 12183 0.0 0.0 2880 464 pts/3 S+ 20:29 0:00 ./test_vsz
Address of set_7k_var(64k) 0x539c40, now heap top is 0x55a000
mysql 12183 0.0 0.0 2880 472 pts/3 S+ 20:29 0:00 ./test_vsz
由以上结果知:1. VSZ 并不是每次 malloc 后都增长,是与上一节说的堆顶没发生变化有关,因为可重用堆顶内剩余的空间,这样的 malloc 是很轻量快速的。2. 但如果 VSZ 发生变化,基本与分配内存量相当,因为 VSZ 是计算虚拟地址空间总大小。3. RSS 的增量很少,是因为 malloc 分配的内存并不就马上分配实际存储空间,只有第一次使用,如第一次 memset 后才会分配。4. 由于每个物理内存页面大小是 4k ,不管 memset 其中的 1k 还是 5k 、 7k ,实际占用物理内存总是 4k 的倍数。所以 RSS 的增量总是 4k 的倍数。5. 因此,不是 malloc 后就马上占用实际内存,而是第一次使用时发现虚存对应的物理页面未分配,产生缺页中断,才真正分配物理页面,同时更新进程页面的映射关系。这也是Linux 虚拟内存管理的核心概念之一。如何查看进程虚拟地址空间的使用情况?
进程地址空间被分为了代码段、数据段、堆、文件映射区域、栈等区域,那怎么查询这些虚拟地址空间的使用情况呢?Linux 提供了 pmap 命令来查看这些信息,通常使用 pmap -d $pid (高版本可提供 pmap -x $pid )查询,如下所示:
mysql@ TLOG_590_591:~/vin/test_memory> pmap -d 17867
17867: test_mmap
START SIZE RSS DIRTY PERM OFFSET DEVICE MAPPING
00400000 8K 4K 0K r-xp 00000000 08:01 /home/mysql/vin/test_memory/test_mmap
00501000 68K 8K 8K rw-p 00001000 08:01 /home/mysql/vin/test_memory/test_mmap
00512000 76K 0K 0K rw-p 00512000 00:00 [heap]
0053e000 256K 0K 0K rw-p 0053e000 00:00 [anon]
2b3428f97000 108K 92K 0K r-xp 00000000 08:01 /lib64/ld-2.4.so
2b3428fb2000 8K 8K 8K rw-p 2b3428fb2000 00:00 [anon]
2b3428fc1000 4K 4K 4K rw-p 2b3428fc1000 00:00 [anon]
2b34290b1000 8K 8K 8K rw-p 0001a000 08:01 /lib64/ld-2.4.so
2b34290b3000 1240K 248K 0K r-xp 00000000 08:01 /lib64/libc-2.4.so
2b34291e9000 1024K 0K 0K ---p 00136000 08:01 /lib64/libc-2.4.so
2b34292e9000 12K 12K 12K r--p 00136000 08:01 /lib64/libc-2.4.so
2b34292ec000 8K 8K 8K rw-p 00139000 08:01 /lib64/libc-2.4.so
2b34292ee000 1048K 36K 36K rw-p 2b34292ee000 00:00 [anon]
7fff81afe000 84K 12K 12K rw-p 7fff81afe000 00:00 [stack]
ffffffffff600000 8192K 0K 0K ---p 00000000 00:00 [vdso]
Total: 12144K 440K 96K
从这个结果可以看到进程虚拟地址空间的使用情况,包括起始地址、大小、实际使用内存、脏页大小、权限、偏移、设备和映射文件等。 pmap 命令就是基于下面两文件内容进行解析的: /proc/$pid/maps/proc/$pid/smaps
并且对于上述每个内存块区间,内核会使用一个 vm_area_struct 结构来维护,同时通过页面建立与物理内存的映射关系,如下图所示。5.free 的内存真的释放了吗(还给 OS ) ?前面所有例子都有一个很严重的问题,就是分配的内存都没有释放,即导致内存泄露。原则上所有 malloc/new 分配的内存,都需 free/delete 来释放。但是, free 了的内存真的释放了吗?要说清楚这个问题,可通过下面例子来说明。(1) 初始状态:如图 (1) 所示,系统已分配 ABCD 四块内存,其中 ABD 在堆内分配, C 使用 mmap 分配。为简单起见,图中忽略了如共享库等文件映射区域的地址空间。(2) E=malloc(100k) :分配 100k 内存,小于 128k ,从堆内分配,堆内剩余空间不足,扩展堆顶 (brk) 指针。(3) free(A) :释放 A 的内存,在 glibc 中,仅仅是标记为可用,形成一个内存空洞 ( 碎片 ),并没有真正释放。如果此时需要分配 40k 以内的空间,可重用此空间,剩余空间形成新的小碎片。(4) free(C) :C 空间大于 128K ,使用 mmap 分配,如果释放 C ,会调用 munmap 系统调用来释放,并会真正释放该空间,还给 OS ,如图 (4) 所示。(5) free(D) :与释放 A 类似,释放 D 同样会导致一个空洞,获得空闲空间,但并不会还给OS 。此时,空闲总空间为 100K ,但由于虚拟地址不连续,无法合并,空闲空间无法满足大于 60k 的分配请求。
(6) free(E) :释放 E ,由于与 D 连续,两者将进行合并,得到 160k 连续空闲空间。同时E 是最靠近堆顶的空间, glibc 的 free 实现中,只要堆顶附近释放总空间(包括合并的空间)超过 128k ,即会调用 sbrk(-SIZE) 来回溯堆顶指针,将原堆顶空间还给 OS ,如图(6) 所示。而堆内的空闲空间还是不会归还 OS 的。由此可见:1. malloc 使用 mmap 分配的内存 ( 大于 128k) , free 会调用 munmap 系统调用马上还给 OS ,实现真正释放。2. 堆内的内存,只有释放堆顶的空间,同时堆顶总连续空闲空间大于 128k 才使用 sbrk(-SIZE) 回收内存,真正归还 OS 。3. 堆内的空闲空间,是不会归还给 OS 的。
程序代码中 malloc 的内存都有相应的 free,就不会出现内存泄露了吗?
狭义上的内存泄露是指 malloc 的内存,没有 free ,导致内存浪费,直到程序结束。而广义上的内存泄露就是进程使用内存量不断增加,或大大超出系统原设计的上限。上一节说到, free 了的内存并不会马上归还 OS ,并且堆内的空洞(碎片)更是很难真正释放,除非空洞成为了新的堆顶。所以,如上一例子情况 (5) ,释放了 40k 和 60k 两片内存,但如果此时需要申请大于 60k (如 70k ),没有可用碎片,必须向 OS 申请,实际使用内存仍然增大。因此,随着系统频繁地 malloc 和 free ,尤其对于小块内存,堆内将产生越来越多不可用的碎片,导致“内存泄露”。而这种“泄露”现象使用 valgrind 是无法检测出来的。下图是 MySQL 存在大量分区表时的内存使用情况 (RSS 和 VSZ) ,疑似“内存泄露”。
因此,当我们写程序时,不能完全依赖 glibc 的 malloc 和 free 的实现。更好方式是建立属于进程的内存池,即一次分配 (malloc) 大块内存,小内存从内存池中获得,当进程结束或该块内存不可用时,一次释放 (free) ,可大大减少碎片的产生。
既然堆内内存不能直接释放,为什么不全部使用 mmap 来分配?
由于堆内碎片不能直接释放,而问题 5 中说到 mmap 分配的内存可以会通过 munmap 进行 free ,实现真正释放。既然堆内碎片不能直接释放,导致疑似“内存泄露”问题,为什么malloc 不全部使用 mmap 来实现呢?而仅仅对于大于 128k 的大块内存才使用 mmap ?其实,进程向 OS 申请和释放地址空间的接口 sbrk/mmap/munmap 都是系统调用,频繁调用系统调用都比较消耗系统资源的。并且, mmap 申请的内存被 munmap 后,重新申请会产生更多的缺页中断。例如使用 mmap 分配 1M 空间,第一次调用产生了大量缺页中断(1M/4K 次 ) ,当 munmap 后再次分配 1M 空间,会再次产生大量缺页中断。缺页中断是内核行为,会导致内核态 CPU 消耗较大。另外,如果使用 mmap 分配小内存,会导致地址空间的分片更多,内核的管理负担更大,见《 高性能编程需要注意大内存申请》。而堆是一个连续空间,并且堆内碎片由于没有归还 OS ,如果可重用碎片,再次访问该内存很可能不需产生任何系统调用和缺页中断,这将大大降低 CPU 的消耗。因此, glibc 的 malloc 实现中,充分考虑了 sbrk 和 mmap 行为上的差异及优缺点,默认分配大块内存 (128k) 才使用 mmap 获得地址空间,也可通过mallopt(M_MMAP_THRESHOLD, <SIZE>) 来修改这个临界值。如何查看进程的缺页中断信息?
可通过以下命令查看缺页中断信息
ps -o majflt,minflt -C <program_name>
ps -o majflt,minflt -p <pid>
其中, majflt 代表 major fault ,指大错误, minflt 代表 minor fault ,指小错误。这两个数值表示一个进程自启动以来所发生的缺页中断的次数。其中 majflt 与 minflt 的不同是,majflt 表示需要读写磁盘,可能是内存对应页面在磁盘中需要 load 到物理内存中,也可能是此时物理内存不足,需要淘汰部分物理页面至磁盘中。例如,下面是 mysqld 的一个例子。mysql@ TLOG_590_591:~> ps -o majflt,minflt -C mysqldMAJFLT MINFLT
144856 15296294
如果进程的内核态 CPU 使用过多,其中一个原因就可能是单位时间的缺页中断次数多个,可通过以上命令来查看。如果 MAJFLT 过大,很可能是内存不足。如果 MINFLT 过大,很可能是频繁分配 / 释放大块内存 (128k) , malloc 使用 mmap 来分配。对于这种情况,可通过 mallopt(M_MMAP_THRESHOLD, <SIZE>) 增大临界值,或程序实现内存池。如何查看堆内内存的碎片情况?
提供了以下结构和接口来查看堆内内存和 mmap 的使用情况。
struct mallinfo {
int arena; / non-mmapped space allocated from system /
int ordblks; / number of free chunks /
int smblks; / number of fastbin blocks /
int hblks; / number of mmapped regions /
int hblkhd; / space in mmapped regions /
int usmblks; / maximum total allocated space /
int fsmblks; / space available in freed fastbin blocks /
int uordblks; / total allocated space /
int fordblks; / total free space /
int keepcost; / top-most, releasable (via malloc_trim) space /
};
/ 返回 heap(main_arena) 的内存使用情况,以 mallinfo 结构返回 /
struct mallinfo mallinfo();
/ 将 heap 和 mmap 的使用情况输出到 stderr /
void malloc_stats();
可通过以下例子来验证 mallinfo 和 malloc_stats 输出结果。include <stdlib.h>
include <stdio.h>
include <string.h>
include <unistd.h>
include <sys/mman.h>
include <malloc.h>
size_t heap_malloc_total, heap_free_total,
mmap_total, mmap_count;
void print_info()
{
struct mallinfo mi = mallinfo();
printf("count by itself:\n"); printf("\theap_malloc_total=%lu heap_free_total=%lu heap_in_use=%lu\n\
tmmap_total=%lu mmap_count=%lun",
heap_malloc_total*1024, heap_free_total*1024, heap_malloc_total*1024 - heap_free_total*1024, mmap_total*1024, mmap_count);
printf("count by mallinfo:\n"); printf("\theap_malloc_total=%lu heap_free_total=%lu heap_in_use=%lu\n\
tmmap_total=%lu mmap_count=%lun",
mi.arena, mi.fordblks, mi.uordblks, mi.hblkhd, mi.hblks);
printf("from malloc_stats:\n"); malloc_stats();
}
define ARRAY_SIZE 200
int main(int argc, char** argv)
{
char** ptr_arr[ARRAY_SIZE]; int i; for( i = 0; i < ARRAY_SIZE; i++) { ptr_arr[i] = malloc(i * 1024);
if ( i < 128) heap_malloc_total += i; else { mmap_total += i; mmap_count++; } } print_info();
for( i = 0; i < ARRAY_SIZE; i++) { if ( i % 2 == 0) continue;
free(ptr_arr[i]);
if ( i < 128) heap_free_total += i; else { mmap_total -= i; mmap_count--; } } printf("\nafter free\n"); print_info();
return 1;
}
该例子第一个循环为指针数组每个成员分配索引位置 (KB) 大小的内存块,并通过 128 为分界分别对 heap 和 mmap 内存分配情况进行计数;第二个循环是 free 索引下标为奇数的项,同时更新计数情况。通过程序的计数与 mallinfo/malloc_stats 接口得到结果进行对比,并通过print_info 打印到终端。下面是一个执行结果:count by itself:heap_malloc_total=8323072 heap_free_total=0 heap_in_use=8323072 mmap_total=12054528 mmap_count=72
count by mallinfo:
heap_malloc_total=8327168 heap_free_total=2032 heap_in_use=8325136 mmap_total=12238848 mmap_count=72
from malloc_stats:
Arena 0:
system bytes = 8327168
in use bytes = 8325136
Total (incl. mmap):
system bytes = 20566016
in use bytes = 20563984
max mmap regions = 72
max mmap bytes = 12238848
after free
count by itself:
heap_malloc_total=8323072 heap_free_total=4194304 heap_in_use=4128768 mmap_total=6008832 mmap_count=36
count by mallinfo:
heap_malloc_total=8327168 heap_free_total=4197360 heap_in_use=4129808 mmap_total=6119424 mmap_count=36
from malloc_stats:
Arena 0:
system bytes = 8327168
in use bytes = 4129808
Total (incl. mmap):
system bytes = 14446592
in use bytes = 10249232
max mmap regions = 72
max mmap bytes = 12238848
由上可知,程序统计和 mallinfo 得到的信息基本吻合,其中 heap_free_total 表示堆内已释放的内存碎片总和。如果想知道堆内片究竟有多碎 ,可通过 mallinfo 结构中的 fsmblks 、 smblks 、 ordblks值得到,这些值表示不同大小区间的碎片总个数,这些区间分别是 0~80 字节, 80~512 字节,512~128k 。如果 fsmblks 、 smblks 的值过大,那碎片问题可能比较严重了。不过, mallinfo 结构有一个很致命的问题,就是其成员定义全部都是 int ,在 64 位环境中,其结构中的 uordblks/fordblks/arena/usmblks 很容易就会导致溢出,应该是历史遗留问题,使用时要注意!除了 glibc 的 malloc/free ,还有其他第三方实现吗?
其实,很多人开始诟病 glibc 内存管理的实现,就是在高并发性能低下和内存碎片化问题都比较严重,因此,陆续出现一些第三方工具来替换 glibc 的实现,最著名的当属 google 的tcmalloc 和 facebook 的 jemalloc 。网上有很多资源,可搜索之,这里就不详述了。
总结
基于以上认识,最后发现 MySQL 的疑似“内存泄露”问题一方面是 MySQL 5.5 分区表使用更多的内存,另一方面跟内存碎片有关,这也是 TMySQL 一个优化方向。然而,以上主要介绍了 glibc 虚拟内存管理主要内容,事实上,在并发情况下, glibc 的虚存管理会更加复杂,碎片情况也可能更严重,这将在另一篇再做介绍。推荐另一篇文章《深入剖析glibc内存管理实现及潜在问题》参考:《深入理解计算机系统》第 10 章