解密native代码的内存使用
前言
无论是从资源使用的角度,还是从发现内存泄漏问题的角度来看,在性能测试或者系统的稳定性测试中,内存的使用情况是一个很重要的监控点。为保证项目的质量前移,输入法内核测试小组的同学分配到了一个新的任务,就是在andriod平台上监测输入法内核 native 代码的内存使用情况。
为了完成这个任务,我们先将任务拆解成重要的几步:
1. 了解 andriod 平台的程序与内存
2. 了解 native代码在andriod平台上是如何进行内存分配的
3. 根据2步骤,方法调研,监测native代码内存使用情况
完成任务前先了解一些基本小知识:
堆和栈:
Stack空间(进栈和出栈)由操作系统控制,其中主要存储函数地址、函数参数、局部变量等等,所以Stack空间不需要很大,一般为几mb大小。
Heap空间 由程序控制,程序员可以使用malloc、new、free、delete等函数调用来操作这片地址空间。heap为程序完成各种复杂任务提供内存空间,所以空间比较大,一般为几百mb到几gb。正是因为heap空间由程序员管理,所以容易出现使用不当导致严重问题,我们重点关注的也是这部分由程序员控制的内存空间。
正文
1
了解 andriod 平台的程序与内存
在Android里,程序内存被分为2部分:native进程和:dalvik进程
native进程:采用c/c++实现,不包含dalvik实例的linux进程,/system/bin/目录下面的程序文件运行后都是以native进程形式存在的。
java进程(dalvik进程):是实例化了dalvik虚拟机实例的linux进程,进程的入口main函数为java函数。dalvik虚拟机实例的宿主进程是fork()系统调用创建的linux进程,所以每一个android上的java进程实际上就是一个linux进程,只是进程中多了一个dalvik虚拟机实例。
使用命令 adb shell prorank 可以看到
/system/bin/* 下面的是native进程,桌面、电话、联系人、状态栏等等都是java进程。
在这里不详细描述,native进程和java进程究竟如何分配的内存,有兴趣的同学可以自行搜索,我们只重点关注两种进程空间中的heap空间的分配情况。heap空间完全由程序员控制分配,一共可分为dalvik heap 和 native heap两种,我们使用的malloc、c++ new和java new所申请的空间都是heap空间, c/c++申请的内存空间在native heap中,而java申请的内存空间则在dalvik heap中。
2
了解 native代码在andriod上是如何进行内存分配的
根据以上,我们已经了解了 c/c++代码申请的内存空间在native heap中,而java代码申请的内存空间则在dalvik heap中,那么 native代码在andriod平台上进行内存分配的这个过程是如何执行的呢,这就离不开一个概念JNI,什么是JNI:JNI是Java Native Interface的缩写,提供了若干API实现了java和其他语言的通信(主要是c&c++)。
在java代码中,java对象被存放在jvm的java heap中,由垃圾回收器自动回收就可以。
在JNI代码中,JNI与java的分配方式有所不同,JNI是java/其他语言交流的媒介。JNI提供了与java相对应的引用类型(如:jobject、jstring、jclass、jarray、jintArray等),以便native代码可以通过JNI函数访问到java对象。引用所指向的java对象通常就是存放在java heap,而native代码持有的引用是存放在native memory中。
举个例子,如下代码:
jstring jstr = env->NewStringUTF("Hello World!");
-
jstring类型是JNI提供的,对应于java的string类型。
-
JNI函数NewStringUTF()用于构造一个string对象,该对象存放在java heap中,同时返回了一个jstring类型的引用。
-
string对象的引用保存在jstr中,jstr是native的一个局部变量,存放在native memory中。
在native代码中,根据下图jvm的内存模型,可以看到本地库接口是在在jvm运行时数据区域之外。因此,调用c/c++写的库,malloc 分配的内存是按照c/c++的规范去操作内存,内存是从native heap中分配的。
3
根据2步骤,方法调研,监测native代码内存使用情况
根据2步骤,我们可以知道,如果想在andriod平台监控native的内存占用情况,我们需要在native代码运行时监控native heap的分配情况。
使用命令adb shell dumpsys meminfo就可以达到以上需求:
一般实时输出的信息像这样:
在这里对主要字段进行解释:
横轴:
pss: proportional set size是内核计算的度量,代表整体的内存情况,它将内存共享考虑在内。系统会根据内存页中的其他进程使用的比例动态调整每个RAM内存页。
private dirty:进程独占的内存。也就是应用进程销毁时系统可以直接回收的内存容量。通常来说,“private dirty”内存是其最重要的部分,因为只被自己的进程使用。它只在内存中存储,因此不能做分页存储到外存。所有分配的dalvik堆和本地堆都是“private dirty”内存。pss/private dirty三列是读取了/proc/process-id/smaps文件获取的,可以通过adbshell cat /proc/process-id/smaps来查看。
private clean:包括该进程私有的干净的内存。
纵轴:
dalvik heap:应用中dalvik分配使用的内存。
native heap:应用中c/c++分配使用的内存。
.so / jar /apk /...mmap:c库 / java /apk /...代码占用的内存。
unknown:无法归类到其它项的内存页。目前,这主要包含大部分的本地分配,就是那些在工具收集数据时由于地址空间布局随机化不能被计算在内的部分。
total:进程总使用的实际使用内存(PSS),是上面所有PSS项的总和。它表明了进程总的内存使用量,可以直接用来和其它进程或总的可以内存进行比较。
结论
使用命令adb shell dumpsys meminfo 监控native heap只能大致分析native 代码的使用情况,与真实使用存在一定的误差,如果对native内存使用有着非常高精确的需求,可以尝试抓取一个带有调用堆栈地址的native heap usage, 有了堆栈地址, 我们可以通过arm-linux-androideabi-addr2line.exe把地址翻译成函数名. 尝试将堆栈地址合并进行内存的累加统计,可以精确的看到函数的内存占用情况,不过小编还没有尝试过这种方法,如果有尝试过的同学,欢迎在评论里和大家分享经验,不胜感谢~