资讯动态

用 adb shell 排查 Android 内存:Java堆、VSS/PSS 与 hprof 实战指南

发布时间:2026/10/1 6:25:53 来源:尧图企业网站定制
有段时间我排查线上闪退logcat 里清一色 OutOfMemoryErrorAndroid Studio 连不上那台测试机抓不了 Memory Profiler整个人急得像热锅上的蚂蚁。后来硬着头皮打开终端用 adb shell 加一串命令把 Java 堆、VSS、PSS、hprof 快照全部扒了一遍才把问题定位到一张 5000×3000 的启动大图上。那次之后我算是想明白了Android Studio 的图形化工具好用但真正到灰头土脸排线上问题的时候adb shell 才是最保底的那套吃饭家伙。这篇文章不聊花架子就围绕一条主线用 adb shell 怎么把 App 的 Java 堆内存、VSS 虚拟内存、详细内存状况、hprof 快照以及系统可用内存全部查明白。内容主要来自我自己项目里的真实操作工具原理和坑都会一并交代适合正在做性能优化、内存排查、崩溃分析的 Android 开发也适合测试同学想自己定位内存问题。1. adb 连接与内存排查的前置准备在敲任何一条内存命令之前先把环境和思路理顺。这步如果出了岔子后面全是白费功夫。1.1 adb 工具链与设备连接的几种形态adb 全称 Android Debug Bridge是 Android SDK 平台工具里的核心命令。我在 macOS 和 Linux 上常用的是通过包管理器装的 platform-toolsWindows 上就直接用 Android Studio 自带的 SDK 目录下的 platform-tools。确认 adb 可用的方式很简单adb version只要能看到版本号输出就说环境没问题。连接设备通常分三类。第一类是 USB 连接插上线之后手机弹窗询问“是否允许 USB 调试”要点允许最好勾选“一律允许使用这台计算机进行调试”。第二类是无线调试Android 11 及以上的设备有官方无线调试功能开发者选项里打开“无线调试”然后adb pair 192.168.1.100:40000 # 输入屏幕上显示的配对码 adb connect 192.168.1.100:40001配对只需要做一次后续直接 connect 就行。第三类是针对模拟器adb connect emulator-5554这类一般默认就通。注意如果执行adb devices看不到设备先别急着怀疑手机。在 Linux 上很常见的是缺少 udev 规则Windows 上则多半是驱动问题。另外adb server 的 5037 端口偶尔会被占用执行adb kill-server再adb start-server重启服务能解决一多半连接异常。1.2 内存排查的需求场景与命令选型做内存排查之前先想清楚两个问题我要查的是哪个对象的什么内存以及当前环境允不允许我拿到完整数据我通常按场景把需求拆成五类每类对应不同的命令组合如下表所示排查需求首选命令说明看进程总内存占用adb shell dumpsys meminfo package最直观按类别列出 Java/Native/Graphics 等看 Java 堆分配情况adb shell dumpsys meminfo package中 Java Heap 部分关注堆大小、已分配、空闲看进程虚拟内存空间adb shell cat /proc/pid/status或 procrankVSS 数值非常大信息量有限但可查抓内存快照做泄漏分析adb shell am dumpheap package /data/local/tmp/xxx.hprof然后 pull 出来用 Android Studio 或 MAT 分析看系统整体内存adb shell cat /proc/meminfodumpsys meminfo区分 MemAvailable 与 MemFree 的不同含义这个表格是我现在项目里贴墙上的清单。遇到具体问题先查表找方向然后对症下药。接下来的内容就是围绕这张表展开的逐项拆解。2. Java 堆内存与 ART 虚拟机分配机制Android App 的内存大头里面Java 堆是绝大多数开发者最先想到的。这部分内容我结合 dumpsys meminfo 的实际输出把 Java Heap 相关的字段讲透。2.1 用 dumpsys meminfo 查看 Java Heap先来一条最常用的命令adb shell dumpsys meminfo com.example.app如果 App 是多进程架构可能需要指定进程名adb shell dumpsys meminfo com.example.app:push输出里有一段长这样不同 Android 版本会有差异但核心字段基本稳定** MEMINFO in pid 12345 [com.example.app] ** Pss Private Private Swap Heap Heap Heap Total Dirty Clean Dirty Size Alloc Free ------ ------ ------ ------ ------ ------ ------ Native Heap 10240 4720 192 3400 20000 18500 1500 Dalvik Heap 24560 17280 0 1200 42000 38000 4000 Dalvik Other 1840 1280 0 20 Stack 220 220 0 0 Cursor 60 60 0 0 Ashmem 34 12 0 0 Other dev 80 32 0 12 .so mmap 5120 2040 156 30 .apk mmap 1280 240 980 0 .ttf mmap 120 12 100 0 .dex mmap 2460 1200 1100 20 .oat mmap 1130 230 860 0 .art mmap 2430 1400 600 40 Other mmap 320 120 80 40 EGL mtrack 4096 4096 0 0 GL mtrack 2048 2048 0 0 Unknown 1110 1024 0 0 TOTAL 56396 34478 4012 3832 62000 56500 5500这里最关键的一行是Dalvik Heap / Java Heap。它的Heap Size表示 ART 虚拟机当前向系统申请的堆大小Heap Alloc表示已经分配出去的对象占用的空间Heap Free表示堆里剩余可用的空间。注意这里的“剩余”是堆内部的空闲空间不是系统内存的剩余。2.2 Java 堆大小限制是怎么算出来的你可能会问为什么Heap Size是 42MB为什么不是 256MB这背后有两个关键属性dalvik.vm.heapgrowthlimit和dalvik.vm.heapsize。在 Android 5.0 之后Dalvik 已经被 ART 替代但这两个属性的名字保留了下来。正常情况下App 启动后 Java 堆上限受到heapgrowthlimit限制这个值一般由厂商配置常见的是 128MB 到 512MB。当年为低端机设计的 2xx 系列设备可能更低。但是一旦在 AndroidManifest.xml 里给application设置了android:largeHeaptrue堆上限就会放宽到heapsize的值通常是 growthlimit 的 2 到 3 倍。这带来一个很重要的排查思路如果 dumpsys meminfo 里看到Heap Alloc已经超过默认 growthlimit系统不会立刻抛 OOM而是在你继续分配大对象的时候才崩。判断是内存泄漏还是内存需求过大一个简单做法是看Heap Alloc相对于Heap Size的占比。如果 Alloc 长期贴着 Size 走GC 之后也降不下来而 Heap Size 还在继续往上涨这多半是泄漏如果 Alloc 偶尔冲高但马上回落可能只是瞬时峰值。2.3 命令行下观察 Java 堆变化的实操方法光看某一次的快照意义有限要判断内存是否异常应该连续采样。我常用的套路是写一个三秒钟循环for i in $(seq 1 10); do adb shell dumpsys meminfo com.example.app | grep -E Dalvik Heap|TOTAL | xargs echo sample $i:; sleep 3; done然后把每次输出的Dalvik Heap Heap Alloc记录下来。如果在用户无操作、页面停留的情况下Heap Alloc 还在单调递增那基本可以确定有 Java 堆泄漏。提示dumpsys meminfo 本身会触发一次 GC但它影响的是进程行为不会篡改最终报告。你可以看到报告里有(free...)之类的附加信息但那不影响各区域内存统计。3. 别被 VSS 骗了虚拟内存与 PSS/RSS 的真实含义很多人在查内存时会先看 VSSVirtual Set Size因为它看起来最大、最容易拿来做文章。但 VSS 恰恰是最没参考价值的指标之一。这一节把 VSS、RSS、PSS、USS 四个概念一次说清白。3.1 VSS/RSS/PSS/USS 的定义与区别这四个指标都是描述进程内存占用但角度完全不同指标全称含义特点VSSVirtual Set Size进程可访问的虚拟地址空间总大小包含未分配的地址段、已映射未加载的库数值最大没有参考价值RSSResident Set Size驻留物理内存大小包含共享库被多个进程共享的部分会重复计算PSSProportional Set Size按比例分摊共享内存后的物理内存每个进程把共享库按进程数分摊所有进程 PSS 之和接近真实系统内存USSUnique Set Size进程独占物理内存不包括任何共享部分反映进程自身真实占用Android 系统里dumpsys meminfo给出的核心参考指标就是 PSS。为什么不用 RSS举一个实际例子如果两个 App 都加载了同一个系统 WebView 的 so 库这个库占 50MB 物理内存。RSS 会把这 50MB 同时算给两个进程导致你看到每个进程都多了 50MB而 PSS 会把 50MB 除以 2每个进程只加 25MB所有进程加起来刚好是真实使用的 50MB。所以系统在判定进程是否该被杀死时用的也是 PSS。VSS 为什么没用因为它统计的是虚拟地址空间大小。换句话讲进程映射了一段地址但这段地址可能还没有对应到任何物理内存。Android 上很多 so 库动辄映射几百 MB 虚拟地址但实际加载到物理内存的代码段只有几十 MB。所以有同行拿着 VSS 报“App 占用 3GB 内存”那是把虚拟地址空间和物理内存搞混了。3.2 没有 root 的情况下怎么看真实内存占用procrank命令可以列出进程的 VSS/RSS/PSS/USS但它需要一定权限普通 App 进程执行不了。实际工作中我在非 root 设备上最常用的替代方案是通过/proc/pid/status文件拿数据adb shell cat /proc/12345/status输出的关键字段VmPeak: 4200000 kB VmSize: 3980000 kB VmRSS: 82000 kB VmData: 2000000 kBVmPeak 是进程启动以来虚拟内存的历史峰值VmSize 是当前虚拟内存总大小VmRSS 是当前驻留物理内存对应 RSSVmData 是数据段大小比较能反映堆的规模不过/proc/pid/status拿不到 PSS 和 USS想看 PSS 还是得用 dumpsys meminfo。在某些系统上还能跑adb shell dumpsys meminfo --package package这个命令会把该应用所有进程的内存全部列出来。经验如果厂商定制 ROM 把 dumpsys 限制得很死可以考虑抓一份adb shell cat /proc/meminfo配合速度测试的 CPU 采样数据交叉判断。内存问题的判断不能靠单一指标多角度对照才不容易被带沟里。3.3 一个真实案例VSS 巨大但 PSS 正常的“假性内存高”之前遇到过一个线上反馈说某个版本内存“飙升到 3GB”。我接过来第一步先查 VSS确实很大但那是因为 App 加载了一个大型游戏引擎 SDKSO 文件映射的虚拟空间非常大。再调出 PSS 一看实际物理占用只有 300 多 MB完全在正常范围内。这个案例说明一个道理做内存汇报和排查时如果你把 VSS 当主要指标很容易制造恐慌也容易走偏。VSS 可以辅助判断某些异常比如虚拟地址耗尽的情况在 32 位进程上偶尔会出现 mmap 失败但它从来不是 Android 内存优化的抓手。真正要盯的是 PSS 和 USS必要时再结合 VmData 看堆的增长趋势。4. dumpsys meminfo 的完整字段拆解与逐项解读dumpsys meminfo 是命令行里最能打的全景图命令。前面已经展示过部分输出但只看了 Java Heap 是不够的。想真正指导问题定位得把整张表读懂。4.1 区域字段的含义与关注重点我把 dumpsys meminfo 输出里的常见区域分成三类重点关注。第一类是Dalvik Heap / Native Heap。Dalvik Heap 对应 Java 对象分配Native Heap 对应 C/C 层 malloc 出来的内存。Android 里常见的 Native 内存增长点包括Bitmap 像素数据部分场景会走 Native、WebView 内核、OpenGL 资源、加密库、图片解码库等。排查时如果看到 Native Heap 持续增长而 Java Heap 稳定重点查 Native 层面的泄漏。第二类是Graphics / EGL mtrack / GL mtrack。这部分对应 SurfaceFlinger 和 GPU 相关的内存。EGL mtrack 是 EGL 缓冲区的跟踪GL mtrack 是 OpenGL 驱动层的跟踪。如果应用里大量使用 SurfaceView、TextureView、自定义 OpenGL 渲染这几个值会很高。之前我遇到过一个视频类 App 内存虚高的问题排查到最后发现是 GL mtrack 上去了删掉一帧没释放的 Surface 就好转。第三类是Cursor这个在旧版本 Android 上需要关注数据库游标是否泄漏。现在的版本里Cursor 内存被算到其他区域看到这个字段为 0 是正常现象。4.2 Private Dirty / Private Clean / Swap 的实战价值报告中每一行都有 Pss、Private Dirty、Private Clean、Swap 这几个子项。这里我挑最常用的两个说一下。Private Dirty 表示该进程独占的、不能换出到存储介质的脏内存。这个值越大说明进程真正“占着不放”的内存越多对系统内存压力影响越大。Private Clean 是进程独占但可以换出的干净内存通常是文件映射页比如 dex/oat/apk 的映射。它可以在内存紧张时被内核回收所以对系统压力相对小。Swap 字段表示该进程被换出到 swap/zram 的脏页大小。很多国产 ROM 开了 zramSwap 非零是正常的但如果一个常驻进程的 Swap 长期大于几百 MB说明它频繁让系统压缩内存性能会严重受影响。排查时我给自己定的习惯是先看 TOTAL 行如果 PSS Total 超过预期再逐行看是哪个区域异常。重点盯 Private Dirty 的涨落因为它是不可回收的实打实的内存。4.3 不同 Android 版本下字段差异Android 8.0 之后系统的 dex2oat 和 ART 改动让 dumpsys meminfo 输出有一些细微变化比如新增了一些 mmap 分类。Android 10 之后系统对内存统计更倾向于按进程整体算字段组织更细化。Android 13 及更高版本中.art mmap这类由应用产生的 ART 映射和系统服务产生的 ART 映射分开统计字段更清晰。如果你换了一台新版本的测试机发现输出格式不一样别慌关键字段Heap、Pss、Private Dirty、Swap的名称和含义基本一致。格式变了也不影响我们判断趋势。提示dumpsys meminfo 在 Android 5.0 以下设备上的输出和现在的差异比较大老设备上往往没有 Native Heap 分类Java Heap 也会写成 Dalvik Heap。这类设备基本已经退出主流本文的方法主要针对 Android 7.0 及以上。5. hprof 内存快照从抓取到分析的全流程实操命令行解决“内存现在有多大”但回答不了“这个对象到底是谁创建的、为什么没释放”。要回答“为什么”必须抓 hprof 快照做对象级分析。这也是内存排查最花时间也最有价值的一步。5.1 三种生成 hprof 的方法对比抓 hprof 并不是只能靠 Android Studio。命令行下至少有两种方式加 Android Studio 一共三种我列个表对比一下方法命令 / 操作适用场景限制Android Studio Memory ProfilerUI 点按钮导出开发调试阶段需要 Debuggable 应用需要 Android Studio 连接设备am dumpheap命令adb shell am dumpheap package /data/local/tmp/xx.hprof命令行环境脚本化批量抓取需要一定权限普通 release 包可能拒绝Debug.dumpHprofData()API在代码里调Debug.dumpHprofData(path)触发特定时机自动抓取需要框架支持生产环境慎用am dumpheap是最符合“adb shell 排查”基调的方式。执行之后hprof 文件会被写到设备端。随后执行adb pull /data/local/tmp/xx.hprof ./xx.hprof这里有几个常见坑/data/local/tmp不是所有进程都能写。如果提示 permission denied可以先切到 shell 用户再试或者让进程自己写到自己有权限的应用私有目录。非 debuggable 的 release 包抓 hprof 经常被拒绝logcat 里会看到 “error opening heap dump file” 之类的提示。遇到这种情况要么用 debuggable 包要么用 root 设备。hprof 文件很大一个 300MB 堆的进程抓出来的 hprof 可能接近 1GB。执行 dump 的瞬间 App 会卡顿甚至假死线上抓快照务必评估好时间窗口。5.2 hprof 转换与 Android Studio 分析路径从设备上拉下来的 hprof 是 Android 格式不是标准 JVM 格式。用老版本的 MAT 直接打不开需要先过一道转换。Android SDK 的 platform-tools 里附带了一个hprof-conv工具hprof-conv -z input.hprof output.hprof-z参数表示排除非 App 分配的 Zygote 堆对象通常能显著缩小文件体积。不过现在 Android Studio 自带的 Memory Profiler 已经能直接打开 Android 格式 hprof所以大多数情况下不需要手动转换。但也有同行习惯用 MAT 看 Histogram 和 Dominator Tree这时候转换就是刚需。用 Android Studio 打开 hprof 之后我通常按这个顺序分析先看Heap 下拉框里的default_heap和image_heap确认自己分析的是 Java 堆。打开Analyze Tasks跑一遍Detect Leaked Activities自动筛查 Activity 泄漏。跑一遍Find Duplicate Classes查重复加载的类。按Retained Size排序从最大的对象往下点看引用链。以之前我处理过的一个启动闪退为例hprof 里发现同一个SplashActivity实例有 20 多个Retained Size 巨大。循着引用链往上找看到一个静态的Handler持有 Activity 引用而 Handler 里的Message一直没被 remove。这就是典型的内存泄漏路径。如果不用 hprof光靠 dumpsys meminfo 只能干瞪眼。5.3 大 hprof 的分析技巧hprof 动不动就上 GBAndroid Studio 打开时经常卡得让人怀疑人生。我自己的几个应对技巧抓快照前先想办法减小堆。让 App 先回到首页、清掉大图界面再抓能有效减小文件体积。用hprof-conv去掉 Zygote 堆和系统堆的共享对象可能把 1.5GB 减到 400MB。分析时关闭 Android Studio 的自动索引和 lint给 IDE 留足内存修改 studio.vmoptions 里的-Xmx。如果只需要看某个特定对象的引用路径优先用 MAT 的 OQL 查询而不是在 Android Studio 里手动翻。-- 示例查找所有未释放的 Activity 实例 SELECT * FROM instanceof android.app.ActivityOQL 在超大 hprof 里的响应速度通常比 GUI 操作快不少。注意抓取和分析 hprof 是内存泄漏分析的黄金手段但并不是所有“内存高”都是泄漏。有些 Native 内存增长在 Java hprof 里完全看不到需要用 malloc debug 或者 Address Sanitizer 去追。先判断问题出在 Java 层还是 Native 层再选对应的工具。6. 系统可用内存与低内存回收机制App 的内存表现离不开宿主环境。系统整体内存是否紧张决定了你的 App 是能自由飞翔还是随时可能被低内存回收机制带走。所以排查 App 内存问题一定要会看系统可用内存。6.1 /proc/meminfo 的关键字段别只看 MemFree查看系统内存的第一条命令是adb shell cat /proc/meminfo输出里最容易被误读的是 MemFree。MemFree 只表示完全空闲的物理页不代表系统“还能用多少内存”。真正的可用内存要看MemAvailableMemAvailable 是内核估算出的、在不触发严重 swap 的情况下还可以分配给新进程的内存大小。它已经把缓存Cached、缓冲Buffers、可回收页都考虑进去了。如果 MemFree 很小但 MemAvailable 还有几 GB说明系统内存管理正常页缓存在背锅。再往下看还有几个字段值得留意Cached页缓存包括文件缓存和匿名页缓存。它可以在内存紧张时被回收所以高并不代表危险。SwapTotal / SwapFreezram 或 swap 分区的总量和空闲。如果 SwapFree 持续走低并且频繁变化说明系统正在频繁压缩内存页。AnonPages匿名页通常是进程堆和栈。这个值走高说明进程实际使用的物理内存变多。典型的值参考一台 8GB 内存的设备开机开了一堆应用后MemAvailable 可能只有 1.5GB 左右。如果低于 500MB系统已经处于偏紧张状态杀后台进程的事件会开始频繁发生。6.2 lowmemorykiller / lmkd 与阈值判断Android 系统有一个专门管理低内存的组件旧版本叫 lowmemorykiller新版本叫 lmkd。它在内存不足时按进程优先级回收进程。通过下面的命令可以看到内核配置的阈值adb shell cat /sys/module/lowmemorykiller/parameters/minfree输出的是六个数字对应六个内存级别。数字是“剩余内存页数 × 每页大小”例如18432, 23040, 27648, 32256, 36864, 46080表示内存剩余低于某个级别时系统开始按 adj 值依次杀进程。当然很多新设备已经改用 lmkd 用户态动态计算阈值这个 sysfs 节点可能只读或数值不再更新。更直接的方式是看 logcat 里的杀进程记录adb logcat -s ActivityManager | grep -E am_kill|lowmemorykiller|lmkd看到类似am_kill: 12345 com.example.app (adj 900)的日志就能定位到 App 是被系统回收的并且能看到当时的 adj 值。如果一个前台 App 的 adj 值过高被系统回收说明它占的内存太大或者系统整体内存濒临耗尽。6.3 结合 meminfo 与 dumpsys meminfo 判断“系统是不是真的不够用”实操中如何判断一个 App 卡顿、被杀是系统内存不足造成的我总结了一套组合拳# 先看系统整体 adb shell cat /proc/meminfo | head -n 20 # 再看设备各进程内存分布 adb shell dumpsys meminfo | grep -E Total PSS|TOTAL PSS|RAM:dumpsys meminfo不带包名的输出末尾有整个系统的内存汇总包括 Total RAM、Free RAM、Used RAM、Lost RAM 等。这里 Free RAM 会把缓存算进去Used RAM 是所有进程 PSS 之和。如果 Used RAM 长期接近 Total RAM而且 MemAvailable 低于几百 MB那系统就是真的紧。我之前在测试机上复现过一个诡异问题App 正常使用切后台再切回来界面重建了。一看 logcat 发现被杀进程的 adj 是 900而当时系统 MemAvailable 只有 150MB。再一看后台有三个 2GB 内存的应用在跑。这根本不是 App 自身的问题是系统整体内存不足导致的回收。这种场景下给 App 自身做再多的内存瘦身收益也有限必须从系统层面做协同优化。6.4 低内存状态下的快速响应判断除了静态的 meminfo现代内核还提供了 PSIPressure Stall Information指标用来衡量内存、CPU、IO 的压力时间。Android 10 及以上的设备上可以看adb shell cat /proc/pressure/memory输出类似some avg100.00 avg600.00 avg3000.00 total0 full avg100.00 avg600.00 avg3000.00 total0some 表示至少有一个任务因内存不足而被阻塞的时间比例full 表示所有任务都被阻塞的时间比例如果 full 的 avg10 长期大于 0基本可以判定系统内存压力大到已经影响所有进程了。这个指标在压测和稳定性测试里很实用比单纯看数字更直观。排查内存问题时我的建议是不要只盯一个数字。先看系统整体有没有压力再看具体进程的 PSS 分布最后用 hprof 定位对象级别的泄漏。三层下来问题基本无所遁形。最后再分享一个我自己的小习惯把常用命令封装成脚本比如mem.sh里面写好采样次数和过滤词连着设备一键执行输出直接落到文件里方便前后对比。排查内存问题最怕的就是无 baseline 可对比有了历史数据任何异常都瞒不过眼睛。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑