概念
概念
在wikipedia这样解读内存泄漏的:
- 在计算机科学中,内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。
详情请看wikipedia内存泄漏。
内存不是无穷无尽的,是有限的,如果申请了,使用了,用完没有释放,那么这块内存就一直无法被重新利用,最后再申请内存就找不到空闲的了(称为out of memory,OOM),可能导致程序逻辑错误崩溃。
以下用一个简单的例子说明内存泄漏:
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
char *p[1000];
int i;
for (i = 0; i < 1000; i++) {
p[i] = malloc(i * 2 + 100);
if (p[i])
memset(p[i], 0, i * 2 + 100);
}
for (i = 0; i < 1000; i++) {
printf("%p\n", p[i]);
}
return 0;
}
上面的程序malloc申请的内存直至退出都没有被释放,这个就是内存泄漏了。
分类
内存泄漏可以根据发生的方式来分类:
- 常发性泄漏
- 故障代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。
- 偶发性泄漏
- 故障代码只有在特定场景或操作过程下才会被执行。常发性和偶发性是相对的。对于特定场景,偶发性的也许就变成了常发性的。所以测试场景和方法对检测内存泄漏至关重要。
- 一次性泄漏
- 故障代码只会被执行一次或者由于逻辑的缺陷导致只有一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存。
- 隐式泄漏
- 程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但对于daemon或需要长时间运行的程序来讲,不及时释放可能最终耗尽所有内存。
另外不同的软件层都有可能存在内存泄漏:kernel层、native层和java层。不同的软件层泄漏的原理都差不多。
危害
内存泄漏是较难检测的异常之一,除了常发性泄漏,其他都是难以检查到的。从用户使用程序的角度来看,内存泄漏本身不会产生什么危害,作为用户无法感觉到内存泄漏的存在,除非泄漏大量内存或一直累积直至消耗光内存,这时就有各种明显表现:
- 性能渐渐变差,因为要做各种内存回收工作。
- 直接出现逻辑错误崩溃,没有做好异常处理(error handling)。
- 本身没有问题,却引起其他程序异常。
既然内存泄漏这么难处理,那么是否有办法自动回收,不需要编程人员管理申请的内存呢?有的,java语言就有内存回收机制(垃圾回收,gc)。
不过即使用java编写的程序也可能出现泄漏,后面会讲解java内存泄漏的原理和调试方法。
通用的调试方法
需要对每一块申请的内存做标记,记录调用栈等信息,然后监控内存用量信息,感觉已经超出正常的内存用量很多时(相差不大时不见得能查到问题),提取这些信息,然后分析信息,找出可能的泄漏点,检查代码,分析逻辑,修复问题。
通过malloc分配的内存泄漏
对于native内存泄漏,比较常见的一类是C堆内存泄漏,即调用malloc申请的内存没有及时释放造成的内存泄漏。
调试方法
1.对于O版本及之后的版本,打开android malloc debug机制复现问题
如何开启android malloc debug机制请参考
MediaTek On-Line > Quick Start > 踩内存专题分析 > native踩内存 > 调试方法(O版本及之后)
2.对于N版本及之前的版本,打开malloc debug15机制复现问题
如何开启malloc debug15请参考:
MediaTek On-Line> Quick Start> 踩内存专题分析 > native踩内存 > 调试方法(N版本及之前版本)
2.检查是否存在内存泄漏。
打开mtklogger,不断通过adb shell procrank -u > procrank.txt查看当前系统内存的使用情况。当看到被监控的进程占用内存(USS字段)超过了正常值很多,则可能存在内存泄漏。
3. 得到该进程内存使用分布情况($pid为被监控进程pid)
(1)adb shell dumpsys meminfo $pid >meminfo_$pid.txt
(2)adb shell procmem $pid > procmem_$pid.txt
(3)adb shell kill -11 $pid>生成DB
分析问题
复现问题后使用E-Consulter分析,分析完coredump会提供分析报告。如果存在超过128M以上的泄漏,那么分析报告会提示如下:
== C堆检查 ==
分配器: dlmalloc, 最多允许使用: 4GB, 最多使用: 188MB, 当前使用: 166MB, 泄露阈值: 128MB, 调试等级: 15
该堆已分配超过128MB (可能存在内存泄露), 以下列出分配最大尺寸和次数的调用栈:
大小: 1184字节, 已分配: 134604次
分配调用栈:
libc_malloc_debug_leak.so 0x40D5ECD4() + 16
libc_malloc_debug_leak.so leak_malloc() + 43
libc.so malloc() + 18
libsqlite.so sqlite3MemMalloc() + 14 <external/sqlite/dist/sqlite3.c:15298>
libsqlite.so mallocWithAlarm() + 86 <external/sqlite/dist/sqlite3.c:18862>
libsqlite.so sqlite3Malloc() + 16 <external/sqlite/dist/sqlite3.c:18895>
......
== 栈结束 ==
大小: 1184字节, 已分配: 5149次
分配调用栈:
libc_malloc_debug_leak.so 0x40D5ECD4() + 16
libc_malloc_debug_leak.so leak_malloc() + 43
libc.so malloc() + 18
libsqlite.so sqlite3MemMalloc() + 14 <external/sqlite/dist/sqlite3.c:15298>
libsqlite.so mallocWithAlarm() + 86 <external/sqlite/dist/sqlite3.c:18862>
......
== 栈结束 ==
大小: 1048584字节, 已分配: 1次
分配调用栈:
libc_malloc_debug_leak.so 0x40D5ECD4() + 16
libc_malloc_debug_leak.so leak_malloc() + 43
libc.so malloc() + 18
libsqlite.so sqlite3MemMalloc() + 14 <external/sqlite/dist/sqlite3.c:15298>
libsqlite.so mallocWithAlarm() + 86 <external/sqlite/dist/sqlite3.c:18862>
libsqlite.so sqlite3Malloc() + 16 <external/sqlite/dist/sqlite3.c:18895>
......
== 栈结束 ==
剩下就是检查代码逻辑,修复问题了
通过mmap分配的内存泄漏
native进程申请内存的方法很多,除了可以通过传统的堆分配函数malloc分配内存,还可以直接通过mmap映射内存。对于通过mmap分配的内存泄漏该如何调试呢?
调试方法
1.打开mmap debug机制复现
在vendor/mediatek/proprietary/external/aee/config_external/init.aee.customer.rc添加:
on init
export LD_PRELOAD libsigchain.so:libudf.so:...
"..."是原先LD_PRELOAD的内容,如果原先LD_PRELOAD没有内容,则":..."就不需要了。
重新打包bootimage并下载开机, 用adb输入: