Android 获取内存信息全攻略:堆内存、系统内存与进程内存

QuibblerQuibbler 2020-06-09 约 21 分钟 200 次阅读

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 内存的真实占用。

相关推荐

精选
ViewPager和PagerAdapter、FragmentPagerAdapter、FragmentStatePager
Android

ViewPager和PagerAdapter、FragmentPagerAdapter、FragmentStatePager

ViewPager1、ViewPagerandroidx.viewpager.widget.ViewPager,Android中使用非常广泛的控件,可以说是APP必备:首次打开引导页、页面Banner广告等。常用方法:setAdapter() 设置适配器setOffscreenPageLimit() 设置缓存的页面个数,默认是 1setCurrentItem() 跳转到特定的页面setOnPage

1.1k
获取Android内置WebView内核版本
Android

获取Android内置WebView内核版本

获取Android内置WebView内核版本竟然能遇到这样奇葩的事情,网页用的技术过于新颖,以至于只支持高版本Chromium内核的,低版本安卓系统中内置的内核版本较低,无法加载前端页面。1、设置查看在系统设置里 > 应用 > 应用管理 > 显示系统应用,查看WebView组件:2、页面查看通过WebView发起的网络请求,都会带上浏览器的UA,通常页面都可以通过UA判断浏览器的内核版本。这里有两

1.1w
Android 14适配总结
Android

Android 14适配总结

Android 14适配总结毕业工作至今已经适配了三个Android大版本,从Android 11到Android 12、再到Android 13。2023年,Google即将推出的Android 14,上半年已经开始第一批适配。现在,第四个Android版本已经适配完,总结记录一下。1、Android 14计划Google一般会在2月份对外发布预告,同时放出开发者预览版。“拉通”各大平台、厂商以

1.1w