Android-切换白天黑夜内存不足问题排查

车机昼夜切换内存上涨排查实录------从 am_pss 台阶到 Native 位图的一生

背景:Android 14 车机 ROM。现象:每切换一次白天/黑夜模式,主应用内存涨 50~70MB,永远不回落 ;连切数次后从 176MB 涨到 304MB。系统侧此前已用 ConfigFlow 全链路打点验证过:配置变更管道(UMMS→ATMS→各进程→各 Activity)完全健康,问题不在下发,在接收。 数据全部来自真实抓取的 logcat(wkk103.log)、dumpsys meminfo 与系统日志。 附:附录A dumpsys meminfo 全表详解(见文末) · 附录B Android GC 深度解析(见文末)


0. 现象与排查总路线

scss 复制代码
现象: 切一次昼夜 +50~70MB, 不回落
  │
  ├─ 第一步 am_pss 对时 ──── 确认"台阶与切换精确对齐"且全局UI进程都涨 → 排除单应用偶然
  ├─ 第二步 GC 日志 ─────── Java 堆毫无压力(1次GC, 13MB/53MB) → 涨的不在 Java 堆
  ├─ 第三步 dumpsys ─────── 只有 Native Heap 在动; Graphics=0(软件位图); 锯齿形(部分可回收)
  ├─ 第四步 应用侧 logcat ── 壁纸每轮重解码 + 皮肤71监听者 + 位图池全miss → 找到分配者
  └─ (可选) heapprofd ───── 拿到精确分配调用栈

1. 第一步:用 am_pss 把内存台阶和切换对时

am_pss 是 AMS 周期性(本机 10s)写入 logcat 的进程内存事件(这里使用了脚本),格式:

ini 复制代码
am_pss : [pid, uid, 进程名, PSS, USS, SwapPss, RSS, imp, procstate, oomadj]
bash 复制代码
#!/system/bin/sh
# Run "dumpsys procstats --start-testing" every 2 minutes.
# Intended to run on the device itself (e.g. via adb shell, or a root shell).
# Stop with Ctrl+C, then run: dumpsys procstats --stop-testing
​
while true; do
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] start-testing"
    dumpsys procstats --start-testing
    sleep 10
done
​

这个命令可以输出am_pss所有进程的内存情况,非常好用

切换标记用 UiModeManager: updateSystemProperties: mNightMode/mode = 1/1(1=白天,2=夜间)。对齐后主应用(pid 4229)的曲线:

采样时刻 PSS 对应事件
14:08:20 176MB 切换前基线
14:08:30 244MB(+68MB) 14:08:23.979 切白天后的第一个采样
14:09:00 297MB(+53MB) 14:08:57.026 切夜间后的第一个采样
14:09:00~14:11:11 297→304MB 平台 不回落

三个定性结论(这一步就能得出):

  1. 涨幅出现在切换后的第一个采样点------台阶型,不是缓慢爬升 → 与切换强相关;
  2. 全局性:涨幅与进程 UI 规模成正比------btmusic 69→84→113MB、launcher 258→276→293MB、splitapp 只 +7MB,非 UI 进程(数据采集、桥接服务等)全程 0~2MB → 是"每个 UI 进程各自重载资源"的通用模式,不是某个进程的孤立 bug;
  3. 同窗口内 lmkd 已因内存压力杀掉输入法(free 604MB/6.4GB,9%)------这种上涨在加速系统撞墙。

2. 第二步:GC 日志排除 Java 堆

Art 的 GC 行如:

yaml 复制代码
14:08:12.492  4229  4254 I .hmi.bev.***: NativeAlloc concurrent copying GC freed
176484(7135KB) AllocSpace objects, 36(896KB) LOS objects, 75% free, 13MB/53MB,
paused 158us,73us total 286.632ms

读法:原因 (NativeAlloc/Background/Alloc/Explicit)+ 回收量 + 堆水位 used/size + 两段 STW 暂停 + 并发总时长(逐字段详解见附录B·B5)。

本例 3.5 分钟里主应用只有这 1 次 GC ,且两次切换之后一次都没有------Java 堆(上限 53MB、活对象 13MB)毫无压力。结论:涨的 50~70MB 不在 Java 堆,Java GC 天然管不到。

知识框:Android 14 的 GC(Concurrent Copying) ------只回收"不可达"的 Java 对象;标记/搬移全并发(读屏障),暂停微秒级;触发按需且惰性;只管 Java 堆。完整机制见附录B。

3. 第三步:dumpsys meminfo------定位到列,再看对象计数

切换数次间连抓 5 次 dumpsys meminfo com.***.carapp

# Native Heap Dalvik Heap Size Graphics AssetManagers Views Local Binders TOTAL PSS
1 基线 57.4MB 31MB 0 0 615 47 112.9MB
2 切后 143.3MB 31MB 0 0 650 57 210.9MB
3 又切+空闲 77.3MB 31MB 0 0 650 57 133.0MB
4 再切 143.5MB 31MB 0 0 650 67 208.6MB
5 再切 181.8MB 31MB 0 0 650 78 248.7MB

四条硬结论:

  1. 只有 Native Heap 在动 → 增长 100% 在 native;
  2. Graphics = 0 → 没有硬件位图/纹理,全是软件位图(像素 malloc 在 native 堆);
  3. AssetManagers=0、Assets=2 恒定 → 换肤库没有泄漏 Resources/AssetManager,排除皮肤包资源对象;
  4. 锯齿形 :143→77MB(回落 66MB)→ 大部分旧图其实"没人引用了",等一次 GC 收掉 Java 壳、native 像素才释放;但谷值 57→77、峰值 143→182 都在抬升 → 每轮仍有 20~38MB 进了常驻容器。

顺带抓到一个数量型泄漏:Local Binders 47→78(每轮 +10,不回落) ------每切换注册一次监听/绑定,从不反注册。

知识框:App Summary 怎么读------PSS=私有页全额+共享页按进程数平摊(评估应用内存的权威值);RSS=驻留总量(共享页每进程全额重复计);逐行含义与对象计数详解见附录A。

4. 第四步:应用侧 logcat 证据链------谁在分配

切换瞬间主应用自己的日志,完整暴露了"重载流水线":

yaml 复制代码
14:08:24.067  SkinManager: updateResource old:0x23,new:0x13        ← 判定昼夜翻转,要换肤
14:08:24.098  SkinManager: refreshSkin skin data size: 4
14:08:25.014  SkinManager: refreshSkin listener: 71                 ← 通知 71 个界面各自换图
14:08:24.072  WallpaperViewModel: loadWallpaper uiMode = 0          ← 壁纸模块独立再触发一轮
14:08:25.149  PictureProvider: getAllDefaultWallpaper               ← 重新查数据库
              Data: fileName = ..._day.png                          ← 按昼夜选文件(还有_night.png)
              ExifInterface: getPngAttributes ×15                   ← 每张候选图解析PNG头
              LruBitmapPool: Missing bitmap=[282880]                ← 位图池全miss,没有可复用的
              → BitmapFactory.decodeFile(...)                       ← malloc(2560×1440×4≈14.7MB)/张
14:08:25.154  OpenGLRenderer: Davey! duration=1065ms                ← 主线程同步解码卡1秒

算术验证 :一张 2560×1440 ARGB_8888 = 14.7MB native;每轮解码 46 张全屏级图 ≈ 6086MB,与台阶 +68/+53MB 吻合。

5. 知识点:位图为什么在 Native------"壳与肉"机制

Android 8.0 起像素刻意放在 native(8.0 前放 Java 堆,撞堆上限、绑架 GC、渲染还得 JNI 拷贝):

scss 复制代码
BitmapFactory.decode → Skia 解码 → malloc 像素(14.7MB, Native堆)   ← 肉
                          ▲ NativeAllocationRegistry 登记
Bitmap(Java对象, 几十字节, 持 native 指针)                          ← 壳, Java堆

NativeAllocationRegistry 把像素的生死绑在 Java 壳上:壳被 GC 判定不可达时,才顺带 free 像素。由此推出全部现象:

  • 壳被强引用 → GC 不收 → 像素常驻("GC 掉内存"的真相);
  • 某次 GC 收了一批不可达壳 → 像素集中释放(dumpsys 里 143→77 的回落);
  • NativeAlloc 型 GC = native 分配量推动水位线,逼 VM 跑一次 GC"顺带收壳";
  • 释放永远滞后、被动------与 onTrimMemory 也没有自动联动。

而 onTrimMemory 只能回收"应用主动放进可释放名单"的东西:本例 level=20 到达时,位图池响应 trim 只清出一张 282KB------大图根本不走池,皮肤/壁纸缓存没实现 trim,纹丝不动。

6. 病根:切换被实现成"重新加载",而不是"查缓存"

先澄清一个误解:不是 SkinManager(或某个系统组件)忘了解引用 。系统在昼夜切换里只送来一个改了 uiMode 字段的 Configuration 对象,之后每一步都是应用自己的代码:SkinManager 只是应用内的分发器(通知 71 个监听者),真正持有图片的是正常的 UI 引用网:

scss 复制代码
像素(native) ← Bitmap壳 ← Drawable ← View.mBackground ← 窗口 ← 活着,天经地义
                              ▲
        另外几个"理所当然"的常驻持有者(问题所在):
        · WallpaperViewModel 的字段  ← ViewModel 故意设计成活得比Activity长
        · RecyclerView Adapter 的数据集合(只增不减)
        · 换肤库缓存 Map(只 put 不 evict)

每次切换的代码路径(由日志反推):

scss 复制代码
void loadWallpaper(int uiMode) {
    List<Wallpaper> list = provider.queryAll(uiMode);   // 每轮查库(路径明明是静态的)
    for (Wallpaper w : list)
        w.bitmap = BitmapFactory.decodeFile(w.path);    // ← 每轮向 native 要一整套
    viewModel.setWallpapers(list);                      // 旧list若还被引用→旧图常驻
    adapter.submitList(list);
}

三道回收闸全部失效的原因:GC 无关 (像素在 native 且壳可达)、池没接住 (大图绕过位图池)、缓存只进不出 (无上限无淘汰)。昼夜资源本质是封闭二元集合,正确形态是两套常驻、切换=换引用;实际形态是每轮重新生产一份副本。

7. 修复方案

  1. 双套常驻缓存(最优)EnumMap<UiMode, SkinSet>,冷启动解码当前模式,首次反向切换补齐另一套------此后切换零解码、零分配、零卡顿,内存收敛在两套之和(约 +30MB 固定值);
  2. 壁纸 loadWallpaper(uiMode) 先查缓存(key 只有 day/night 两个),命中直接复用 Bitmap;
  3. 静态信息前置:昼夜壁纸路径/列表启动时查一次建索引,别每轮查库 + 15 次 Exif 解析;
  4. 大图接入统一的位图内存缓存(Glide 磁盘+内存管线),消灭 Missing bitmap 直解码;
  5. 缓存实现 onTrimMemory>= TRIM_MEMORY_BACKGROUND 丢掉非当前模式那套;
  6. 排查每轮 +10 的 binder/listener 注册点,配对反注册。

8. 排查套路速查表

症状/疑问 看什么 判据
内存涨和操作有没有因果关系 am_pss 序列 vs 操作时间戳 台阶出现在操作后第一个采样 → 强相关
是不是 Java 堆泄漏 GC 行 + dumpsys Dalvik Size/Alloc Size 恒定、Alloc 小、无 GC → 无关
涨在哪类内存 dumpsys 逐列 diff Native Heap/ Graphics/ Java Heap 哪列动
位图是软是硬 Graphics 列 0=软件位图(像素在Native);非0=含硬件位图
有没有资源/View/监听器泄漏 dumpsys 对象计数 AssetManagers/Views/Binders 单调涨即泄漏
"可回收却没回收"还是"真泄漏" 多次采样的锯齿形态 有回落=延迟回收;谷值持续抬升=留存累积
谁在分配 heapprofd(heap_profile) 按增量排调用栈
trim 为什么没用 应用 onTrimMemory 实现 只清出 KB 级 = 大缓存没登记可释放

9. 复测验收标准

连切昼夜 5 次 + 静置 1 分钟:

  • 通过:内存收敛(第 2 次切换后不再新高,稳定在两套资源之和 ±10%),切换无 Davey(<16ms/帧);
  • 不通过:PSS 随切换次数线性上涨 → 仍有按轮累积的常驻容器,用 heapprofd 定位剩余持有者。

10.Perfetto 安卓Native内存分析

heapprofd具有如下特性: 1.不用特别设置,只需要一个命令就可以生成需要的信息 2.可以抓取正在运行的进程,在抓取时,进程不需要重启 3.可以抓取应用进程的native的内存信息 4.在不抓取信息时,对系统性能完全没有影响 5.可以抓取系统上所有的进程信息 6.没有或者极少的额外进程内存开销 7.对系统性能影响极小

heapprofd 实现原理: heapprofd既然也是一款监控内存的工具,那么也是需要代理-堆栈回溯-缓存记录三个模块的 heapprofd的代理部分采用的是hook malloc、calloc、realloc、free 等内存分配相关的函数,在系统中 建立一块共享内存buffer,然后按照百分比例进行采样,将进程中的内存申请堆栈复制到共享内存buffer里面,接着使用 libunwindstack 进行堆栈的回溯,最后根据回溯的信息构建一个 Bookkeeping 来跟踪内存分配

Heap Profiles工具可以直接在源码文件夹下使用

在源码这个路径打开android14\external\perfetto\tools命令窗口直接执行命令

sql 复制代码
PS M:\wkk\android14\external\perfetto\tools> python.exe .\heap_profile -p 27625 -c 5000 -o E:\0719\3
Profiling active. Press Ctrl+C to terminate.
You may disconnect your device.
​
Waiting for profiler shutdown...
Wrote profiles to E:\0719\3
The raw-trace file can be viewed using https://ui.perfetto.dev.
The heap_dump.* files can be viewed using pprof/ (Googlers only) or https://www.speedscope.app/.
The two above are equivalent. The raw-trace contains the union of all the heap dumps.

-p是你要抓取的进程号

-c是你要抓取的时间间隔

-o是你要输出的文件夹(必须是要空的)

生成的文件直接用Perfetto打开,里面可以看到未释放的内存,一目了然

附录A:dumpsys meminfo 全表详解

A.1 页是怎么记账的------四个基本量

内核按 (4KB)记账,dumpsys 的数字全部来自 /proc/<pid>/smaps。每一页有三种属性组合:

定义 特点
RSS 驻留物理内存总量,共享页每个进程全额计 各进程 RSS 相加会重复计数,看"物理占用粗感"用
PSS 私有页全额 + 共享页 ÷ 共享进程数 所有进程 PSS 相加 ≈ 物理内存;系统记账与应用排查的权威值
Private Dirty 独占且不可再生(匿名页/改写过的数据页) 丢了就没了;堆、栈、malloc 出来的都在这;找泄漏盯它
Private Clean 独占但可从文件重新读回(代码/资源映射页) 内存紧张时内核可直接回收再换入,"占着但不可怕"

由此:USS = Private Dirty + Private Clean;同一进程 RSS > PSS 恒成立(差值=被平摊的共享页)。本例主应用 RSS 357MB / PSS 249MB,差 109MB 主要是共享的 .so/.oat/framework 代码页。

A.2 详细表逐行(对照本例真实数值)

是什么 本例值 排查意义
Native Heap C/C++ malloc(Scudo 分配器):Bitmap 像素(8.0+)、Skia、编解码缓冲、各类 native 库 57→182MB 图片类泄漏的主战场;Heap Size/Alloc/Free 三列=分配器预留/已分配/空闲
Dalvik Heap ART Java 堆 21MB(Size 恒 31MB) Size 单调涨+Alloc 大 → Java 泄漏;Size 恒定+Alloc 小 → Java 健康(本例)
Dalvik Other 虚拟机自身数据:JIT 代码缓存、linear alloc(类元数据)、间接引用表、GC 标记栈 3MB 类加载器/JIT 异常时涨
Stack 线程栈驻留部分(每线程虚拟预留 1MB,只算碰过的页) 0.9MB 涨=线程数涨(查 HandlerThread/线程池)
Ashmem 匿名共享内存:CursorWindow(数据库查询窗口,本例 launcher 的 app_info.db 每轮 2MB)、SharedMemory ≈0 Cursor 泄漏在此现形
Other dev 设备节点映射(老 ION、gralloc 相关 fd) 0
.so mmap 动态库(libc/libhwui/libskia...),file-backed 2.6MB PSS / 39MB RSS 共享大户,几乎全是可回收页
.jar/.apk mmap 代码与资源文件映射(drawable 源文件在这,未解码前只是 mmap) 29MB PSS APK 很大但此列低=按需换页
.ttf/.dex/.oat mmap 字体 / dex 字节码 / AOT 编译产物 3MB/0.5/0.5MB
.art mmap boot image 内的 intern 字符串、类数据------本身就是 GC Roots 的一部分 1.5MB
Other mmap / Unknown 未归类映射 / smaps 归类不明的匿名页 3MB 持续大涨再深挖(匿名共享段等)

A.3 列的含义

Pss Total / Private Dirty / Private Clean / SwapPss Dirty / Rss Total / Heap Size / Heap Alloc / Heap Free

  • SwapPss :被换出到 zRAM(内存压缩盘)并按 PSS 规则分摊的部分。注意 zRAM 换出省的是"页数"不省"带宽"------数据还在内存里只是压缩了;
  • Heap 三列 只对 Native/Dalvik 两行有意义:Size=分配器向内核要的总量,Alloc=已分配出去,Free=Size−Alloc。判断"分配器虚胖还是真用"看 Alloc;判断增长看 Size 与 Alloc 同涨

A.4 App Summary 与详细表的映射

App Summary 行 来源
Java Heap Dalvik Heap(Private 为主)
Native Heap Native Heap Private Dirty
Code .so/.jar/.apk/.ttf/.dex/.oat 的私有部分(多为 Clean)
Stack Stack 行
Graphics GPU 图形内存:hardware bitmap 像素(gralloc)、GL/Vulkan buffer、Surface------本例为 0,即全部软件位图
Private Other Dalvik Other/Ashmem/Other dev 的私有部分
System 共享系统页按 PSS 平摊部分

A.5 Objects 对象计数------数量型泄漏的快筛

计数 含义 增长意味
Views 拿到 mAttachInfo 的 View 数 涨=界面缓存无限累积(本例 615→650 后稳定,排除)
ViewRootImpl 窗口数 涨=窗口泄漏(Dialog/Popup 没 dismiss)
AppContexts / Activities Context/Activity 实例 涨=最经典泄漏(静态持有 Activity)
Assets / AssetManagers 资源对象 涨=换肤框架每轮 new Resources(本例恒定,排除皮肤资源对象泄漏
Local/Proxy Binders 本端/对端 binder 对象 本例 Local 47→78(每轮+10)→ 监听/服务注册未反注册
Parcel memory / count parcel 缓冲 binder 通信异常时涨
Death Recipients 死亡通知 涨=注册了没解绑
WebViews WebView 实例 >0 且涨=内核进程内存连带

A.6 实战 diff 手法

bash 复制代码
# 切换前后各抓一次,重点 diff Private Dirty 列
adb shell dumpsys meminfo com.xxx > before.txt   # 执行操作
adb shell dumpsys meminfo com.xxx > after.txt    # 比较两文件
# 周期采样(排查增长趋势)
while(1){ adb shell dumpsys meminfo com.xxx | Select-String "TOTAL PSS"; sleep 10 }

进阶:adb shell showmap <pid>(smaps 原始视图,看 [anon:scudo:] 段确认 native 分配器持有量)、procmem(USS/PSS/RSS 分解)。


附录B:Android GC 深度解析(Android 14 / ART)

B.1 GC 只做一件事:回收"不可达"的对象

GC Roots (当前各线程栈上的局部变量、静态字段、JNI 全局/局部引用、正在运行的对象、.art mmap 里的 intern/类数据等)出发沿引用链遍历,走不到的对象 = 垃圾。三个直接推论:

  • 被强引用持有的缓存永远不是垃圾(本例 SkinManager/ViewModel/Adapter 持有的位图);
  • GC 只管 Java 堆;native 内存要靠"壳对象被回收时顺带释放"(见 B.7);
  • System.gc() 不是万能钥匙------显式 GC 可被 ROM 禁用,且它同样只收不可达对象。

B.2 演进:为什么今天是 Concurrent Copying

时代 收集器 痛点
Dalvik(≤4.4) Mark-Sweep 全程 STW,卡顿以百毫秒计
ART 5.0~7.x Concurrent Mark Sweep 标记并发了,但回收仍要暂停整堆;不搬移对象 → 碎片;大堆(图片时代)暂停被拉长
ART 8.0+(含14) Concurrent Copying(CC) 标记+搬移 都并发;碎片靠复制整理消除;暂停降到微秒级

CC 的核心武器是读屏障:应用线程每次从堆里加载一个引用,都检查目标对象的标记/转发位------如果 GC 正在搬它,就自动取新地址(Baker 式)。代价是引用读取多几条指令(JIT 消化掉大半),换来"GC 搬家时应用照常跑"。

B.3 ART 堆布局(GC 日志名词的来源)

空间 放什么 特性
Image Space boot image:zygote 预加载的核心类 只读、跨进程共享、永不回收 → Java Heap PSS 远小于 RSS 的原因
Zygote Space zygote fork 后未被改写的页 与所有 App 共享
Non-moving Space 不能搬的对象:JNI 引用、带 finalize 的、native 正在访问的 CC 不碰
Bump-pointer/RosAlloc 新分配对象(年轻代角色) 指针碰撞分配,极快
RegionSpace 主战场:按 region 组织 CC 在此并发搬运整理
LOS(Large Object Space) ≥12KB 的大对象(大数组、大 Bitmap 壳) 日志单独报:36(896KB) LOS objects

注意 ART 分代是弱化的:没有传统 Minor/Major 节奏,基本整堆并发标记,只是搬移时选部分 region。

B.4 一次 CC GC 的完整流程

scss 复制代码
触发 ─→ ①初始暂停(STW,标记roots直接引用)      ← paused 158us
        ②并发标记(与应用并行;改引用经卡表记脏页)  ← total 286ms 的大头,不停应用
        ③复查暂停(STW,处理脏卡新增引用)          ← paused 73us
        ④并发搬移:存活对象复制到新region,旧region整块丢弃
        ⑤报告 freed N objects, X% free, used/size

"卡表(card table)":应用线程在并发标记期间改了引用,就把所在 4KB 页标脏,③只需复查脏页------这是并发标记不丢引用的关键。

B.5 GC 日志逐字段解码(用本例真实行)

scss 复制代码
NativeAlloc concurrent copying GC freed 176484(7135KB) AllocSpace objects,
36(896KB) LOS objects, 75% free, 13MB/53MB, paused 158us,73us total 286.632ms
 └────┬────┘ └──────────┬─────────┘ └──────────┬─────────┘ └────┬────┘└──────┬──────┘└────┬─────┘
   原因           收集器              普通空间回收量+LOS回收量      堆水位    两段STW暂停   并发总时长
  • 原因(B6 表)标明"谁逼 GC 跑的",排查时先看它;
  • 75% free, 13MB/53MB:回收后存活 13MB / 当前堆上限 53MB(上限由增长策略决定,见 B.6);
  • paused 两段 对应流程①③;total 是并发标记的墙钟时间------total 大不卡应用,paused 大才卡。

B.6 触发原因全表与堆增长策略

日志前缀 触发条件
Alloc 分配时堆触顶(最常见)
Background 进程空闲时的整理性 GC
NativeAlloc native 分配量过水位(Bitmap 像素计入)------本例唯一一次
Explicit 谁调了 System.gc()
CollectorTransition / Trim / HomogeneousSpaceCompact 前后台切换 / onTrimMemory / 碎片整理

堆上限不是写死的:ART 按 min-free/max-free/target-utilization(约 75% 目标利用率)在水位间伸缩;dalvik.vm.heapsize 是 growth limit,android:largeHeap=true 可申请 large limit。日志出现 Clamp target GC heap = 堆已顶到 limit 被强制压回------频繁出现即"分配速度超过回收速度"的实锤。

B.7 GC 与 native 的交界------一张位图的完整一生

c 复制代码
decode: Skia malloc 像素(native) + Java壳新建 + NativeAllocationRegistry登记
  ↓ 显示
View 持 Drawable 持 Bitmap壳 → 强引用可达 → GC 不收
  ↓ 换图/换页
壳不可达(没人引用) → 等待...(时长不定!)
  ↓ 某次 GC 发生
壳被回收 → NNR 回调 → 像素 free → Scudo 保留空闲页(PSS 可能不降) → 最终 madvise 还给内核

这五步解释了本例全部现象:常驻(第2步卡死)、滞后回落(第3步等 GC)、锯齿(第4步集中释放)、"GC 了也没降"(第5步分配器留页)。

B.8 GC 无能为力清单(对症下药)

现象 GC 为什么帮不上 正确手段
强引用缓存涨 可达=活着 缓存设上限/LRU/主动清理
Native 位图涨 像素不在管辖,壳还可达 查缓存复用、recycle、Glide
回收后 PSS 不降 Scudo 保留空闲页 正常,madvise 最终归还
频繁 GC 但内存平稳 分配速率高(密集 Alloc GC) Allocation Tracker/内存分析器找分配热点
WaitForGcToComplete 频现 GC 转换期间分配被阻塞 降低分配速率,查大对象频繁创建

B.9 排查 GC 行为的实操

bash 复制代码
# 只看目标进程的 GC 行(logcat 里 GC 行的 tag 是进程名)
adb logcat -s com.***.carapp
# 关注四点: 原因分布 / freed 均值 / used-size 水位走势 / paused 峰值
# 健康参考: 稳态 UI 应用每分钟个位数 GC; 每秒多次 = 分配速率异常

数据来源:wkk103.log(logcat,含 am_pss/GC/lmkd/应用日志)、5 次 dumpsys meminfo 抓取;系统侧链路健康性由 ConfigFlow 全链路打点另行验证(见《Android配置变更流程日志解析》)。

相关推荐
聚美智数1 小时前
快递拦截-物流在途拦截-在途拦截API接口介绍
android
Dreams_l1 小时前
如何保证RabbitMQ消息可靠传输
java·rabbitmq·java-rabbitmq
MacroZheng1 小时前
面试官皱眉:"你懂 Vibe Coding,那你说superpowers和grill-me怎么选?",我:"小孩才做选择,我全都要!"
java·人工智能·后端
flower_drop1 小时前
从 QtScrcpy 到 TabQA:如何在浏览器侧边栏搞定 Android 投屏与测试提单?
android
大模型码小白1 小时前
【AI大模型】DeepSeek Harness 深度解析:大模型评测框架的架构与实践
java·运维·人工智能·spring·架构·自动化
Mr.Java.1 小时前
本地测试全绿,上线 SQL 报错?Spring Boot 容器级 DT 测试实战指南(附Skills)
java·测试用例·springboot·testcontainers·dt·java测试skills·dt测试
维天说2 小时前
Agent会听人话,反而更难管
java·开发语言·人工智能
SamDeepThinking2 小时前
if嵌套最好控制在3层以内
java·后端·程序员
边境悍匪2 小时前
蜗牛学苑 Java 智能体学习 Day36|阿里云 OSS 文件上传、Spring 事务、全局异常思维导图复盘
java·学习·阿里云