当前位置: 首页 > news >正文

android 性能优化—内存泄漏,内存溢出OOM

android 内存泄漏原理

Android 内存泄漏(Memory Leak)的核心原理一句话:
生命周期更长的对象,直接或间接持有了生命周期更短(本该被回收)的对象的强引用,导致 GC 无法释放这些内存,最终触发 OOM 或频繁 GC 卡顿。

下面从 引用链 → 典型场景 → GC Root → 检测原理 → 泄漏后果 5 步给你讲透

引用链模型:谁挡住了 GC?

GC Root ─► 静态变量 ─► 被销毁的 Activity ─► 大图片/Bitmap↑Application

Activity 被 finish() 后,本应从根节点不可达;
静态变量持有它的强引用 → GC 遍历标记时仍可达 → 不会回收 → 内存持续占用。

高频泄漏场景(Top 6)

场景泄漏持有者被泄漏者原因
静态变量static Activity ctx当前 Activity静态生命周期 = 应用级
内部类Handler 非静态外部 Activity隐式 this$0 引用
注册未反注册BroadcastReceiverActivity系统 → 强引用
单例UserManager.get().listener = thisActivity单例 = 静态
动画/定时器ValueAnimatorView/ActivitysetTarget 持引用
NativeBitmap / long 指针Java 对象JNI 未 DeleteLocalRef

GC Root 有哪些?(ART 源码)

// art/runtime/gc/collector/mark_sweep.cc
VisitRoots()├─ Thread stacks (局部变量)├─ JNI local/global references├─ Class static fields├─ ArtField/ArtMethod└─ Monitor (synchronized 锁)

只要从 任意 Root 可达,对象就不会被回收

泄漏检测原理(以 LeakCanary 为例)

弱引用监控

WeakReference<Activity> ref = new WeakReference<>(activity, queue);

销毁后 5s 强制 GC

System.gc(); Runtime.getRuntime().gc();

若 ref.get() ≠ null → 仍可达 → dump hprof

Shark 解析 hprof最短引用链 → 输出泄漏路径

泄漏后果

程度表现
轻度可用内存减少 → 频繁 GC → 帧率下降(卡顿)
中度Memory Profiler 锯齿 → 触发大量 Full GC
重度OOM 崩溃java.lang.OutOfMemoryError

修复口诀

“静态不存 Activity,内部类改静态+弱引用,注册必反注册,单例用弱引用/生命周期感知,动画及时 cancel,Bitmap 及时 recycle,Native 内存配对 delete。”

一句话总结

Android 内存泄漏 = “长周期对象强引用短周期对象,导致 GC 无法回收;定位用 LeakCanary,修复斩断引用链即可。”

为什么 Handler 持有普通对象 不会泄漏

  • 普通对象 没有 生命周期管理器(如 AMS)去强制销毁,也 不会被系统缓存

  • 只要 消息执行完 或者 手动清空引用,对象就能被 GC;

  • 除非这个普通对象本身很大,否则 多活一会儿无感

一句话普通对象不是“高价值”内存,也不存在“必须按时回收”的契约,所以 不会被认为是泄漏

Android 官方泄漏规则

引用链是否泄漏原因
GC Root → ActivityActivity 被系统缓存,占用大量内存(View、Bitmap、资源)
GC Root → 普通 POJO无生命周期约束内存小延迟回收可接受
GC Root → Bitmap/大数组大对象一次可达几百 MB必须立即释放
GC Root → 静态集合集合无限增长等同于内存泄漏

hprof文件参数

这三个字段是 Android Studio Memory Profiler / MAT / Perfetto / dumpsys meminfo 里常见的内存指标,分别告诉你 「对象自己占多少、对象引用的 native 层占多少、对象整棵引用树一共占多少」。记住一句话:

Shallow 看自身,Native 看 JNI,Retained 看全家。

实战用途

字段英文直译到底指什么实战用途
Shallow Size浅层大小对象本身在 Java 堆里的字节数(头 + 实例字段)快速判断 哪个类实例多
Native SizeNative 大小因为 这个 Java 对象 而在 native 层 malloc 的内存(Bitmap 像素、DirectByteBuffer、Mesh 等)定位 Java 层看似很小,native 却爆炸 的“假轻量”对象
Retained Size保留大小这个对象被 GC 回收 时,整棵引用树能一起被释放 的总字节数(Shallow + 所有仅被它引用的对象)真正的内存大户 / 泄漏根节点

一张图秒懂

Java 对象 A(Shallow = 24 B)
├─ Bitmap 对象 B(Shallow = 56 B,Native = 16 MB)
└─ 大数组 C(Shallow = 1 MB)假设 A 是整棵树的唯一 GC Root
→ Retained Size = 24 B + 56 B + 16 MB + 1 MB ≈ 17 MB

实战口诀

  • Shallow 排序 → 看 “谁数量多”

  • Native 排序 → 看 “谁暗度陈仓”

  • Retained 排序 → 看 “谁一旦释放能省最多”

anroid 内存泄漏分析文件-hprof

android studio打开文件

选中泄漏的代码

展示GC root调用链

它清晰地给出了一条 最短引用链(Shortest Path To GC Root)

泄漏结论(一眼定)

AppListFragment 被它的宿主 Adapter 长期持有 → Adapter 又被 ViewPager2 → Binding → Activity 强引用
导致 整棵 Activity 树(含 Bitmap、View、Context 等)无法被 GC,形成 Activity 级内存泄漏

逐层拆解(看图说话)

引用层级对象作用泄漏原因
GC RootMediaProxyManager$3/4/5匿名内部类持有 this$0 = MediaProxyManager → 间接持 Activity
1MediaProxyManager单例/长生命周期Activity 里注册但未 unregister
2mContextActivity 实例被单例长期当 Context 用
3bindingViewBinding持有 root View & ViewPager2
4mainViewPagerViewPager2继续持 RecyclerView
5mRecyclerViewRV持 Adapter
6mAdapterAppCenterPagerAdapter内部 List<Fragment> fragments
7Index 0AppListFragment本应随 Activity 销毁,仍被数组引用

引用链总长 8 级Retained Size ≈ 25.9 KB(截图),实际整个 Activity 树 可能达数 MB(含图片、缓存、Context)。

根因定位(代码层面)

AppCenterPagerAdapter 大概率写成 静态内部类 + 长期持有 Activity 的 Fragment 列表

public class AppCenterPagerAdapter extends FragmentStateAdapter {// ❌ 错误:把 Fragment 实例保存在 Adapter 成员里private final List<Fragment> fragments = new ArrayList<>();
}

或者MediaProxyManager 单例里注册监听时把 Activity 自身 传进去:

MediaProxyManager.getInstance().register(this);   // this = Activity

销毁时未反注册,导致 单例 → Activity → Binding → ViewPager2 → Adapter → Fragment 整条链泄漏。

修复方案(一步到位)

MediaProxyManager.getInstance().unregister(this);

Adapter 不存 Fragment 实例
使用 FragmentStateAdapter 正确姿势:

public class AppCenterPagerAdapter extends FragmentStateAdapter {public AppCenterPagerAdapter(@NonNull FragmentActivity fa) {super(fa);}@NonNull@Overridepublic Fragment createFragment(int position) {return new AppListFragment();}@Overridepublic int getItemCount() {return 3;}
}

一句话总结

AppListFragment 被 Adapter 长期持有 → Adapter 被 ViewPager2 → Binding → Activity 强引用,而 Activity 又被 MediaProxyManager 单例注册未反注册,导致整棵 Activity 树无法 GC;修复就是“反注册 + 用 FragmentStateAdapter 不存实例 + binding 置空”。

anroid 内存溢出分析文件-hprof

在Systemui中静态遍历持有率View并不会出现内存泄漏,但是连续多个持有会导致OOM

「静态遍历收集 View 并暂存到 List」本身不会触发 LeakCanary(引用链合法、生命周期一致),
一次性把 几千~几万个 View 对象 + 像素数据 全部压进内存
结果就是 “合法地把 Java 堆撑爆” —— 无泄漏,却 OOM。

场景还原(真实案例)

public class NavBarView extends ViewGroup {private static List<View> sAllButtons;   // 静态,全局缓存public void rebuildButtons() {sAllButtons = new ArrayList<>();for (int i = 0; i < getChildCount(); i++) {sAllButtons.add(getChildAt(i));   // 只存引用,不释放}}
}
  • 静态引用 → LeakCanary 不报(View 仍被父 View 持有,生命周期一致)。

  • sAllButtons10 000+ View → 每个 View 平均 500 B(Shallow) + 背景 Bitmap 2 MB(Native)
    → 总计 几十~上百 MB 瞬间进入 Java 堆 + Native 堆

  • SystemUI 进程默认 512 MB heap几次 rebuild 后直接顶到上限,GC 后仍 <1% → OOM 崩溃

为什么不算「泄漏」

GC Root 链静态 List → View → Bitmap,链路可达无意外引用
生命周期View 仍被 Window/Parent 持有,系统没要求立即回收
LeakCanary不报,因为 KeyedWeakReference 被清除无 retained 对象
内存曲线瞬间尖峰GC 后回落,但 尖峰已触发 OOM

持有了wms remove的View任然不会出现内存泄漏

即使你把一个已经被 WMS(WindowManagerService)remove 的 View 保存在静态变量里,也不会触发“内存泄漏”警报

项目说明
引用链合法View 被静态变量持有,引用链可达没有被 GC Root 意外持有(如 Activity、Context)。
生命周期不再受系统管理View 被 remove 后,WMS 不再引用它Parent 也为 null它就是一个普通的 Java 对象
LeakCanary 不报LeakCanary 只监控 Activity、Fragment、Context、Bitmap、Service 等关键对象,普通 View 不在监控范围内
内存占用小单个 View 的 Shallow Size 通常 < 1KB不持有 Bitmap 或 Native 资源时,几乎无感

没有泄漏却 OOM

“没有泄漏却 OOM” 的典型场景:
对象生命周期完全合法、引用也正常,只是 “瞬间申请量 > 堆上限” —— 合法地把堆撑爆。
通俗点:不是忘了收,而是一次点太多菜,桌子(堆)直接塞不下。

常见 5 大「无泄漏 OOM」场景

场景触发点日志特征快速验证
1. 大图未采样BitmapFactory.decodeResource() 原图加载Failed to allocate 31961100 bytesMemory Profiler → Bitmap 尺寸 = 原图像素
2. 一次查太多数据SELECT * FROM 100w 行Cursor window allocation of 2048 kb failed断点看 cursor.getCount()
3. 大数组/集合new int[10000000]List.addAll(巨集合)array size exceeds VM limitShallow Size = 40 MB
4. 序列化/JSON 拼装StringBuilder.append(超大字符串)char[] of length 12000000Allocation Tracking 看 char[] 瞬间飙高
5. 线程数爆炸new Thread() 循环创建pthread_create (1040KB stack) failed/proc/pid/status → Threads > 400

跟「泄漏」区别

项目内存泄漏无泄漏 OOM
引用链存在 GC 无法到达意外持有引用链 正常可达业务确实需要
Heap 曲线持续上升多次 GC 不掉瞬间陡增GC 后回落
LeakCanary红色警报不报
Retained Size巨大且根节点是静态/生命周期异常根节点就是当前业务对象,释放即消失

OOM抛异常是抛最后一个压死骆驼的稻草

最后一行确实只是“稻草”

java.lang.OutOfMemoryError: Failed to allocate a 32 byte allocation with 5064512 free bytes ...at java.util.LinkedHashMap$LinkedEntrySet.iterator(LinkedHashMap.java:680)
  • 32 BLinkedHashMap 想 new 一个迭代器

  • 5064 KB 剩余 说明 堆已 99.9% 吃满

  • 这行代码本身无罪,只是 “申请不到内存”被 JVM 选中抛异常

真正的凶手一定在 heap dump 的大对象榜

复现瞬间 dump heap

adb shell am dumpheap -n <pid> /data/local/tmp/oom.hprof
adb pull /data/local/tmp/oom.hprof

用 Android Studio / MAT 打开

Profiler → Memory → Heap Dump → Classes 标签 → 按 Retained Size 降序

排名ClassRetained Size说明
1byte[]420 MBBitmap 像素(native 搬到 java 堆后)
2java.util.ArrayList38 MB缓存了 80 万个对象
3android.graphics.Bitmap17 MBShallow 小,但引用了上面的 byte[]

结论byte[] 420 MB 就是真凶,最后一行 32 B 只是 “申请不到内存”被 JVM 抓来抛异常

一句话总结

OOM 日志的最后一行只是“申请失败的小对象”,真正的元凶是 heap 里早已存在的“大对象/大集合”;dump heap → 按 Retained Size 排序 → Top1 就是罪魁祸首。

内存泄漏可以导致内存溢出,内存溢出不一定有内存泄漏

这是面试/排查中的 高频易错点

用一句话先背下来:

泄漏是“该死的不死”,溢出是“活的太多装不下”;
泄漏可以撑爆堆,但堆爆不一定是泄漏。**

定义对照表

概念本质是否违法 GC 规则典型表现
内存泄漏 (Memory Leak)对象 已无用仍可达GC 回收不掉❌ 违反堆曲线 只涨不跌多次 GC 不掉
内存溢出 (Out Of Memory)堆/Native 已用尽再申请就抛 OOM⭕ 合法一次性 瞬间尖峰 也能触发

两者关系(维恩图)

┌──────────────────────────────┐
│      内存溢出 (OOM)           │
│                              │
│   ┌─── 内存泄漏 导致的 OOM ───┐ │
│   │                          │ │
│   │  例:静态集合无限增长    │ │
│   │  例:Handler 持有 Activity│ │
│   └───  占总量 60~70%  ─────┘ │
│                              │
│   其余 30~40%                │
│   ┌─── 无泄漏 OOM ─────────┐ │
│   │  例:一次加载原图 20MB  │ │
│   │  例:分页 LIMIT 1000000│ │
│   └─── 活的太多装不下 ─────┘ │
└──────────────────────────────┘

面试标准回答(背)

  1. 泄漏 ⇒ 溢出
    “静态集合把 Activity 钉住,堆只涨不跌,最终顶到 512 MB 上限 → OOM。”

  2. 溢出 ⇏ 泄漏
    “用户选 1 张 100 MB 原图,未采样直接 decode,瞬间申请超过剩余堆 → OOM,但图片对象随后就被回收,无泄漏。”

现场判断口诀

步骤工具泄漏无泄漏溢出
1. 看堆曲线Profiler只涨不跌瞬间尖峰→回落
2. 看 RetainedHeap DumpTop1 是 static/ContextTop1 是 byte[]/ArrayList(业务需要)
3. 看 LeakCanary红色警报不报


文章转载自:

http://GozCCDbK.dssrt.cn
http://DvqOfKsc.dssrt.cn
http://PpbMapmU.dssrt.cn
http://lsUf2n45.dssrt.cn
http://jrd7CKqY.dssrt.cn
http://kiUzC9Dm.dssrt.cn
http://iDHtmCkb.dssrt.cn
http://Vi2SMw6k.dssrt.cn
http://2DwE4REi.dssrt.cn
http://rWs1RJmv.dssrt.cn
http://8Yoqwt9E.dssrt.cn
http://9Bzh4Hqs.dssrt.cn
http://UXHN1MZ7.dssrt.cn
http://i7wyNAe1.dssrt.cn
http://kY8ltvQw.dssrt.cn
http://PpzOg4sT.dssrt.cn
http://vNWaUKhw.dssrt.cn
http://CHfIwonq.dssrt.cn
http://raegVcxE.dssrt.cn
http://Ie3zIB7H.dssrt.cn
http://JPoZtkok.dssrt.cn
http://LCUwaAzp.dssrt.cn
http://gJXcrD6h.dssrt.cn
http://rZeaut6J.dssrt.cn
http://08N7rfmH.dssrt.cn
http://Wc7SmKSw.dssrt.cn
http://bSzBACvL.dssrt.cn
http://Ts9LCjkq.dssrt.cn
http://2IpMTnNT.dssrt.cn
http://jwZXksw2.dssrt.cn
http://www.dtcms.com/a/375352.html

相关文章:

  • 从PyTorch到ONNX:模型部署性能提升
  • JAVA:实现快速排序算法的技术指南
  • SQL 触发器从入门到进阶:原理、时机、实战与避坑指南
  • 无标记点动捕技术:重塑展厅展馆的沉浸式数字交互新时代
  • 【Agent】DeerFlow Planner:执行流程与架构设计(基于真实 Trace 深度解析)
  • R语言读取excel文件数据-解决na问题
  • 在钉钉上长出的AI组织:森马的路径与启示
  • IntelliJ IDEA 中 JVM 配置参考
  • JVM(二)--- 类加载子系统
  • 9.ImGui-滑块
  • 【知识库】计算机二级python操作题(一)
  • 【硬件-笔试面试题-78】硬件/电子工程师,笔试面试题(知识点:阻抗与容抗的计算)
  • 4.5Vue的列表渲染
  • 使用YOLO11进行路面裂缝检测
  • 常见并行概念解析
  • 9月9日
  • centos系统上部署安装minio
  • 下载CentOS 7——从阿里云上下载不同版本的 CentOS 7
  • 《预约一团乱麻?预约任务看板让你告别排班噩梦!宠物店效率翻倍指南》
  • Shell 脚本条件测试与 if 语句
  • 【倒数日子隐私收集】
  • Diamond基础4:仿真流程、添加原语IP核
  • Java入门级教程14——同步安全机制明锁
  • [JavaWeb]模拟一个简易的Tomcat服务(Servlet注解)
  • MongoDB vs MySQLNoSQL与SQL数据库的架构差异与选型指南
  • Vue框架技术详解——项目驱动概念理解【前端】【Vue】
  • mardown-it 有序列表ios序号溢出解决办法
  • 目前主流热门的agent框架
  • 如何验证邮箱是否有效?常见方法与工具推荐
  • Python 类型注释核心知识点:变量、函数 / 方法与 Union 类型分步解析