Android 获取内存信息全攻略:堆内存、系统内存与进程内存
内存优化始终是 Android 性能工程的重头戏,而"优化"的前提是"量得到"。但不少同学照着网上的代码一抄,发现拿到的内存值要么只涨不跌、与 Android Studio Profiler 对不上,要么数字大得离谱——根源在于 Android 的"内存"有好几套口径:Java 堆、系统可用内存、进程 PSS、设备物理内存,各自 API 不同、含义不同。本文把 Runtime、ActivityManager、Debug.MemoryInfo、/proc/meminfo 四条路径一次讲透,并给出与 Android Profiler 完全对齐的获取方式。
1、内存指标速览:看懂这些数才有意义
动手之前,先分清几个常被混为一谈的指标,否则后面取到的数全是"糊涂账":
指标 含义 获取方式
---------------------------------------------------------------------------
maxMemory JVM 堆最大可使用内存(受 largeHeap 约束) Runtime.getRuntime().maxMemory()
totalMemory JVM 堆当前已向系统申请的内存 Runtime.getRuntime().totalMemory()
freeMemory 已申请堆中尚未使用的部分 Runtime.getRuntime().freeMemory()
availMem 整机当前可用内存 ActivityManager.MemoryInfo.availMem
dalvikPrivateDirty 进程 Java 堆私有脏页(KB) Debug.MemoryInfo.dalvikPrivateDirty
Total PSS 进程总 PSS,按比例分摊共享内存(KB) Debug.MemoryInfo.getTotalPss()
MemTotal 设备物理内存总量(KB) /proc/meminfo 首行核心实现(口径区分):
1. Runtime 三件套只反映当前进程的 Java 堆,不含 Native/图形内存
2. PSS(Proportional Set Size)会把共享库按进程数分摊后计入,是最接近"进程实际占用"的指标
3. Private Dirty是进程独占、且未写回磁盘的部分,判断"杀了它能省多少"看它
4. availMem是整机维度,不是单个 App;想看"App 用了多少"别用它
2、应用 Java 堆内存:Runtime 三件套
最简单也最常用的,是 Runtime。它是每个进程一个的 JVM 单例,直接反映当前进程 Java 堆的使用情况,无需任何权限,主线程也能调:
// Runtime 单例:反映当前进程 Java 堆使用情况
Runtime runtime = Runtime.getRuntime();
// JVM 堆最大可使用内存(受 largeHeap、设备分配上限约束,单位 byte)
long maxMemory = runtime.maxMemory();
// 当前已向系统申请的堆总内存
long totalMemory = runtime.totalMemory();
// 已申请的堆中尚未使用的部分
long freeMemory = runtime.freeMemory();
// 真正"已使用" = 已申请 - 未使用(注意不是 maxMemory - freeMemory)
long usedMemory = totalMemory - freeMemory;
// 换算成 MB
String heap = String.format("堆内存: 已用 %.2f MB / 上限 %.2f MB",
usedMemory / 1024f / 1024f,
maxMemory / 1024f / 1024f);核心实现:
1. 通过 Runtime.getRuntime() 拿到当前进程的 Runtime 单例
2. maxMemory() 是堆上限,totalMemory() 是已分配,freeMemory() 是已分配中的空闲
3. 真正"已用"= totalMemory - freeMemory,而不是 maxMemory - freeMemory(这是最常见的误算)
4. 返回值单位为 byte,除以 1024×1024 得到 MB
- 适用场景:内存监控浮窗、GC 前后对比、判断是否逼近堆上限引发 OOM
3、系统可用内存:ActivityManager.MemoryInfo
想知道"整机还剩多少内存"、"是不是快低内存了",用 ActivityManager。它还顺带告诉你这个 App 被分配的堆上限:
// 系统级内存信息:可用内存、低内存阈值
ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
ActivityManager.MemoryInfo info = new ActivityManager.MemoryInfo();
am.getMemoryInfo(info);
// 系统当前可用内存(byte),整机维度
long availMem = info.availMem;
// 内存紧张阈值,低于它系统开始回收后台进程
long threshold = info.threshold;
// 是否处于低内存状态(availMem <= threshold 时为 true)
boolean isLow = info.lowMemory;
// App 被分配的堆上限(MB),对应不开启 largeHeap 的默认值
int memoryClass = am.getMemoryClass(); // 如 192
// 开启 largeHeap 后的堆上限(MB)
int largeMemoryClass = am.getLargeMemoryClass(); // 如 512核心实现:
1. getSystemService(ACTIVITY_SERVICE) 获取 ActivityManager 实例
2. new 一个空的 MemoryInfo 对象,传入 getMemoryInfo(outInfo) 由系统填充
3. availMem 是整个系统当前可用内存,不是单个应用,别拿它当 App 占用
4. lowMemory 为 true 时应主动释放资源(如清空图片缓存、trim 内存)
5. getMemoryClass() 返回的就是"你这 App 最多能用多少堆",与 Runtime.maxMemory() 基本一致
4、进程内存:Debug.MemoryInfo(对齐 Profiler,重点)
这是最容易踩坑、也最关键的一节。很多文章的内存值"对不上 Profiler",就是因为没用对 Debug.MemoryInfo。它能拿到与 dumpsys meminfo / Android Studio Profiler 完全一致的口径。
4.1、基础用法:PSS 与 Private Dirty
// 进程级内存:与 dumpsys meminfo / Profiler 口径一致
ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
int pid = android.os.Process.myPid();
// 注意:返回的是数组,与传入的 pid 一一对应
Debug.MemoryInfo[] memInfo = am.getProcessMemoryInfo(new int[]{pid});
if (memInfo != null && memInfo.length > 0) {
Debug.MemoryInfo mi = memInfo[0];
// 各类私有脏页(KB)
int dalvikPrivate = mi.dalvikPrivateDirty; // Java 堆私有
int nativePrivate = mi.nativePrivateDirty; // Native 堆私有
int otherPrivate = mi.otherPrivateDirty;
// 总私有脏页(KB),最接近"杀掉进程能立即回收的量"
int totalPrivateDirty = mi.getTotalPrivateDirty();
// 总 PSS(KB),含按比例分摊的共享内存,最接近"进程实际占用"
int totalPss = mi.getTotalPss();
// 换算成 MB
double totalMB = totalPss / 1024.0;
}核心实现:
1. am.getProcessMemoryInfo(int[]) 按 pid 查询进程内存,返回 Debug.MemoryInfo 数组
2. getTotalPss() / getTotalPrivateDirty() 是粗粒度总量,dalvikPrivateDirty 等是分量
3. 所有数值单位均为 KB,求和后再除以 1024 才是 MB
4. 这是排查"为什么我的内存比 Runtime 大那么多"的关键——Runtime 只算 Java 堆,Debug.MemoryInfo 算全进程(Native/Code/Graphics/Stack 全含)
4.2、进阶:按 Profiler 维度精确取值
// API 23 (M) 起,可按 Android Profiler 的分类维度精确取值
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Debug.MemoryInfo mi = memInfo[0];
// 与 Android Profiler 完全一致的口径(单位 KB)
int javaHeap = Integer.parseInt(mi.getMemoryStat("summary.java-heap"));
int nativeHeap = Integer.parseInt(mi.getMemoryStat("summary.native-heap"));
int code = Integer.parseInt(mi.getMemoryStat("summary.code"));
int stack = Integer.parseInt(mi.getMemoryStat("summary.stack"));
int graphics = Integer.parseInt(mi.getMemoryStat("summary.graphics"));
int totalPss = Integer.parseInt(mi.getMemoryStat("summary.total-pss"));
// 把各分项相加,即可得到与 Profiler 图形完全吻合的总占用
}核心实现:
1. API 23 起支持 getMemoryStat(statName),按字符串 key 取单项
2. 可用的 key 包括 summary.java-heap、summary.native-heap、summary.code、summary.stack、summary.graphics、summary.system、summary.total-pss 等
3. 各分项求和即等于 summary.total-pss,与 Android Profiler 的着色分类一一对应
4. 低版本(API < 23)回退到 getTotalPss() / dalvikPrivateDirty 即可
- 这正是"调试浮窗显示的内存与 Profiler 不一致"的根治方法:用 getMemoryStat 而非 Runtime
5、设备总内存:读取 /proc/meminfo
ActivityManager 没有直接给出"设备物理内存总量"。最稳妥的方式是读取 Linux 内核文件 /proc/meminfo,它的首行 MemTotal 就是总量:
// 设备物理内存总量与空闲,读 /proc/meminfo
public static long[] getDeviceMemory() {
long[] result = new long[]{0, 0}; // [总内存, 空闲内存],单位 KB
try (BufferedReader reader = new BufferedReader(
new FileReader("/proc/meminfo"), 8192)) {
// 第一行:MemTotal: 2835268 kB
result[0] = parseMemLine(reader.readLine());
// 第二行:MemFree: 512340 kB
result[1] = parseMemLine(reader.readLine());
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
// 解析 "MemTotal: 2835268 kB" 这类行,返回 KB 数值
private static long parseMemLine(String line) {
if (line == null) return 0;
String[] parts = line.trim().split("\\s+");
// parts[0]=名称,parts[1]=数值,parts[2]=单位(kB)
if (parts.length >= 2) {
return Long.parseLong(parts[1]);
}
return 0;
}核心实现:
1. 读取 /proc/meminfo 第一行 MemTotal 得到设备总内存,第二行 MemFree 得到空闲
2. 用空白正则 \\s+ 拆分,取第二段数字,单位为 kB
3. 想"总量 - 已用"时,更准确的"可用"建议用 ActivityManager.availMem(已扣除缓存),而非 MemFree
- 适用场景:内存清理类 App 显示"总内存/可用内存",设备信息页
6、命令行与可视化工具
除了代码,日常排查内存更高效的是命令行与图形工具,它们底层数据源正是上面这些 API:
// ① dumpsys meminfo:最权威,与系统设置里显示的占用一致
// 命令:adb shell dumpsys meminfo <包名>
// 输出含 TOTAL(PSS)、Java Heap、Native Heap、Graphics、Code、Stack 等
// ② procrank:查看所有进程的 VSS/RSS/PSS/USS(需 root)
// PID VSS RSS PSS USS cmdline
// 看 USS(进程独占)判断真实占用,看 PSS 看分摊后占用
// ③ Android Studio Profiler:可视化实时内存
// 分类着色显示 Java/Native/Graphics/Code/Stack
// Capture Heap Dump 抓堆快照定位泄漏,Record Allocations 找分配热点核心使用步骤:
1. dumpsys meminfo <包名>:看 TOTAL 行的 PSS,最接近用户在设置里感知的"应用占用"
2. procrank(需 root):看 USS 判断"独占多少",适合对比多个进程
3. Profiler:抓 Heap Dump 找内存泄漏,Record Allocations 找高频分配
4. 代码里 Debug.MemoryInfo 的数值,就是这些工具数据的"同源"
7、注意事项与常见坑
这些坑不避开,取到的内存值要么报错要么失真:
① getRunningAppProcesses 的权限收紧。Android 5.0(API 21)起,普通应用调用它只返回自己进程,无法再遍历系统中所有进程的内存。想做"内存清理"遍历全部 App,需引导用户授予 PACKAGE_USAGE_STATS(使用情况访问)系统权限。
② getProcessMemoryInfo 别在主线程频繁调。它走 Binder IPC、开销较大,放在 onDraw 或每帧调用会卡顿;建议在子线程按秒级轮询,或仅在关键节点采样。
③ Runtime 只反映 Java 堆。Native 内存(如 Bitmap 在 Android 8.0 前的像素数据)、图形内存都不在 Runtime 统计内,排查"内存涨但 Runtime 不动"要看 Debug.MemoryInfo 的 native/graphics 分项。
④ 别迷信 largeHeap。它在 manifest 开启后抬高 getLargeMemoryClass 上限,只是"延缓 OOM"而非"治本",真正的泄漏和冗余仍要修。
⑤ 监听低内存主动释放。实现 ComponentCallbacks2 的 onTrimMemory(level) / onLowMemory(),在 lowMemory 为 true 或 level 较高时释放图片缓存、清空临时列表。
8、总结
Android 的"内存"从来不是一个数,而是四套口径:Runtime 看 Java 堆、ActivityManager.MemoryInfo 看整机可用、Debug.MemoryInfo 看进程 PSS、/proc/meminfo 看设备总量。想"量得准",关键是按场景选对口径。
关键要点:
- Java 堆监控用 Runtime 三件套,已用 = totalMemory - freeMemory
- 整机可用内存与低内存判断用 ActivityManager.MemoryInfo
- 进程占用 Debug.MemoryInfo,API 23+ 用 getMemoryStat 对齐 Profiler
- 设备总内存读 /proc/meminfo 的 MemTotal
- 命令行 dumpsys meminfo 与代码 Debug.MemoryInfo 数据同源
对于做性能优化、内存监控浮窗、清理类工具的开发者而言,分清口径、用对 API是必要的前提——尤其是用 getMemoryStat 让代码数值与 Android Studio Profiler 完全对齐,这一步能省掉大半"为什么数字对不上"的排查时间。把这几条路径串起来,你就能在任何场景下准确量出 Android 内存的真实占用。

