线上图片详情页连续旋转几次后,旧 Activity 已经退出,内存曲线却没有回落;随后列表继续加载大图,ART 无法满足一次分配时会抛出 OutOfMemoryError,若错误未被处理,应用进程通常会崩溃。Android 也可能在独立的系统内存压力策略下杀死后台进程,但那不是这次 Java 分配失败本身。表面现象是“图片太大”,真正要先回答的问题是:旧页面为什么仍然从某个 GC Root 可达,谁又在持续制造分配压力?
一句话结论
Android OOM 不是“堆变大”这一条结论,而是进程在 ART 的可用内存、回收时机和分配压力约束下无法再满足一次分配;先沿最短路径找出 GC Root 到对象的持有链,再分别修正生命周期所有权、缓存上限和图片解码策略。
版本与边界: 本文用 Java 8 语言模型解释引用与堆栈,并以现代 Android/ART 的概念行为为边界;ART 的具体 GC 算法、分代策略、堆上限与厂商调度会随 Android 版本、设备和 OEM 实现变化。Zygote、ART、Activity、Bitmap、LeakCanary 风格的引用观察与 Android Studio Memory Profiler 均是 Android 语境;它们不能被简单等同于桌面 HotSpot JVM 的实现细节。
先把一次泄漏画成可达性
Java 的 GC 判定不是“对象多久没用”,而是从一组根出发,看对象是否仍可达。heap dump 常见的根类别包括线程栈、静态或系统类字段、JNI 全局引用和运行中的线程;具体名称以分析器输出为准。系统服务对象可以出现在一条保留路径上,却不是可与这些类别并列的通用 GC Root 类别。
#mermaid-svg-TlVjdETfPesuSdtE{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-TlVjdETfPesuSdtE .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-TlVjdETfPesuSdtE .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-TlVjdETfPesuSdtE .error-icon{fill:#552222;}#mermaid-svg-TlVjdETfPesuSdtE .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-TlVjdETfPesuSdtE .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-TlVjdETfPesuSdtE .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-TlVjdETfPesuSdtE .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-TlVjdETfPesuSdtE .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-TlVjdETfPesuSdtE .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-TlVjdETfPesuSdtE .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-TlVjdETfPesuSdtE .marker{fill:#333333;stroke:#333333;}#mermaid-svg-TlVjdETfPesuSdtE .marker.cross{stroke:#333333;}#mermaid-svg-TlVjdETfPesuSdtE svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-TlVjdETfPesuSdtE p{margin:0;}#mermaid-svg-TlVjdETfPesuSdtE .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-TlVjdETfPesuSdtE .cluster-label text{fill:#333;}#mermaid-svg-TlVjdETfPesuSdtE .cluster-label span{color:#333;}#mermaid-svg-TlVjdETfPesuSdtE .cluster-label span p{background-color:transparent;}#mermaid-svg-TlVjdETfPesuSdtE .label text,#mermaid-svg-TlVjdETfPesuSdtE span{fill:#333;color:#333;}#mermaid-svg-TlVjdETfPesuSdtE .node rect,#mermaid-svg-TlVjdETfPesuSdtE .node circle,#mermaid-svg-TlVjdETfPesuSdtE .node ellipse,#mermaid-svg-TlVjdETfPesuSdtE .node polygon,#mermaid-svg-TlVjdETfPesuSdtE .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-TlVjdETfPesuSdtE .rough-node .label text,#mermaid-svg-TlVjdETfPesuSdtE .node .label text,#mermaid-svg-TlVjdETfPesuSdtE .image-shape .label,#mermaid-svg-TlVjdETfPesuSdtE .icon-shape .label{text-anchor:middle;}#mermaid-svg-TlVjdETfPesuSdtE .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-TlVjdETfPesuSdtE .rough-node .label,#mermaid-svg-TlVjdETfPesuSdtE .node .label,#mermaid-svg-TlVjdETfPesuSdtE .image-shape .label,#mermaid-svg-TlVjdETfPesuSdtE .icon-shape .label{text-align:center;}#mermaid-svg-TlVjdETfPesuSdtE .node.clickable{cursor:pointer;}#mermaid-svg-TlVjdETfPesuSdtE .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-TlVjdETfPesuSdtE .arrowheadPath{fill:#333333;}#mermaid-svg-TlVjdETfPesuSdtE .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-TlVjdETfPesuSdtE .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-TlVjdETfPesuSdtE .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-TlVjdETfPesuSdtE .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-TlVjdETfPesuSdtE .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-TlVjdETfPesuSdtE .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-TlVjdETfPesuSdtE .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-TlVjdETfPesuSdtE .cluster text{fill:#333;}#mermaid-svg-TlVjdETfPesuSdtE .cluster span{color:#333;}#mermaid-svg-TlVjdETfPesuSdtE div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-TlVjdETfPesuSdtE .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-TlVjdETfPesuSdtE rect.text{fill:none;stroke-width:0;}#mermaid-svg-TlVjdETfPesuSdtE .icon-shape,#mermaid-svg-TlVjdETfPesuSdtE .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-TlVjdETfPesuSdtE .icon-shape p,#mermaid-svg-TlVjdETfPesuSdtE .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-TlVjdETfPesuSdtE .icon-shape .label rect,#mermaid-svg-TlVjdETfPesuSdtE .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-TlVjdETfPesuSdtE .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-TlVjdETfPesuSdtE .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-TlVjdETfPesuSdtE :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
GC Root:主线程栈
Activity:图片详情页
GC Root:静态字段
Singleton:全局管理器
GC Root:JNI 全局引用
Native:跨边界持有
Callback:匿名内部回调
Activity:旋转前页面
View:图片容器
Bitmap:解码后的像素
Bitmap:旧页面像素
ART GC:标记可达对象
谁创建:系统创建进程、线程和页面,业务代码创建单例、回调与位图;谁持有:GC Root 直接或间接持有可达对象;何时触发:旋转后旧页面应失去所有权,但长期单例中的回调仍然指向它;结果交给谁:GC 只会回收不可达对象,因此旧 Activity 与其 Bitmap 会继续占用内存。这里的 GC Root 是分析术语,不是一个可由应用随意 new 出来的 Java 类。
JVM 概念如何落到 ART 进程
类加载、栈与堆
Java 类从字节码被类加载器加载、链接并初始化后,静态字段与类型元数据按运行时实现管理。Android APK 中的 DEX 由 ART 执行或 AOT/JIT 编译;这保留了 Java 的“对象通过引用访问”和“方法调用有栈帧”的学习模型,但不意味着 ART 使用 HotSpot 的同一内存布局或同一回收器。
- Java 栈: 每个线程的调用栈保存栈帧、局部变量、参数和返回地址。栈帧里的引用可以让堆对象保持可达;栈溢出和堆 OOM 是不同问题。
- Java 堆: new 出来的对象通常由运行时堆管理;数组、集合节点、页面树及 Java 层的 Bitmap 包装对象都体现为对象引用关系。堆增长本身只是现象,可能是正常缓存、一次性峰值、碎片或真正的保留链。
- 对象引用: 变量保存的是对象引用,不是对象副本。把 Activity 传给长生命周期对象,相当于把“这个页面活多久”的决定权交给了更长的所有者。
Zygote、ART 与应用进程边界
Android 启动应用时,常见路径是 Zygote 预加载一部分运行时和框架资源,再 fork 出应用进程;应用进程在 ART 中运行自己的 Java/Kotlin 代码、线程和堆。预加载与 copy-on-write 有助于启动和共享,但它不让应用随意共享另一个进程的 Java 堆,也不替应用修复泄漏。
来源事实: ART 是 Android 的应用运行时,Zygote 是创建应用进程的重要系统机制。 生产建议: 把“应用进程可能被杀后重建”视为常态,不把单例缓存当成可靠持久化;同时把“同一进程内静态字段能跨旋转存活”当成泄漏审查点。
引用强度不是万能开关
| 强引用 | 只要从 GC Root 链路可达,对象通常不会被回收 | 默认业务对象、明确所有权 | 生命周期设计;错误强引用仍会泄漏 |
| 软引用 | 内存紧张时更可能被回收,具体时机不应作为业务契约 | 少量可重建、非关键的内存提示数据 | 有界图片缓存;回收不可预测 |
| 弱引用 | 没有强引用时可在后续 GC 回收 | 观察器、映射键等特定模式 | 通用泄漏修复;可能过早失效、掩盖所有权错误 |
| 虚引用 | 不能通过它取得对象,只能配合 ReferenceQueue 观察回收后处理 | 资源清理通知与诊断基础设施 | Java 业务对象的正常访问与及时关闭 |
弱引用只能改变“这条边是否保活”,不能取消已经排队的任务、不能保证回调在主线程、也不能让资源马上释放。一个回调即使只弱持有 Activity,仍可能持有大数组、Bitmap 或错误地在页面已停止后更新 UI。生产代码应先建立明确的取消和解绑边界,再在确实需要观察而不拥有对象时使用弱引用。
旋转时,长生命周期回调怎样留下旧页面
配置变化会销毁并重建 Activity 实例。若单例请求管理器保存一个匿名内部回调,匿名内部类默认可能捕获外部 Activity;只要请求、线程、队列或缓存仍持有该回调,旧实例就仍可达。
#mermaid-svg-vwmNX8oM8ndyQD9F{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-vwmNX8oM8ndyQD9F .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-vwmNX8oM8ndyQD9F .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-vwmNX8oM8ndyQD9F .error-icon{fill:#552222;}#mermaid-svg-vwmNX8oM8ndyQD9F .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-vwmNX8oM8ndyQD9F .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-vwmNX8oM8ndyQD9F .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-vwmNX8oM8ndyQD9F .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-vwmNX8oM8ndyQD9F .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-vwmNX8oM8ndyQD9F .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-vwmNX8oM8ndyQD9F .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-vwmNX8oM8ndyQD9F .marker{fill:#333333;stroke:#333333;}#mermaid-svg-vwmNX8oM8ndyQD9F .marker.cross{stroke:#333333;}#mermaid-svg-vwmNX8oM8ndyQD9F svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-vwmNX8oM8ndyQD9F p{margin:0;}#mermaid-svg-vwmNX8oM8ndyQD9F .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-vwmNX8oM8ndyQD9F .cluster-label text{fill:#333;}#mermaid-svg-vwmNX8oM8ndyQD9F .cluster-label span{color:#333;}#mermaid-svg-vwmNX8oM8ndyQD9F .cluster-label span p{background-color:transparent;}#mermaid-svg-vwmNX8oM8ndyQD9F .label text,#mermaid-svg-vwmNX8oM8ndyQD9F span{fill:#333;color:#333;}#mermaid-svg-vwmNX8oM8ndyQD9F .node rect,#mermaid-svg-vwmNX8oM8ndyQD9F .node circle,#mermaid-svg-vwmNX8oM8ndyQD9F .node ellipse,#mermaid-svg-vwmNX8oM8ndyQD9F .node polygon,#mermaid-svg-vwmNX8oM8ndyQD9F .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-vwmNX8oM8ndyQD9F .rough-node .label text,#mermaid-svg-vwmNX8oM8ndyQD9F .node .label text,#mermaid-svg-vwmNX8oM8ndyQD9F .image-shape .label,#mermaid-svg-vwmNX8oM8ndyQD9F .icon-shape .label{text-anchor:middle;}#mermaid-svg-vwmNX8oM8ndyQD9F .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-vwmNX8oM8ndyQD9F .rough-node .label,#mermaid-svg-vwmNX8oM8ndyQD9F .node .label,#mermaid-svg-vwmNX8oM8ndyQD9F .image-shape .label,#mermaid-svg-vwmNX8oM8ndyQD9F .icon-shape .label{text-align:center;}#mermaid-svg-vwmNX8oM8ndyQD9F .node.clickable{cursor:pointer;}#mermaid-svg-vwmNX8oM8ndyQD9F .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-vwmNX8oM8ndyQD9F .arrowheadPath{fill:#333333;}#mermaid-svg-vwmNX8oM8ndyQD9F .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-vwmNX8oM8ndyQD9F .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-vwmNX8oM8ndyQD9F .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vwmNX8oM8ndyQD9F .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-vwmNX8oM8ndyQD9F .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vwmNX8oM8ndyQD9F .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-vwmNX8oM8ndyQD9F .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-vwmNX8oM8ndyQD9F .cluster text{fill:#333;}#mermaid-svg-vwmNX8oM8ndyQD9F .cluster span{color:#333;}#mermaid-svg-vwmNX8oM8ndyQD9F div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-vwmNX8oM8ndyQD9F .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-vwmNX8oM8ndyQD9F rect.text{fill:none;stroke-width:0;}#mermaid-svg-vwmNX8oM8ndyQD9F .icon-shape,#mermaid-svg-vwmNX8oM8ndyQD9F .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vwmNX8oM8ndyQD9F .icon-shape p,#mermaid-svg-vwmNX8oM8ndyQD9F .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-vwmNX8oM8ndyQD9F .icon-shape .label rect,#mermaid-svg-vwmNX8oM8ndyQD9F .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vwmNX8oM8ndyQD9F .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-vwmNX8oM8ndyQD9F .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-vwmNX8oM8ndyQD9F :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
系统:发生配置变化
Activity:旧实例执行销毁流程
Activity:创建新实例
Singleton:请求管理器
Request:尚未完成
Callback:捕获旧 Activity
Activity:旋转前实例
View:旧视图树
Bitmap:旧图片
Activity:旋转后实例
GC Root:静态单例字段
谁创建:系统创建新旧页面,单例创建或接收请求与回调;谁持有:静态字段到单例、请求到回调、回调到旧页面;何时触发:旋转后未取消或未清理的异步工作完成前;结果交给谁:回调原本想把结果交给页面,实际却把旧页面的生命周期延长了。不要把 onDestroy() 当成唯一保险:多窗口、导航、临时不可见和状态保存都要求依据工作语义选择 onStop()、onCleared()、协程/请求取消或明确的订阅解绑。
谁负责创建、持有、触发与接收
| Activity | Android 框架 | 系统任务栈与当前界面流程 | 生命周期回调、配置变化、用户交互 | 将 UI 事件交给 ViewModel、控制器或请求层 |
| 单例管理器 | Application 初始化或首次访问代码 | 静态字段或 DI 单例作用域 | 请求提交、监听注册、缓存读取 | 只能交付给仍有效的订阅者,不能默认拥有 Activity |
| Bitmap 缓存 | 图片加载层或业务缓存 | 有界 LruCache 或明确的请求所有者 | 解码完成、命中与逐出 | 向正在显示且仍有效的 UI 提供像素 |
| ART GC | Android 运行时 | 由 GC Root 可达关系决定,不是业务对象手工持有 | 分配压力与运行时调度共同触发 | 回收不可达对象及其可回收内存 |
| LeakCanary 风格观察器 | 调试库在页面销毁后注册观察 | 观察器持有弱引用和引用队列等诊断状态 | 预期应回收的对象仍长时间可达时 | 向开发者报告最短保留路径与疑似根 |
LeakCanary 风格的机制是“观察本应死亡的对象是否仍被保留”,不是替代内存模型。Android Studio Memory Profiler 更适合看分配、对象数量、堆转储与引用链;两者都需要结合操作路径、设备内存档位和真实图片尺寸判断。诊断工具给出的是线索,所有权模型才是修复。
分配压力、Bitmap 与 OOM 诊断链路
大图是最典型的分配压力来源之一。以 ARGB_8888 粗略估算,width * height * 4 字节的像素就足以让全尺寸相机图迅速占满预算;实际占用还受采样、色彩空间、硬件位图、解码器、缩放副本和实现版本影响。旧 Android 版本与现代版本对像素存储位置和 Bitmap 实现细节不同:Java 包装对象会参与 ART 堆的可达性分析,像素或图形资源的实际分配也可能涉及运行时、native 或图形实现。因此不要用“像素一定只在 native”或“一定只在 Java heap”概括所有设备。
#mermaid-svg-X5g9zcvF5RSqGJgK{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-X5g9zcvF5RSqGJgK .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-X5g9zcvF5RSqGJgK .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-X5g9zcvF5RSqGJgK .error-icon{fill:#552222;}#mermaid-svg-X5g9zcvF5RSqGJgK .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-X5g9zcvF5RSqGJgK .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-X5g9zcvF5RSqGJgK .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-X5g9zcvF5RSqGJgK .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-X5g9zcvF5RSqGJgK .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-X5g9zcvF5RSqGJgK .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-X5g9zcvF5RSqGJgK .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-X5g9zcvF5RSqGJgK .marker{fill:#333333;stroke:#333333;}#mermaid-svg-X5g9zcvF5RSqGJgK .marker.cross{stroke:#333333;}#mermaid-svg-X5g9zcvF5RSqGJgK svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-X5g9zcvF5RSqGJgK p{margin:0;}#mermaid-svg-X5g9zcvF5RSqGJgK .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-X5g9zcvF5RSqGJgK .cluster-label text{fill:#333;}#mermaid-svg-X5g9zcvF5RSqGJgK .cluster-label span{color:#333;}#mermaid-svg-X5g9zcvF5RSqGJgK .cluster-label span p{background-color:transparent;}#mermaid-svg-X5g9zcvF5RSqGJgK .label text,#mermaid-svg-X5g9zcvF5RSqGJgK span{fill:#333;color:#333;}#mermaid-svg-X5g9zcvF5RSqGJgK .node rect,#mermaid-svg-X5g9zcvF5RSqGJgK .node circle,#mermaid-svg-X5g9zcvF5RSqGJgK .node ellipse,#mermaid-svg-X5g9zcvF5RSqGJgK .node polygon,#mermaid-svg-X5g9zcvF5RSqGJgK .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-X5g9zcvF5RSqGJgK .rough-node .label text,#mermaid-svg-X5g9zcvF5RSqGJgK .node .label text,#mermaid-svg-X5g9zcvF5RSqGJgK .image-shape .label,#mermaid-svg-X5g9zcvF5RSqGJgK .icon-shape .label{text-anchor:middle;}#mermaid-svg-X5g9zcvF5RSqGJgK .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-X5g9zcvF5RSqGJgK .rough-node .label,#mermaid-svg-X5g9zcvF5RSqGJgK .node .label,#mermaid-svg-X5g9zcvF5RSqGJgK .image-shape .label,#mermaid-svg-X5g9zcvF5RSqGJgK .icon-shape .label{text-align:center;}#mermaid-svg-X5g9zcvF5RSqGJgK .node.clickable{cursor:pointer;}#mermaid-svg-X5g9zcvF5RSqGJgK .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-X5g9zcvF5RSqGJgK .arrowheadPath{fill:#333333;}#mermaid-svg-X5g9zcvF5RSqGJgK .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-X5g9zcvF5RSqGJgK .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-X5g9zcvF5RSqGJgK .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-X5g9zcvF5RSqGJgK .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-X5g9zcvF5RSqGJgK .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-X5g9zcvF5RSqGJgK .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-X5g9zcvF5RSqGJgK .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-X5g9zcvF5RSqGJgK .cluster text{fill:#333;}#mermaid-svg-X5g9zcvF5RSqGJgK .cluster span{color:#333;}#mermaid-svg-X5g9zcvF5RSqGJgK div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-X5g9zcvF5RSqGJgK .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-X5g9zcvF5RSqGJgK rect.text{fill:none;stroke-width:0;}#mermaid-svg-X5g9zcvF5RSqGJgK .icon-shape,#mermaid-svg-X5g9zcvF5RSqGJgK .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-X5g9zcvF5RSqGJgK .icon-shape p,#mermaid-svg-X5g9zcvF5RSqGJgK .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-X5g9zcvF5RSqGJgK .icon-shape .label rect,#mermaid-svg-X5g9zcvF5RSqGJgK .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-X5g9zcvF5RSqGJgK .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-X5g9zcvF5RSqGJgK .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-X5g9zcvF5RSqGJgK :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
否
是
是
否
仍不足
用户操作:滚动图片列表
图片解码:创建 Bitmap
运行时与图形分配:对象与像素
分配压力是否升高
UI:显示图片
ART GC:尝试回收不可达对象
可用空间是否足够
Profiler:抓取分配与堆转储
GC Root 路径:确认保留链
修复:解绑、取消、限缓存、采样
重新测量:同一操作路径
OOM:本次分配失败
这里 System.gc() 不是修复手段。它最多是提示运行时尝试一次回收,不能回收仍被引用的对象,不能保证时间点,还可能扰乱性能测试。优先减少一次分配、按显示尺寸采样、设置缓存上限,并验证取消后旧页面不再接收结果。
Java-first 简化示例:从错误捕获到显式解绑
下面第一段展示泄漏风险:静态执行器持有任务,任务持有匿名回调,回调隐式捕获 Activity。第二段把回调变成可解绑的监听器,并在页面停止时取消任务。它是教学性简化,不是图片加载框架。
final class ImageJobs {
static final ExecutorService EXECUTOR = Executors.newSingleThreadExecutor();
static void loadLeaky(Activity activity, String url) {
EXECUTOR.execute(() -> {
Bitmap bitmap = decode(url);
activity.runOnUiThread(() -> activity.showBitmap(bitmap));
});
}
}
activity 被工作任务和 UI lambda 捕获;若解码慢、队列拥塞或请求无法取消,旋转后的旧页面会被单例式执行器路径继续保活。
final class ImageRequest {
private final ExecutorService executor;
private final Handler mainHandler = new Handler(Looper.getMainLooper());
// start、stop 与 deliverSuccess 只在主线程调用。
private final Map<RequestToken, Listener> listeners = new HashMap<>();
private RequestToken active;
private Future<?> running;
private long nextRequestId;
interface Listener {
void onBitmap(Bitmap bitmap);
}
ImageRequest(ExecutorService executor) {
this.executor = executor;
}
void start(String url, Listener listener) {
requireMainThread();
stop();
RequestToken token = new RequestToken(++nextRequestId);
active = token;
listeners.put(token, listener);
Handler handler = mainHandler;
WeakReference<ImageRequest> owner = new WeakReference<>(this);
running = executor.submit(() -> {
Bitmap bitmap = decode(url);
if (Thread.currentThread().isInterrupted() || token.cancelled) return;
handler.postAtTime(
new Delivery(owner, token, bitmap), token, SystemClock.uptimeMillis()
);
});
}
void stop() {
requireMainThread();
RequestToken token = active;
Future<?> task = running;
active = null; // 先失效,已入队的交付会在主线程闸门失败。
running = null;
if (token != null) {
token.cancelled = true;
listeners.remove(token);
mainHandler.removeCallbacksAndMessages(token);
}
if (task != null) task.cancel(true);
}
private void deliverSuccess(RequestToken token, Bitmap bitmap) {
requireMainThread();
if (active != token || token.cancelled) return;
Listener listener = listeners.remove(token);
active = null;
running = null;
if (listener != null) listener.onBitmap(bitmap);
}
private static final class RequestToken {
final long id;
volatile boolean cancelled;
RequestToken(long id) {
this.id = id;
}
}
private static final class Delivery implements Runnable {
private final WeakReference<ImageRequest> owner;
private final RequestToken token;
private final Bitmap bitmap;
Delivery(WeakReference<ImageRequest> owner, RequestToken token, Bitmap bitmap) {
this.owner = owner;
this.token = token;
this.bitmap = bitmap;
}
@Override public void run() {
ImageRequest request = owner.get();
if (request != null) request.deliverSuccess(token, bitmap);
}
}
private static void requireMainThread() {
if (Looper.myLooper() != Looper.getMainLooper()) {
throw new IllegalStateException("Call from the main thread only");
}
}
}
final class PhotoActivity extends Activity {
private ImageRequest request;
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
request = new ImageRequest(ImageJobs.EXECUTOR);
}
@Override protected void onStart() {
super.onStart();
request.start("photo", this::showBitmap); // deliverSuccess 已在主线程运行。
}
@Override protected void onStop() {
request.stop();
super.onStop();
}
}
简化说明: 这段伪代码用“主线程令牌闸门”表达关键边界:stop() 先令旧 token 失效、清除 listener 并移除已入队的 token 回调;即使不响应中断的解码在此后投递,deliverSuccess 也会在主线程拒绝旧 token。工作线程与已入队 Delivery 只弱引用 ImageRequest,不强持有页面;真正的所有权修复是主线程的显式取消和解绑,弱引用不是通用泄漏修复。
它故意省略 native allocations、OEM GC scheduling、完整 heap-dump 解析、image decode configuration、真实图片库的请求合并、主线程串行状态机的完整错误分支、缓存逐出、错误传播和竞争条件的全部处理。cancel(true) 只请求中断,不能保证不合作的解码立即停止;真实项目优先使用具备生命周期整合、请求取消、尺寸采样与缓存策略的成熟图片库,并让 ViewModel/界面层明确谁订阅、谁取消、谁显示。
常见误判与修复方向
- 静态字段、单例或 Application 对象不能保存 Activity、View 或页面回调;需要 Context 时优先确认是否只需 applicationContext。
- 匿名内部类、lambda 与延迟任务可能捕获外部对象;查看实际闭包和队列所有者,不只看代码表面。
- 只依赖 onDestroy() 往往太晚或覆盖不足;按工作是否应在不可见时停止来选择清理边界,并保证可重复调用。
- 不要把弱引用批量贴到所有监听器上。它会让回调消失、状态更难推理,却没有处理请求取消、缓存或大对象保留。
- 不要把 System.gc() 放进滚动、图片加载或“内存修复”逻辑;测量、限额和所有权修复才可验证。
- 不要长时间保留原始大 Bitmap;按显示容器采样、避免无意义副本、限制缓存并在压力设备上复测。
- 堆持续增长不等于已确认泄漏。对比重复操作后的稳定趋势、对象计数、强制转储中的最短 GC Root 路径和缓存预期,才下结论。
可动手改造的练习:让旧 Activity 留下来,再放它走
决策清单
- 这条引用的拥有者比 Activity 活得更久吗?若是,为什么它需要页面对象而不是数据、ID、applicationContext 或可取消订阅?
- 请求、回调、线程队列和缓存分别由谁停止或清空?配置变化后是否仍有旧订阅者?
- Bitmap 是否按显示尺寸解码,缓存是否有明确上限和逐出策略?
- Heap dump 中旧对象的最短 GC Root 路径是什么?它与预期缓存是否一致?
- 是否区分了正常峰值、缓存增长、分配抖动与确定的保留泄漏?
- 修复是否在真实设备内存档位、重复旋转和列表滚动下重新测量?
面试表达与下一站
面试表达: “Java 的对象是否可回收取决于从 GC Root 的可达性,不取决于变量是否离开视野。Android 上我会区分 ART 与桌面 JVM 的实现边界:Zygote fork 出应用进程,ART 在分配压力下调度 GC;若单例、线程或回调仍持有旋转前 Activity,旧视图和 Bitmap 就不会回收。排查时我用 Memory Profiler 或 LeakCanary 风格的观察找到最短保留路径,再通过取消、解绑、缓存上限和按尺寸解码修复,而不是滥用弱引用或 System.gc()。”
下一步进入 Java 8 Lambda/Stream 时,可以继续追问闭包捕获:lambda 让回调更短,却不会改变它捕获对象后的生命周期;Stream 的中间对象也会增加短时分配。把这两点放回 ART 的分配压力与所有权模型,才不会把语法糖误认为内存安全保证。
网硕互联帮助中心







评论前必须登录!
注册