云计算百科
云计算领域专业知识百科平台

JVM、内存与 Android OOM:从 GC Root 追到一次页面泄漏

线上图片详情页连续旋转几次后,旧 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 留下来,再放它走

  • 在调试示例中让一个静态请求管理器保存 Listener,使 Listener 以匿名内部类引用当前 Activity;页面打开图片请求后立刻旋转。
  • 用 Android Studio Memory Profiler 记录分配并抓取 heap dump,搜索旧 Activity 实例,查看它到一个 GC Root 的 shortest path;记录是静态字段、线程、队列还是缓存持有。
  • 改为请求对象只保存可取消的订阅,并在 onStop() 或对应的 ViewModel 清理边界调用 stop();不要靠弱引用蒙混过关。
  • 重复相同旋转路径,确认旧实例不再出现在意外保留链中,同时确认新页面仍能显示有效结果。再把图片改为接近显示尺寸的采样版本,比较分配次数和峰值。
  • 决策清单

    • 这条引用的拥有者比 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 的分配压力与所有权模型,才不会把语法糖误认为内存安全保证。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » JVM、内存与 Android OOM:从 GC Root 追到一次页面泄漏
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!