GLIBC内存分配机制引发的“内存泄露”

简介:

我们正在开发的类数据库系统有一个内存模块,出现了一个疑似”内存泄露”问题,现象如下:内存模块的内存释放以后没有归还操作系统,比如内存模块占用的内存为10GB,释放内存以后,通过TOP命令或者/proc/pid/status查看占用的内存有时仍然为10G,有时为5G,有时为3G, etc,内存释放的行为不确定。

首先说一下内存模块的内存管理机制。我们的内存管理很简单,使用全局的定长内存池,每一个内存块为64KB,如果申请的内存小于等于64KB时,直接从内存池的空闲链表中获取一个内存块,内存释放时归还空闲链表;如果申请的内存大于64KB,直接通过操作系统的malloc和free获取。某些数据结构涉及到很多小对象的管理,比如Hash表,B-Tree,这些数据结构从全局内存池获取内存后再根据数据结构的特点进行组织。为了提高内存申请/释放的效率,减少锁冲突,为每一个线程单独保留一个8MB的内存块,每个线程优先从线程专属的8MB内存块获取内存,专属内存不足时才从全局的内存池获取。

由于我们的所有内存申请/释放操作都需要通过全局的内存池进行,我们在全局的内存池中加入对每个子模块的内存统计功能:每个子模块申请内存时都将子模块编号传给全局的内存池,全局的内存池进行统计。复现问题后发现全局的内存池的统计结果符合预期,因此怀疑是操作系统或者glibc的行为。

Linux下Glibc的内存管理机制大致如下:

从操作系统的角度看,进程的内存分配由两个系统调用完成:brk和mmap。brk是将数据段(.data)的最高地址指针_edata往高地址推,mmap是在进程的虚拟地址空间中找一块空闲的。其中,mmap分配的内存由munmap释放,内存释放时将立即归还操作系统;而brk分配的内存需要等到高地址内存释放以后才能释放。也就是说,如果先后通过brk申请了A和B两块内存,在B释放之前,A是不可能释放的,仍然被进程占用,通过TOP查看疑似”内存泄露”。默认情况下,大于等于128KB的内存分配会调用mmap/mummap,小于128KB的内存请求调用sbrk(可以通过设置M_MMAP_THRESHOLD来调整)。详细的内存管理机制可以参考百度分享的文章

我们的内存模块申请/释放内存都是以2MB为单位的,按理说应该是使用mmap和munmap进行内存分配和释放的,不会出现内存释放以后仍然被进程占用的情况。在内核同学的协助下,经过长时间的分析定位,发现了Glibc的新特性:M_MMAP_THRESHOLD可以动态调整。M_MMAP_THRESHOLD的值在128KB到32MB(32位机)或者64MB(64位机)之间动态调整,每次申请并释放一个大小为2MB的内存后,M_MMAP_THRESHOLD的值被调整为2M到2M + 4K之间的一个值(具体可以参考Glibc的patch说明)。例如:

char* no_used = new char[2 * 1024 * 1024];

memset(no_used, 0xfe, 2 * 1024 * 1024);

delete[] no_used;

// M_MMAP_THRESHOLD的值调整为2M到2M + 4K之间的一个值,后续申请 <= 2 * 1024 * 1024的内存块都会走sbrk而不是mmap

了解到这种现象后,我们找到了”内存泄露”的原因:M_MMAP_THRESHOLD的值动态调整,后续的2MB的内存申请通过sbrk实现,而sbrk需要等到高地址内存释放以后低地址内存才能释放。可以通过显式设置M_MMAP_THRESHOLD或者M_MMAP_MAX来关闭M_MMAP_THRESHOLD动态调整的特性,从而避免上述问题。

当然,mmap调用是会导致进程产生缺页中断的,为了提高性能,常见的做法如下:

1, 将动态内存改为静态,比如采用内存池技术或者启动的时候给每个线程分配一定大小,比如8MB的内存,以后直接使用;

2, 禁止mmap内存调用,禁止Glibc内存缩紧将内存归还系统,Glibc相当于实现了一个内存池功能。只需要在进程启动的时候加入两行代码:

mallopt(M_MMAP_MAX, 0); // 禁止malloc调用mmap分配内存

mallopt(M_TRIM_THRESHOLD, 0); // 禁止内存缩进,sbrk申请的内存释放后不会归还给操作系统

花絮:

追查”内存泄露”问题的过程中,尝试使用Glibc的钩子函数(Malloc Hook) 统计malloc和free的内存量:具体做法为malloc的时候多申请8个字节,其中4个字节记录长度,4个字节记录magic_num,malloc和free的时候统计进程申请和释放的内存量。实践表明无论自定义钩子函数是否加锁,malloc和free钩子函数在多线程的情况下运行都不正常,其它同学也发现了相同的问题(Malloc Hook多线程问题)。

目录
相关文章
|
1月前
|
存储 监控 算法
Java中的内存管理:理解Garbage Collection机制
本文将深入探讨Java编程语言中的内存管理,着重介绍垃圾回收(Garbage Collection, GC)机制。通过阐述GC的工作原理、常见算法及其在Java中的应用,帮助读者提高程序的性能和稳定性。我们将从基本原理出发,逐步深入到调优实践,为开发者提供一套系统的理解和优化Java应用中内存管理的方法。
|
2月前
|
监控 算法 Java
Java中的内存管理:理解Garbage Collection机制
本文将深入探讨Java编程语言中的内存管理,特别是垃圾回收(Garbage Collection, GC)机制。我们将从基础概念开始,逐步解析垃圾回收的工作原理、不同类型的垃圾回收器以及它们在实际项目中的应用。通过实际案例,读者将能更好地理解Java应用的性能调优技巧及最佳实践。
91 0
|
18天前
|
存储 算法 Java
Go语言的内存管理机制
【10月更文挑战第25天】Go语言的内存管理机制
21 2
|
20天前
|
存储 运维 Java
💻Java零基础:深入了解Java内存机制
【10月更文挑战第18天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
27 1
|
1月前
|
存储 安全 NoSQL
driftingblues9 - 溢出ASLR(内存地址随机化机制)
driftingblues9 - 溢出ASLR(内存地址随机化机制)
35 1
|
2月前
|
消息中间件
共享内存和信号量的配合机制
【9月更文挑战第16天】本文介绍了进程间通过共享内存通信的机制及其同步保护方法。共享内存可让多个进程像访问本地内存一样进行数据交换,但需解决并发读写问题,通常借助信号量实现同步。文章详细描述了共享内存的创建、映射、解除映射等操作,并展示了如何利用信号量保护共享数据,确保其正确访问。此外,还提供了具体代码示例与步骤说明。
|
1月前
|
程序员 编译器 数据处理
【C语言】深度解析:动态内存管理的机制与实践
【C语言】深度解析:动态内存管理的机制与实践
|
3月前
|
存储 编译器 C语言
【C语言篇】数据在内存中的存储(超详细)
浮点数就采⽤下⾯的规则表⽰,即指数E的真实值加上127(或1023),再将有效数字M去掉整数部分的1。
370 0
|
22天前
|
存储 C语言
数据在内存中的存储方式
本文介绍了计算机中整数和浮点数的存储方式,包括整数的原码、反码、补码,以及浮点数的IEEE754标准存储格式。同时,探讨了大小端字节序的概念及其判断方法,通过实例代码展示了这些概念的实际应用。
45 1
|
26天前
|
存储
共用体在内存中如何存储数据
共用体(Union)在内存中为所有成员分配同一段内存空间,大小等于最大成员所需的空间。这意味着所有成员共享同一块内存,但同一时间只能存储其中一个成员的数据,无法同时保存多个成员的值。