Android 希望提供世界上第一个完整的开放手机平台解决方案。它将以 Linux 为基础,这一点和德州仪器一样,但它同时也将提供其他所有必要的组件;手机厂商只需要用这一个系统就可以推出自己的设备。Android 还将为应用开发者提供统一的编程模型,这样他们的应用程序就可以在所有的设备上运行。通过采用统一的平台来支持所有设备,Android 能简化手机厂商和开发者的工作。------《Android 传奇》
前言:Linux 内存管理的重要性
内存 作为应用性能指标中非常重要的一项,会对应用软件的稳定性、流畅性产生直接影响。如果应用内存使用不当,会导致频繁发生 GC,而 JVM 的 GC 具有 Stop The World 的特性,这将引起包括 UI 在内的全部线程卡顿。更有甚者,无法回收的内存会耗尽系统分配给应用的可用空间,最终发生 OOM。典型的问题场景有内存泄漏、内存抖动等。
可以说,应用的大部分性能问题都可以归结在 内存 上,要想保证应用具有良好的用户体验,首当其冲要解决的就是内存问题。即使应用当前不存在这方面的问题,但是随着功能的不断迭代,架构也必然逐渐腐化,如果没有全面完善的内存监控和预警机制,就难以做到在内存问题发生之前,防患于未然。
对于内存问题,我们不仅要知其然,还要知其所以然,Android 底层是基于 Linux 实现的,其内存机制也是脱胎于经典的 Linux 内存管理。
此外,在本文中,还将介绍几个很有用的 shell 命令,包含 dumpsys meminfo、cat /proc/[pid]/status 等等,用好这些命令,会是内存分析过程中的一大助力。
备注:本文所有的命令都省略了开头的
adb shell。
dumpsys meminfo:查看进程当前的内存使用情况
在电脑控制台执行 adb shell dumpsys meminfo [packageName],可以查看该进程当前内存使用情况。
bash
λ adb shell dumpsys meminfo com.google.samples.apps.nowinandroid.demo.debug
Applications Memory Usage (in Kilobytes):
Uptime: 655869283 Realtime: 912305084
** MEMINFO in pid 22144 [com.google.samples.apps.nowinandroid.demo.debug] **
Pss Private Private SwapPss Rss Heap Heap Heap
Total Dirty Clean Dirty Total Size Alloc Free
------ ------ ------ ------ ------ ------ ------ ------
Native Heap 11347 11336 0 9038 12196 31844 20762 7264
Dalvik Heap 9079 9060 0 1768 9740 108170 9866 98304
[下略]
在 Private Dirty 列中,记录了 Native 和 Dalvik(Java)内存使用量,而 dirty 的含义是什么?以及 clean, Rss, Pss, Swat 分别又代表了什么?
虚拟内存(VMA)与物理内存(Physical Memory)
这些都是 Linux 环境下内存管理的概念,站在内核的视角下,内存分配的最小粒度是 pages(分页),一个 page 大小是 4KB,这是 Linux 内核的基础页大小,是内存管理的最小单位,mmap()/malloc()/匿名页 最终都是按照 4KB 进行对齐。pages 在 "虚拟内存区(Virtual Memory Area,VMA)" 中进行管理和组织,它跟真实内存区是一一对应的,通过 VMA,可以将碎片化的真实内存映射为连续的虚拟内存。
应用进程并不通过 mmap() 函数直接声明内存占用,而是调用 malloc() ------native 代码,和 new() ------Java 代码来进行申请。
有两种类型的"虚拟内存区":
- 基于文件的 VMA :底层使用文件实现,通过将文件描述符(fd)提供给
mmap()函数,将在 VMA 上的读写操作映射为文件读写。在 Linux 系统中,动态链接器(ld)在运行新进程、动态加载库时,会使用基于文件的 VMA。Android 系统则在加载 dex 文件和资源时使用这一方式。 - 匿名 VMA :非文件系统,只通过内存实现。调用
mmap(... MAP_ANONYMOUS ...)会使用这种方式。动态申请的内存统统归于此类,例如 C 的malloc()、C++ 的new()和 Java 的new X()------ 这也是我们要关注的重点。
应用从虚拟内存中申请内存,最终会映射到物理内存上,前者往往大于后者。我们统计内存占用时,通常只关注物理内存的大小,忽略虚拟内存。例如,系统分配给应用32MB 的虚拟内存,应用随后向其中写入 4KB 数据,实际上只占用了 4KB 的物理内存。在64位的系统上,虚拟内存地址很难耗尽,绝大多数场景下无需关注。
RSS 与 PSS
应用占用的物理内存大小,称为 "驻留内存大小(Resident Set Size,RSS)"。
有了以上基础知识,接下来就可以对虚拟内存 VMA 中的分页进行归类了,这也就对应着前面 dumpsys meminfo 指令的输出结果。
- 驻留状态(Resident) :该分页已经被映射到物理内存,可细分为两类。
- 干净(Clean):分页内容与底层文件中的内容相同,在这种情况下,内核可以自由清除分页内容,而不必担心数据丢失。
- 脏(Dirty):分页内容与底层文件内容不相同,在大多数情况下甚至不存在底层映射文件。此时,不允许内核清除分页内容。
- 交换状态(Swapped):脏分页被写入到交换文件(在 Android 中是 ZRAM),这是一个临时状态,当下次缺页异常(page fault)发生时,会将这部分内容重新转移到内存。
- 未占用状态(Not present):该分页从未使用过。
在 Android 系统中,应当重点关注脏分页 Private Dirty,因为脏分页无法被转移到文件系统,因此不能被内核回收再利用。
作为操作系统,Android 对于多个应用都要使用到的库(例如 libc.so、framework.dex)进行了优化,系统将它们作为共享资源进行加载。这些数据最初属于 zygote 进程,当系统 fork() 新的应用进程时,使用的是相同的页面地址。同一时刻即使系统里运行了 100 个应用,在这部分上公有库占用的内存仍然只有一份。
因此引入了 "按比例内存占用(Proportional Set Size,PSS)" 的概念,对于共享内存按比例划分。例如我们将 4KB 的分页共享给4条进程,每一条进程占用的内存则是 1KB。
cat /proc/[pid]/status:查看应用启动以来 RSS 的最大值
前文介绍过,dumpsys meminfo 可以返回应用当前的详细内存使用状态,如果说我们想要跟踪自应用进程启动以来,所申请过的最大 RSS,则可以通过 cat /proc/[pid]/status 来查看。
bash
λ adb shell cat /proc/19947/status
[略]
VmPeak: 37292428 kB
VmSize: 32416288 kB
VmLck: 192 kB
VmPin: 0 kB
VmHWM: 1187096 kB <=== RSS High Watermark
VmRSS: 521832 kB
RssAnon: 122076 kB
RssFile: 362528 kB
RssShmem: 37228 kB
VmData: 1669512 kB
VmStk: 8192 kB
VmExe: 8 kB
VmLib: 224720 kB
VmPTE: 5572 kB
VmSwap: 258472 kB
[略]
VmHWM 记录了进程自启动以来,最大的瞬时 RSS 值,可以看到高达 1187M。
使用 Perfetto 记录内存(RSS)增长曲线
配置文件 config.pbtx
plaintext
buffers: {
size_kb: 8960
fill_policy: DISCARD
}
buffers: {
size_kb: 1280
fill_policy: DISCARD
}
data_sources: {
config {
name: "linux.process_stats"
target_buffer: 1
process_stats_config {
scan_all_processes_on_start: true
}
}
}
data_sources: {
config {
name: "linux.ftrace"
ftrace_config {
ftrace_events: "mm_event/mm_event_record"
ftrace_events: "kmem/rss_stat"
ftrace_events: "kmem/ion_heap_grow"
ftrace_events: "kmem/ion_heap_shrink"
}
}
}
duration_ms: 30000
使用命令 cat config.pbtx | adb shell perfetto -c - --txt -o /data/misc/perfetto-traces/trace.pftrace 开启抓取,操作界面,30s 后结束抓取。
这里我采用官方给出的配置并未能正常抓取 trace 文件,只能获取到一个大约 830KB 的空文件。先采用官方的图片作为讲解,如果有读者找到问题原因,欢迎不吝赐教。

从这个图可以看出,在大约时间进行到2/3时,mem.rss.ion 发生增长,说明此时用户操作引起了内存分配。
am dumpheap [packageName]:抓取 JVM 内存快照
bash
λ adb shell am dumpheap com.google.samples.apps.nowinandroid.demo.debug
File: /data/local/tmp/heapdump-20251212-093630.prof
Waiting for dump to finish...
抓取对应包名的 prof 文件后,将其 pull 到电脑上,改名为 *.hprof后,就可以在 AndroidStudio 中查看当前进程的 JVM 内存快照。

也可以用 Perfetto 在线打开该快照,会以火焰图的方式展示对象占用堆大小,对于类型相同的对象聚合后展示,优点是更加直观,缺点是无法查看对象内部数据。

showmap [pid]:展示内存-文件映射
底层通过读取 /proc/PID/smaps 文件来实现。
首先获取 NowInAndroid 的 PID=19147。
bash
λ adb shell ps -A | grep nowinandroid
u0_a225 19147 996 28342616 303680 do_epoll_wait 0 S com.google.samples.apps.nowinandroid.demo.debug
随后查看其内存中的基于文件的 VMA 映射。
输入:
bash
λ adb shell showmap 19147
输出:
在下文中,输出内容中已经省略去了大部分,原本的映射文件数量有869个。这里面有 jar 包、so 库、ttf 字体文件、dex 文件等。可以说,占据大多数的是应用使用到的资源文件。
虽然数量多,但由于是"共享"的原因,真正的 PSS 和 private dirty 并不多。
| 区域 | 大小(b) | 备注 |
|---|---|---|
| RSS | 296680 | RSS = shared clean + shared dirty + private clean + private dirty |
| PSS | 131754 | |
| shared dirty | 34224 | |
| private dirty | 55536 |
plaintext
virtual shared shared private private Anon Shmem File Shared Private
size RSS PSS clean dirty clean dirty swap swapPSS HugePages PmdMapped PmdMapped Hugetlb Hugetlb Locked # object
-------- -------- -------- -------- -------- -------- -------- -------- -------- --------- --------- --------- -------- -------- -------- ---- ------------------------------
292 96 19 88 0 8 0 8 0 0 0 0 0 0 0 1 /apex/com.android.adbd/lib64/libadbconnection_client.so
492 184 8 184 0 0 0 0 0 0 0 0 0 0 0 1 /apex/com.android.adservices/javalib/framework-adservices.jar
1172 208 97 144 0 64 0 0 0 0 0 0 0 0 0 1 /apex/com.android.art/javalib/apache-xml.jar
72 60 32 0 56 0 4 12 0 0 0 0 0 0 0 1 /memfd:gralloc_shared_memory (deleted)
65536 0 0 0 0 0 0 12 0 0 0 0 0 0 0 1 /memfd:jit-zygote-cache (deleted)
8 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 /product/overlay/GmsConfigOverlayCommon.apk
24 20 0 20 0 0 0 4 0 0 0 0 0 0 0 1 /system/bin/app_process64
2716 988 777 384 0 604 0 0 0 0 0 0 0 0 0 1 /system/fonts/NotoColorEmoji.ttf
[中间略去大部分]
36 36 36 0 0 36 0 0 0 0 0 0 0 0 0 1 /system/fonts/NotoSansSymbols-Regular-Subsetted2.ttf
48 32 2 32 0 0 0 4 0 0 0 0 0 0 0 1 /system/framework/arm64/boot-mediatek-ims-base.oat
4 4 0 4 0 0 0 0 0 0 0 0 0 0 0 1 /system/framework/boot-vivo-vgcclient.vdex
700 264 19 260 0 4 0 4 0 0 0 0 0 0 0 1 /vendor/lib64/egl/libMEOW_gift.so
-------- -------- -------- -------- -------- -------- -------- -------- -------- --------- --------- --------- -------- -------- -------- ---- ------------------------------
virtual shared shared private private Anon Shmem File Shared Private
size RSS PSS clean dirty clean dirty swap swapPSS HugePages PmdMapped PmdMapped Hugetlb Hugetlb Locked # object
-------- -------- -------- -------- -------- -------- -------- -------- -------- --------- --------- --------- -------- -------- -------- ---- ------------------------------
28342616 296680 131754 143316 34224 63604 55536 30336 14411 0 0 0 0 0 0 869 TOTAL
getprop dalvik.vm.heapgrowthlimit 和 dalvik.vm.heapsize:设备分配给应用的内存上限
在不同硬件的设备上,应用能够使用的最大内存值不一样,这一般是由 ROM 厂商决定。通常来说,硬件越好,应用内存上限也越高。可以通过 adb 命令查看设备的分配给应用的内存上限。这两个值是写死在 /system/build.prop 文件当中的。
在控制台获取这两个值:
bash
λ adb shell getprop dalvik.vm.heapgrowthlimit
256m
λ adb shell getprop dalvik.vm.heapsize
512m
在应用代码,可以使用 ActivityManager 提供的静态函数获取这两个值,其实现本质上也是访问上述系统参数。
java
// ActivityManager.java
// 获取 heapgrowthlimit
static public int staticGetMemoryClass() {
// Really brain dead right now -- just take this from the configured
// vm heap size, and assume it is in megabytes and thus ends with "m".
String vmHeapSize = SystemProperties.get("dalvik.vm.heapgrowthlimit", "");
if (vmHeapSize != null && !"".equals(vmHeapSize)) {
return Integer.parseInt(vmHeapSize.substring(0, vmHeapSize.length()-1));
}
return staticGetLargeMemoryClass();
}
// 获取 heapsize
static public int staticGetLargeMemoryClass() {
// Really brain dead right now -- just take this from the configured
// vm heap size, and assume it is in megabytes and thus ends with "m".
String vmHeapSize = SystemProperties.get("dalvik.vm.heapsize", "16m");
return Integer.parseInt(vmHeapSize.substring(0, vmHeapSize.length() - 1));
}
如何理解这两个值的含义?
heapgrowthlimit:应用默认允许使用的 dalvik/arm 虚拟机内存上限。heapsize:在声明largeHeap="true"后,应用可以扩展到的内存上限。
已前文我使用的手机为例,其 heapgrowthlimit/heapsize 是 256m/512m,这意味着,应用在运行时,最多可以申请 256m 的内存,当实时内存接近(未超过)这个值,JVM 会频繁触发 GC 以回收内存,伴随着大量的 STW。而一旦内存使用超过这个值,则会发生 OOM 崩溃。
xml
<application
android:largeHeap="false"/>
那可能就会有聪明的同学提问,是否可以在 AndroidManifext.xml 里开启 largeHeap 续一波命?通常来说这并不是一个好的选择,站在系统的角度,largeHeap 是用来提供给某些需要大内存的应用显式声明,例如相册、相机、编辑器、短视频等。如果你的应用不属于此类,还是建议从代码上寻找原因,做好内存的管控。
