预备知识

页:

我们把磁盘分很多块,我们把这个块称为页(Memory paging),页同时也是硬盘和内存最小的交换单位(一般为4k).

如果你想了解更多请参阅wikipedia: Memory paging(英文)

内存映射文件(Memory-mapped file)

假设我有一个exe程序要运行,这个exe大小为5g,那么操作系统会将这个文件装载到内存(当然不会一次性装载完.).我们把这种操作称为内存映射文件(Memory-mapped file).

共享内存

操作系统很多资源是可用让多个程序共享的,比如动态库代码.在Android的Zygote在进行fork出一个app时,其内部包含很多共享的主题和资源等.在程序没有修改这些共享资源的时候,所有的程序的共享资源指向同一物理内存.

Android 映射调用私有方法_Android 映射调用私有方法

假设进程B修改动态库的内容,那么会将会拷贝动态库作为一个副本,然后在在副本里面修改.

Android 映射调用私有方法_Android 映射调用私有方法_02

Swap space

当内存不足时候linux操作会执行内存交换的操作,大白话将就是一些不重要的程序的内存换出到硬盘,腾出的内存空间给新任务使用,而这块空间在linux我们把它称为交换空间(Swap space).等不重要的程序被重新调度时在将硬盘数据重新载入,但是成本极高. Android虽然是linux,但是在内存不足的时候并不会执行内存交换操作,主要原因手机存储比较小,另外频繁的读取容易对多媒体存储硬件带来损失以及带来大量迁移操作会引起卡顿.

swap-space-linux-systems(英文)

VSS/RSS/PSS/USS

这四个是一个程序内存区域的一个术语

Vss = virtual set size

Rss = resident set size

Pss = proportional set size

Uss = unique set size

vss:虚拟空间大小,操作系统会让程序认为自己可以掌握整个内存,所以虚拟一个完整内存空间给程序,表示整个程序内存,实际和物理内存需要做一次地址转化.(这里是操作系统基础知识不在这里铺开)

Rss:程序实际占用的物理内存,包含共享内存的大小(比如加载动态就是一个共享内存),所以不方便查看实际自身所占用的内存

Uss:不包含共享内存的程序大小

Pss:两个内存区域大小的结合,第一个内存区域是自身非共享内存的大小(Uss),另一个共享内存所占的平均值,注意是平均值,这个平均值计算:假设共享内存为 4MB,然后这个共享内存被两个程序加载,那么 4MB/2=2MB.平均值是2MB.

Pss是我们最常拿来做内存分析指标.

举例:

假设有程序A和程序B都被程序加载,操作分配一个虚拟存储空间给他们操作,大小为4G(这个就是Vss),

然后程序A和程序B此时实际使用了内存了2G(Uss).

程序A和程序B加载同一动态库xxx.so,而xxx.so大小为1G,那么程序A和程序B的Rss就是3G

计算xxx.so平均值为 1G/2=0.5G.所以程序的Pss的为2.5G

举例2:

Android提供procrank方便我们查看我们的所有程序(默认以Pss倒序排序)

Android 映射调用私有方法_Android 映射调用私有方法_03

脏页和干净页

页的概念前面已经讲解过,而内存映射文件(Memory-mapped file)也是页单位进行映射的.假设某个代码在A页,然后修改A页里面的变量,那么这个页叫做脏页.对于共享内存也是同理.

Android 内存管控机制

详细权威的可以查看官网 进程间的内存分配

下图是一个典型的Android内存模式,RAM是内存,zRAM也是RAM,的一部分,而Storage可以理解为我们的硬盘.

Android 映射调用私有方法_Memory_04

可能zRAM大家并不了解,这里大致说下作用,当RAM不足时,会将部分在RAM内容压缩后放入zRAM,等内存充足时解压zRAM在放回RAM.

Android 映射调用私有方法_Android_05

内存不足策略1:kswapdkswapd是linux的一个守护进程用于在内存不足将干净页进行回收好腾出内存空间,而脏页则会进行zRAM压缩.这里非常好理解,如果是干净页那么需要时再从文件读取即可,而脏页因为修改过了和硬盘文件的不符合,无法从新读取.

内存不足策略2:LMKkswapd的回收和压缩机制并不能解决所有问题,所以Android还有另一个守护进程低内存终止守护进程(LMK/Low Memory Killer))进行跟进一步的内存回收.LMK会根据一个叫OOM_ADJ_SCORE得到所有进程的优先级.(数值越低越不容易被杀死).

你可以通过:

cat proc/{pid}/oom_adj来查看自己进程的优先级

Android 映射调用私有方法_共享内存_06

oom_adj的数值一般根据以下图排序(高到低,越下面越不容易被杀死):

Android 映射调用私有方法_Memory_07

图来自官方文档,下面的说明简单修改官网文档

以下是上表中各种类别的说明:

  • 后台应用:之前运行过且当前不处于活动状态的应用。LMK 将首先从具有最高 oom_adj_score 的应用开始终止后台应用。
  • 上一个应用:最近用过的后台应用。上一个应用比后台应用具有更高的优先级(得分更低),因为相比某个后台应用,用户更有可能切换到上一个应用。
  • 主屏幕应用:这是启动器应用。终止该应用会使壁纸消失。
  • 服务:服务由应用启动,可能包括同步或上传到云端。
  • 可觉察的应用:用户可通过某种方式察觉到的非前台应用,例如运行一个显示小界面的搜索进程或听音乐。可以简单理解用户可见APP,但是用户没有直接交互,比如有一个通知栏正在播放音乐
  • 前台应用:当前正在使用的应用。终止前台应用看起来就像是应用崩溃了,可能会向用户提示设备出了问题。
  • 持久性(服务):这些是设备的核心服务,例如电话和 WLAN。
  • 系统:系统进程。这些进程被终止后,手机可能看起来即将重新启动。
  • 原生:系统使用的极低级别的进程(例如,kswapd)。

在进行LMK时会多次调用onTrimMemory(level: Int)函数,参数level告诉当前内存紧张状态,你可以通过level的状态进行内存回收,否则你的app将被杀死.如果对于API 14的应用可以使用onLowMemory(),相当于onTrimMemory传入一个TRIM_MEMORY_COMPLETE(当前已经准备扫描完所有LRU列表进程,如果内存还不足将杀死你的进程). 另外这里有一个注意点,你不应该直接==判断level,应该使用>=或者<=进行操作,因为android未来没准插入新的状态.

  • 一个小Demo:
override fun onTrimMemory(level: Int) {
        super.onTrimMemory(level)
        //TRIM_MEMORY_UI_HIDDEN 用于告诉你,你应该回收ui视图,
        if (level>= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN ) {
            rootView.removeAllViews()
        }
    }

常用技巧

getMemoryInfo()

MemoryInfo可以通过getMemoryInfo获取,通过MemoryInfo我们更加细腻度掌握内存状态.

Demo(来自google官方):

public void doSomethingMemoryIntensive() {

        // Before doing something that requires a lot of memory,
        // check to see whether the device is in a low memory state.
        ActivityManager.MemoryInfo memoryInfo = getAvailableMemory();

        if (!memoryInfo.lowMemory) {
            // Do memory intensive work ...
        }
    }

    // Get a MemoryInfo object for the device's current memory status.
    private ActivityManager.MemoryInfo getAvailableMemory() {
        ActivityManager activityManager = (ActivityManager) this.getSystemService(ACTIVITY_SERVICE);
        ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
        activityManager.getMemoryInfo(memoryInfo);
        return memoryInfo;
    }

生成HEAP DUMP

am命令

使用pid:am dumpheap {pid} /data/local/tmp/fileName.hprof 使用package:am dumpheap {package} /data/local/tmp/fileName.hprof

代码

android.os.Debug.dumpHprofData("/sdcard/dump.hprof");

以上生成的heap dump可以拖入as自带的profiler进行分析

Android 映射调用私有方法_Memory_08

但是如果想使用JAVA之类的工具分析(MAT,jprofiler)需要hprof-conv工具做下转化.

工具位于SDK目录:sdk/platform-tools/hprof-conv

  • 使用demo:hprof-conv in.hprof out.hprof

DDMS卡死等问题

ddms卡死jdk版本解决办法

Android 映射调用私有方法_Android_09

参考

Android Memory Usage(英文)(注意pss描述不正确)内存耗用:VSS/RSS/PSS/USS 的介绍dumpsys文档进程间的内存分配