尊重原创作者,转载请注明出处:

http://blog.csdn.net/gemmem/article/details/8920039

最近在网上看了不少Android内存管理方面的博文,但是文章大多都是就单个方面去介绍内存管理,没有能全局把握,缺乏系统性阐述,而且有些观点有误。

这样对Android内存管理进行局部性介绍,很难使读者建立系统性概念,无法真正理解内存管理,对提高系统优化和系统稳定性分析方面的能力是不够的。

我结合自己的一些思考和理解,从宏观层面上,对内存管理做一个全局性的介绍,在此与大家交流分享。


首先,回顾一下基础知识,基础知识是理解系统机制的前提和关键:

1、 进程的地址空间

在32位操作系统中,进程的地址空间为0到4GB,

示意图如下:



图1

这里主要说明一下Stack和Heap:

Stack空间(进栈和出栈)由操作系统控制,其中主要存储函数地址、函数参数、局部变量等等,所以Stack空间不需要很大,一般为几MB大小。

Heap空间的使用由程序员控制,程序员可以使用malloc、new、free、delete等函数调用来操作这片地址空间。Heap为程序完成各种复杂任务提供内存空间,所以空间比较大,一般为几百MB到几GB。正是因为Heap空间由程序员管理,所以容易出现使用不当导致严重问题。

2、进程内存空间和RAM之间的关系

进程的内存空间只是虚拟内存(或者叫作逻辑内存),而程序的运行需要的是实实在在的内存,即物理内存(RAM)。在必要时,操作系统会将程序运行中申请的内存(虚拟内存)映射到RAM,让进程能够使用物理内存。

RAM作为进程运行不可或缺的资源,对系统性能和稳定性有着决定性影响。另外,RAM的一部分被操作系统留作他用,比如显存等等,内存映射和显存等都是由操作系统控制,我们也不必过多地关注它,进程所操作的空间都是虚拟地址空间,无法直接操作RAM。

示意图如下:


图2

基础知识介绍到这里,如果读者理解以上知识有障碍,请好好恶补一下基础知识,基础理论知识至关重要。

3、 Android中的进程

(1)native进程:采用C/C++实现,不包含dalvik实例的进程,/system/bin/目录下面的程序文件运行后都是以native进程形式存在的。如图 3,/system/bin/surfaceflinger、/system/bin/rild、procrank等就是native进程。

(2)java进程:Android中运行于dalvik虚拟机之上的进程。dalvik虚拟机的宿主进程由fork()系统调用创建,所以每一个java进程都是存在于一个native进程中,因此,java进程的内存分配比native进程复杂,因为进程中存在一个虚拟机实例。如图3,Android系统中的应用程序基本都是java进程,如桌面、电话、联系人、状态栏等等。




图3

4、 Android中进程的堆内存

图1和图4分别介绍了native process和java process的结构,这个是我们程序员需要深刻理解的,进程空间中的heap空间是我们需要重点关注的。heap空间完全由程序员控制,我们使用的malloc、C++ new和java new所申请的空间都是heap空间, C/C++申请的内存空间在native heap中,而java申请的内存空间则在dalvikheap中。




图4

5、 Android的 java程序为什么容易出现OOM

这个是因为Android系统对dalvik的vm heapsize作了硬性限制,当java进程申请的java空间超过阈值时,就会抛出OOM异常(这个阈值可以是48M、24M、16M等,视机型而定),可以通过adb shell getprop | grep dalvik.vm.heapgrowthlimit查看此值。

也就是说,程序发生OMM并不表示RAM不足,而是因为程序申请的java heap对象超过了dalvik vm heapgrowthlimit。也就是说,在RAM充足的情况下,也可能发生OOM。

这样的设计似乎有些不合理,但是Google为什么这样做呢?这样设计的目的是为了让Android系统能同时让比较多的进程常驻内存,这样程序启动时就不用每次都重新加载到内存,能够给用户更快的响应。迫使每个应用程序使用较小的内存,移动设备非常有限的RAM就能使比较多的app常驻其中。但是有一些大型应用程序是无法忍受vm heapgrowthlimit的限制的,后面会介绍如何让自己的程序跳出vm heapgrowthlimit的限制。

6、 Android如何应对RAM不足

在第5点中提到:java程序发生OMM并不是表示RAM不足,如果RAM真的不足,会发生什么呢?这时Android的memory killer会起作用,当RAM所剩不多时,memory killer会杀死一些优先级比较低的进程来释放物理内存,让高优先级程序得到更多的内存。我们在分析log时,看到的进程被杀的log,如图5,往往就是属于这种情况。




图5

7、 如何查看RAM使用情况

可以使用adb shell cat /proc/meminfo查看RAM使用情况:

MemTotal:396708 kB

MemFree:4088 kB

Buffers:5212 kB

Cached:211164 kB

SwapCached:0 kB

Active:165984 kB

Inactive:193084 kB

Active(anon):145444 kB

Inactive(anon):248 kB

Active(file):20540 kB

Inactive(file):192836 kB

Unevictable:2716 kB

Mlocked:0 kB

HighTotal:0 kB

HighFree:0 kB

LowTotal:396708 kB

LowFree:4088 kB

SwapTotal:0 kB

SwapFree:0 kB

Dirty:0 kB

Writeback:0 kB

AnonPages:145424 kB

……

……

这里对其中的一些字段进行解释:

MemTotal:可以使用的RAM总和(小于实际RAM,操作系统预留了一部分)

MemFree:未使用的RAM

Cached:缓存(这个也是app可以申请到的内存)

HightTotal:RAM中地址高于860M的物理内存总和,只能被用户空间的程序使用。

HightFree:RAM中地址高于860M的未使用内存

LowTotal:RAM中内核和用户空间程序都可以使用的内存总和(对于512M的RAM: lowTotal= MemTotal)

LowFree: RAM中内核和用户空间程序未使用的内存(对于512M的RAM: lowFree = MemFree)

8、 如何查看进程的内存信息

(1)、使用adb shell dumpsys meminfo + packagename/pid:

从图6可以看出,com.example.demo作为java进程有2个heap,native heap和dalvik heap,

native heap size为159508KB,dalvik heap size为46147KB



图6

(2)、使用adb shell procrank查看进程内存信息

如图7:



图7

解释一些字段的意思:

VSS- Virtual Set Size 虚拟耗用内存(包含共享库占用的内存)

RSS- Resident Set Size 实际使用物理内存(包含共享库占用的内存)

PSS- Proportional Set Size 实际使用的物理内存(比例分配共享库占用的内存)

USS- Unique Set Size 进程独自占用的物理内存(不包含共享库占用的内存)

一般来说内存占用大小有如下规律:VSS >= RSS >= PSS >= USS

注意:dumpsys meminfo可以查看native进程和java进程,而procrank只能查看java进程。

9、 应用程序如何绕过dalvikvm heapsize的限制

对于一些大型的应用程序(比如游戏),内存使用会比较多,很容易超超出vm heapsize的限制,这时怎么保证程序不会因为OOM而崩溃呢?

(1)、创建子进程

创建一个新的进程,那么我们就可以把一些对象分配到新进程的heap上了,从而达到一个应用程序使用更多的内存的目的,当然,创建子进程会增加系统开销,而且并不是所有应用程序都适合这样做,视需求而定。

创建子进程的方法:使用android:process标签

(2)、使用jni在native heap上申请空间(推荐使用)

nativeheap的增长并不受dalvik vm heapsize的限制,从图6可以看出这一点,它的native heap size已经远远超过了dalvik heap size的限制。

只要RAM有剩余空间,程序员可以一直在native heap上申请空间,当然如果 RAM快耗尽,memory killer会杀进程释放RAM。大家使用一些软件时,有时候会闪退,就可能是软件在native层申请了比较多的内存导致的。比如,我就碰到过UC web在浏览内容比较多的网页时闪退,原因就是其native heap增长到比较大的值,占用了大量的RAM,被memory killer杀掉了。

(3)、使用显存(操作系统预留RAM的一部分作为显存)

使用OpenGL texturesAPItexture memory不受dalvik vm heapsize限制,这个我没有实践过。再比如Android中的GraphicBufferAllocator申请的内存就是显存。

10、Bitmap分配在native heap还是dalvik heap上?

一种流行的观点是这样的:

Bitmap是jni层创建的,所以它应该是分配到native heap上,并且为了解释bitmap容易导致OOM,提出了这样的观点:

native heap size + dalvik heapsize <= dalvik vm heapsize

详情请看:http://devspirit.blog.163.com/blog/static/16425531520104199512427/

但是请大家看看图6,native heap size为159508KB,远远超过dalvik vm heapsize,所以,事实证明以上观点是不正确的。

正确的观点:

大家都知道,过多地创建bitmap会导致OOM异常,且native heapsize不受dalvik限制,所以可以得出结论:

Bitmap只能是分配在dalvik heap上的,因为只有这样才能解释bitmap容易导致OOM。

但是,有人可能会说,Bitmap确实是使用java native方法创建的啊,为什么会分配到dalvik heap中呢?为了解决这个疑问,我们还是分析一下源码:

涉及的文件:

[java] view plain copy
  1. framework/base/graphic/java/Android/graphics/BitmapFactory.java
  2. framework/base/core/jni/Android/graphics/BitmapFactory.cpp
  3. framework/base/core/jni/Android/graphics/Graphics.cpp

BitmapFactory.java里面有几个decode***方法用来创建bitmap,最终都会调用:

private staticnative Bitmap nativeDecodeStream(InputStream is, byte[] storage,Rect padding,Options opts);

而nativeDecodeStream()会调用到BitmapFactory.cpp中的deDecode方法,最终会调用到Graphics.cpp的createBitmap方法。

我们来看看createBitmap方法的实现:

[java] view plain copy
  1. jobjectGraphicsJNI::createBitmap(JNIEnv*env,SkBitmap*bitmap,jbyteArraybuffer,
  2. boolisMutable,jbyteArrayninepatch,intdensity)
  3. {
  4. SkASSERT(bitmap);
  5. SkASSERT(bitmap->pixelRef());
  6. jobjectobj=env->NewObject(gBitmap_class,gBitmap_constructorMethodID,
  7. static_cast<jint>(reinterpret_cast<uintptr_t>(bitmap)),
  8. buffer,isMutable,ninepatch,density);
  9. hasException(env);//Forthesideeffectoflogging.
  10. returnobj;
  11. }

从代码中可以看到bitmap对象是通过env->NewOject( )创建的,到这里疑惑就解开了,bitmap对象是虚拟机创建的,JNIEnv的NewOject方法返回的是java对象,并不是native对象,所以它会分配到dalvik heap中。

11、java程序如何才能创建native对象

必须使用jni,而且应该用C语言的malloc或者C++的new关键字。实例代码如下:

[java] view plain copy
  1. JNIEXPORTvoidJNICALLJava_com_example_demo_TestMemory_nativeMalloc(JNIEnv*,jobject)
  2. {
  3. void*p=malloc(1024*1024*50);
  4. SLOGD("allocate50MBytesmemory");
  5. if(p!=NULL)
  6. {
  7. //memorywillnotusedwithoutcallingmemset()
  8. memset(p,0,1024*1024*50);
  9. }
  10. else
  11. SLOGE("mallocfailure.");
  12. ….
  13. ….
  14. free(p);//freememory
  15. }

或者:

[java] view plain copy
  1. JNIEXPORTvoidJNICALLJava_com_example_demo_TestMemory_nativeMalloc(JNIEnv*,jobject)
  2. {
  3. SLOGD("allocate50MBytesmemory");
  4. char*p=newchar[1024*1024*50];
  5. if(p!=NULL)
  6. {
  7. //memorywillnotusedwithoutcallingmemset()
  8. memset(p,1,1024*1024*50);
  9. }
  10. else
  11. SLOGE("newobjectfailure.");
  12. ….
  13. ….
  14. free(p);//freememory
  15. }

这里对代码中的memset做一点说明:

new或者malloc申请的内存是虚拟内存,申请之后不会立即映射到物理内存,即不会占用RAM,只有调用memset使用内存后,虚拟内存才会真正映射到RAM。


本文旨在让大家对Android内存管理有一个整体性的认识,着重全局性理解,希望对大家有用。

如果对java层内存泄漏感兴趣,可以阅读我的文章Android内存泄漏分析及调试

更多相关文章

  1. Android官方建议关于Android(安卓)APP UI界面设计的一些参考原则
  2. android自学笔记(1):android简介
  3. Android开发规范(编码+性能+UI)
  4. Android(安卓)Fresco图片加载库基础使用详解
  5. 进度圈的显示
  6. [Android(安卓)分享]GitHub Android(安卓)最火开源项目Top20
  7. 箭头函数的基础使用
  8. NPM 和webpack 的基础使用
  9. Python list sort方法的具体使用

随机推荐

  1. Android Map库
  2. 今日签到
  3. android Fragments (Android官方文档中文
  4. Dalvik虚拟机和ART(Android(安卓)RunTime
  5. Android adb不是内部或外部命令
  6. android从raw里面读取txt文件
  7. android直接找一个文件,部分编译等实用
  8. 详解Android中Intent对象与Intent Filter
  9. 通过浏览器直接打开Android应用程序
  10. android颜色设置