内存管理基础

App内存组成以及限制

Android 给每个 App 分配一个 VM ,让App运行在 dalvik 上,这样即使 App 崩溃也不会影响到系统。系统给 VM 分配了一定的内存大小, App 可以申请使用的内存大小不能超过此硬性逻辑限制,就算物理内存富余,如果应用超出 VM 最大内存,就会出现内存溢出 crash。

由程序控制操作的内存空间在 heap 上,分 java heapsize 和 native heapsize

  • Java申请的内存在 vm heap 上,所以如果 java 申请的内存大小超过 VM 的逻辑内存限制,就会出现内存溢出的异常
  • native层内存申请不受其限制, native 层受 native process 对内存大小的限制

如何查看Android设备对App的内存限制

  1. 主要查看系统配置文件 build.prop ,我们可以通过 adb shell 在 命令行窗口查看
adb shell cat /system/build.prop

image-20220819150211884

image-20220819150239269

  1. 通过代码获取

    ActivityManager activityManager = (ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE) activityManager.getMemoryClass();//以m为单位
  2. 可以修改

    • 修改 \frameworks\base\core\jni\AndroidRuntime.cpp

      int AndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv, bool zygote) 
      {
      /*

      * The default starting and maximum size of the heap. Larger
      * values should be specified in a product property override.
      */parseRuntimeOption("dalvik.vm.heapstartsize", heapstartsizeOptsBuf, "-Xms", "4m"); parseRuntimeOption("dalvik.vm.heapsize", heapsizeOptsBuf, "-Xmx", "16m");//修改这里
      * }
    • 修改 platform/dalvik/+/eclair-release/vm/Init.c

      gDvm.heapSizeStart = 2 * 1024 * 1024; // Spec says 16MB; too big for us. 
      gDvm.heapSizeMax = 16 * 1024 * 1024; // Spec says 75% physical mem

内存指标概念

Item 全称 含义 等价
USS Unique Set Size 物理内存 进程独占的内存
PSS Proportional Set Size 物理内存 PSS= USS+ 按比例包含共享库
RSS Resident Set Size 物理内存 RSS= USS+ 包含共享库
VSS Virtual Set Size 虚拟内存 VSS= RSS+ 未分配实际物理内存

总结:VSS >= RSS >= PSS >= USS,但/dev/kgsl-3d0部份必须考虑VSS,一般我们只需要考虑PSS

Android内存分配与回收机制

这部分内容小编已经在之前的章节介绍了

附上链接:JVM内存管理剖析

Android低内存杀进程机制

Anroid基于进程中运行的组件及其状态规定了默认的五个回收优先级:

image-20220819151608768

  • Empty process(空进程)

  • Background process(后台进程)

  • Service process(服务进程)

  • Visible process(可见进程)

  • Foreground process(前台进程)

系统需要进行内存回收时最先回收空进程,然后是后台进程,以此类推最后才会回收前台进程(一般情况

下前台进程就是与用户交互的进程了,如果连前台进程都需要回收那么此时系统几乎不可用了)。

ActivityManagerService 会对所有进程进行评分(存放在变量adj中),然后再讲这个评分更新到内

核,由内核去完成真正的内存回收( lowmemorykiller , Oom_killer )。

什么是OOM

OOM(OutOfMemoryError)内存溢出错误,在常见的Crash疑难排行榜上,OOM绝对可以名列前茅并且经久不衰。因为它发生时的Crash堆栈信息往往不是导致问题的根本原因,而只是压死骆驼的最后一根稻草

发生OOM的条件

  • Android 2.x系统 GC LOG中的dalvik allocated + external allocated + 新分配的大小 >=

    getMemoryClass()值的时候就会发生OOM。 例如,假设有这么一段Dalvik输出的GC LOG:

    GC_FOR_MALLOC free 2K, 13% free 32586K/37455K, external 8989K/10356K, paused 20ms,

    那么32586+8989+(新分配23975)=65550>64M时,就会发生OOM。

  • Android 4.x系统 Android 4.x的系统废除了external的计数器,类似bitmap的分配改到dalvik的

    java heap中申请,只要allocated + 新分配的内存 >= getMemoryClass()的时候就会发生OOM

OOM原因

image-20220819152424482

对于应用开发来说,其实只需要关心上述第1、2点以及4即可。

Android内存分析命令介绍

常用的内存调优分析命令:

  1. dumpsys meminfo
  2. procrank
  3. cat /proc/meminfo
  4. free
  5. showmap
  6. vmstat

dumpsys meminfo

命令:

adb shell dumpsys meminfo [App文件名]

通常可以通过比较Heap Size以及Objects数据的增加来判断发生了内存泄漏

操作方法:首先执行一次dumpsys meminfo,然后再App的各项Activity或者不同App之间跳转,最后再切回来,再执行一次dumpsys meminfo,比较数据变化。

image-20220819154734084

image-20220819161522100

虽然参数很多,但是我们主要关注Dalvik Heap与TOTAL这两行的内容即可。

相关参数的说明:

Pss Total:是一个进程实际使用的内存,该统计方法包括比例分配共享库占用的内存,即如果有三个进

程共享了一个共享库,则平摊分配该共享库占用的内存。Pss Total统计方法的一个需要注意的地方是如

果使用共享库的一个进程被杀死,则共享库的内存占用按比例分配到其他共享该库的进程中,而不是将

内存资源返回给系统,这种情况下PssTotal不能够准确代表内存返回给系统的情况。

Private Dirty:进程私有的脏页内存大小,该统计方法只包括进程私有的被修改的内存。

Private Clear:进程私有的干净页内存大小,该统计方法只包括进程私有的没有被修改的内存。

Swapped Dirty:被交换的脏页内存大小,该内存与其他进程共享。

其中private Dirty + private Clean = Uss,该值是一个进程的使用的私有内存大小,即这些内存唯一被

该进程所有。该统计方法真正描述了运行一个进程需要的内存和杀死一个进程释放的内存情况,是怀疑

内存泄露最好的统计方法。

共享比例:sharing_proportion = (Pss Total - private_clean - private_dirty) / (shared_clean +

shared_dirty)

享学课堂能够被共享的内存:swappable_pss = (sharing_proportion * shared_clean) + private_clean

Native Heap:本地堆使用的内存,包括C/C++在堆上分配的内存

Dalvik Heap:dalvik虚拟机使用的内存

Dalvik other:除Dalvik和Native之外分配的内存,包括C/C++分配的非堆内存

Cursor:数据库游标文件占用的内存

Ashmem:匿名共享内存

Stack:Dalvik栈占用的内存

Other dev:其他的dev占用的内存

.so mmap:so库占用的内存

.jar mmap:.jar文件占用的内存

.apk mmap:.apk文件占用的内存

.ttf mmap:.ttf文件占用的内存

.dex mmap:.dex文件占用的内存

image mmap:图像文件占用的内存

code mmap:代码文件占用的内存

Other mmap:其他文件占用的内存

Graphics:GPU使用图像时使用的内存

GL:GPU使用GL绘制时使用的内存

Memtrack:GPU使用多媒体、照相机时使用的内存

Unknown:不知道的内存消耗

Heap Size:堆的总内存大小

Heap Alloc:堆分配的内存大小

Heap Free:堆待分配的内存大小

Native Heap | Heap Size : 从mallinfo usmblks获的,当前进程Native堆的最大总共分配内存

Native Heap | Heap Alloc : 从mallinfo uorblks获的,当前进程navtive堆的总共分配内存

Native Heap | Heap Free : 从mallinfo fordblks获的,当前进程Native堆的剩余内存

Native Heap Size ≈ Native Heap Alloc + Native Heap Free

mallinfo是一个C库,mallinfo()函数提供了各种各样通过malloc()函数分配的内存的统计信息。

Dalvik Heap | Heap Size : 从Runtime totalMemory()获得,Dalvik Heap总共的内存大小

Dalvik Heap | Heap Alloc : 从Runtime totalMemory() - freeMemory()获得,Dalvik Heap分配的内存大小

Dalvik Heap | Heap Free : 从Runtime freeMemory()获得,Dalvik Heap剩余的内存大小

Dalvik Heap Size = Dalvik Heap Alloc + Dalvik Heap Free

Obejcts当前进程中的对象个数

Views:当前进程中实例化的视图View对象数量

ViewRootImpl:当前进程中实例化的视图根ViewRootImpl对象数量

AppContexts:当前进程中实例化的应用上下文ContextImpl对象数量

Activities:当前进程中实例化的Activity对象数量

Assets:当前进程的全局资产数量

AssetManagers:当前进程的全局资产管理数量

Local Binders:当前进程有效的本地binder对象数量

Proxy Binders:当前进程中引用的远程binder对象数量

Death Recipients:当前进程到binder的无效链接数量

OpenSSL Sockets:安全套接字对象数量

procrank

功能: 获取所有进程的内存使用的排行榜,排行是以 Pss 的大小而排序。 procrank 命令比 dumpsys meminfo 命令,能输出更详细的VSS/RSS/PSS/USS内存指标。

cat /proc/meminfo

功能:能否查看更加详细的内存信息

指令:

adb shell cat /proc/meminfo

输出结果如下(结果内存值不带小数点,此处添加小数点的目的是为了便于比对大小):

root@phone:/ # cat /proc/meminfo 
MemTotal: 2857.032 kB //RAM可用的总大小 (即物理总内存减去系统预留和内核二进 制代码大小)
MemFree: 1020.708 kB //RAM未使用的大小
Buffers: 75.104 kB //用于文件缓冲
Cached: 448.244 kB //用于高速缓存
SwapCached: 0 kB //用于swap缓存
Active: 832.900 kB //活跃使用状态,记录最近使用过的内存,通常不回收用于其它目的
Inactive: 391.128 kB //非活跃使用状态,记录最近并没有使用过的内存,能够被回 收用于其他目的
Active(anon): 700.744 kB //Active = Active(anon) + Active(file)
Inactive(anon): 228 kB //Inactive = Inactive(anon) + Inactive(file)
Active(file): 132.156 kB
Inactive(file): 390.900 kB
Unevictable: 0 kB
Mlocked: 0 kB
SwapTotal: 524.284 kB //swap总大小
SwapFree: 524.284 kB //swap可用大小
Dirty: 0 kB //等待往磁盘回写的大小
Writeback: 0 kB //正在往磁盘回写的大小
AnonPages: 700.700 kB //匿名页,用户空间的页表,没有对应的文件
Mapped: 187.096 kB //文件通过mmap分配的内存,用于map设备、文件或者库
Shmem: .312 kB
Slab: 91.276 kB //kernel数据结构的缓存大小,
Slab=SReclaimable+SUnreclaim
SReclaimable: 32.484 kB //可回收的slab的大小
SUnreclaim: 58.792 kB //不可回收slab的大小
KernelStack: 25.024 kB

PageTables: 23.752 kB //以最低的页表级
NFS_Unstable: 0 kB //不稳定页表的大小
Bounce: 0 kB
WritebackTmp: 0 kB
CommitLimit: 1952.800 kB
Committed_AS: 82204.348 kB //评估完成的工作量,代表最糟糕case下的值,该值也包含 swap内存
VmallocTotal: 251658.176 kB //总分配的虚拟地址空间
VmallocUsed: 166.648 kB //已使用的虚拟地址空间
VmallocChunk: 251398.700 kB //虚拟地址空间可用的最大连续内存块

对于cache和buffer也是系统可以使用的内存。所以系统总的可用内存为 MemFree+Buffers+Cached

free

主功能:查看可用内存,缺省单位KB。该命令比较简单、轻量,专注于查看剩余内存情况。数据来源于/proc/meminfo。

总结

其实小编使用过的就只有dumpsys meminfocat /proc/meminfo其他的实用情况不多

  1. dumpsys meminfo 适用场景: 查看进程的oom adj,或者dalvik/native等区域内存情况,或者某

个进程或apk的内存情况,功能非常强大;

  1. procrank 适用场景: 查看进程的VSS/RSS/PSS/USS各个内存指标;

  2. cat /proc/meminfo 适用场景: 查看系统的详尽内存信息,包含内核情况;

  3. free 适用场景: 只查看系统的可用内存;

  4. showmap 适用场景: 查看进程的虚拟地址空间的内存分配情况;

  5. vmstat 适用场景: 周期性地打印出进程运行队列、系统切换、CPU时间占比等情况;

Android内存泄漏分析工具

MAT

这个需要在官网下载

Android Studio Memory-profiler

就在Android Studio下方菜单栏中

具体用法可参考官网:

https://developer.android.google.cn/studio/profile/memory-profiler#performance

LeakCanary

检测内存泄漏非常方便的工具

只需要导入,就可以在运行界面查看定位到的内存泄漏位置,以及依赖树

https://github.com/square/leakcanary

Android内存泄漏常见场景以及解决方案

1、资源性对象未关闭

对于资源性对象不再使用时,应该立即调用它的close()函数,将其关闭,然后再置为null。例如Bitmap等资源未关闭会造成内存泄漏,此时我们应该再Activity销毁时关闭。

2、注册对象未销毁

例如BroadcastReceiver、EventBus未注销造成的内存泄漏,我们应该在Activity销毁时及时注销。

3、类的静态变量持有大数据对象

尽量避免使用静态变量存储数据,特别是大数据对象,建议使用数据库存储。

4、单例造成的内存泄漏

优先使用Application的Context,如需使用Activity的Context,可以在传入Context时使用弱引用进行封装,然后,在使用到的地方从弱引用中获取Context,如果获取不到,则直接return即可。

5、非静态内部类的静态实例

该实例的生命周期和应用一样长,这就导致该静态实例一直持有该Activity的引用,Activity的内存资源不能正常回收。此时,我们可以将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用Context,尽量使用Application Context,如果需要使用Activity Context,就记得用完后置空让GC可以回收,否则还是会内存泄漏。

Handler临时性内存泄漏

Message发出之后存储在MessageQueue中,在Message中存在一个target,它是Handler的一个引用,Message在Queue中存在的时间过长,就会导致Handler无法回收。如果Handler是非静态的,则会导致Activity或者Service不会被回收。并且消息队列是在一个Looper线程中不断地处理消息,当这个Activity退出时,消息队列中还有未处理的消息或者正在处理的消息,并且消息队列中的Message持有Handler实例的引用,Handler持有Activity的引用,所以导致Activity的内存资源无法及时回收,引发内存泄漏。解决方案如下:

  • 1、使用一个静态Handler内部类,然后对Handler持有的对象(一般是Activity)使用弱引用,这样在回收时,也可以回收Handler持有的对象。
  • 2、在Activity的Destroy或者Stop时,应该移除消息队列中的消息,避免Looper线程的消息队列中有待处理的消息需要处理。

需要注意的是,AsyncTask内部也是Handler机制,同样存在内存泄漏风险,但其一般是临时性的。对于
类似AsyncTask或是线程造成的内存泄漏,我们也可以将AsyncTask和Runnable类独立出来或者使用静态内部类。

7、容器中的对象没清理造成的内存泄漏

在退出程序前,将集合里的东西clear,然后置为null,再退出程序。

8、WebView

WebView都存在内存泄漏的问题,在应用中只要使用一次WebView,内存就不会被释放掉。我们可以为WebView开启一个独立的进程,使用AIDL与应用的主进程进行通信,WebView所在的进程可以根据业务的需要选择合适的时机进行销毁,达到正常释放内存的目的。

9、使用ListView造成的内存泄漏

在构造Adapter时,使用缓存的convertView。