编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • Android提升进阶

    • 库的解读

    • 专栏博客

      • 系统启动Zygote
      • Binder通信原理
      • Handler消息机制
      • Activity启动原理
      • 四大组件原理分析
      • AMS与组件管理
      • View绑制与渲染
      • 事件分发机制
      • Surface渲染原理
      • 自定义View设计
      • WMS窗口管理
      • PMS与APK安装
      • 虚拟机与类加载
      • 内存管理与GC
        • 一、引言:Android为什么频繁OOM
        • 二、Android进程的内存模型
          • 2.1 进程的虚拟内存空间
          • 2.2 Android的内存组成
        • 三、Java堆内存与ART虚拟机
          • 3.1 ART堆的结构
          • 3.2 堆大小限制
        • 四、ART的垃圾回收器
          • 4.1 ART GC的演进
          • 4.2 GC触发条件
        • 五、GC算法原理详解
          • 5.1 可达性分析
          • 5.2 标记-清除算法
          • 5.3 标记-压缩算法
        • 六、Concurrent Copying GC
          • 6.1 CC GC的工作原理
          • 6.2 Read Barrier技术
          • 6.3 CC GC的并发过程
          • 6.4 分代收集(Android 12+)
        • 七、对象的内存分配策略
          • 7.1 TLAB(Thread Local Allocation Buffer)
          • 7.2 大对象分配
        • 八、Native内存管理
          • 8.1 Native内存分配器
          • 8.2 JNI内存管理的陷阱
        • 九、Bitmap的内存管理演进
          • 9.1 Bitmap内存位置的变迁
          • 9.2 Bitmap内存计算
          • 9.3 NativeAllocationRegistry机制
        • 十、内存泄漏的原理与检测
          • 10.1 常见内存泄漏场景
          • 10.2 LeakCanary的检测原理
          • 10.3 引用链分析的底层原理
          • 10.4 Android Profiler内存分析深入
        • 十一、LowMemoryKiller与进程回收
          • 11.1 LMK的工作原理
          • 11.2 LMKD(Android 9+)
        • 十二、内存优化实战策略
          • 12.1 图片优化
          • 12.2 内存抖动优化
        • 十三、面试高频问题与深度分析
          • 13.1 Java对象的四种引用类型
          • 13.2 finalize()的问题
          • 13.3 如何分析OOM问题?
        • 十四、Android内存监控工具详解
          • 14.1 adb meminfo解读
          • 14.2 使用Android Profiler分析内存
        • 十五、总结
      • 线程与并发编程
      • 性能优化与监控
      • 序列化与数据存储
      • 组件化与路由设计
      • 插件化与热修复
      • NDK开发实践
      • WebView核心设计
      • ADB常见使用操作
    • 智能硬件

  • iOS开发和进阶

  • Web开发和进阶

  • Linux应用开发

  • Apps
  • Android提升进阶
  • 专栏博客
杨充
2026-04-14
目录

内存管理与GC

# 14.内存管理与GC

# 目录介绍

  • 一、引言:Android为什么频繁OOM
  • 二、Android进程的内存模型
    • 2.1 进程的虚拟内存空间
    • 2.2 Android的内存组成
  • 三、Java堆内存与ART虚拟机
    • 3.1 ART堆的结构
    • 3.2 堆大小限制
  • 四、ART的垃圾回收器
    • 4.1 ART GC的演进
    • 4.2 GC触发条件
  • 五、GC算法原理详解
    • 5.1 可达性分析
    • 5.2 标记-清除算法
    • 5.3 标记-压缩算法
  • 六、Concurrent Copying GC
    • 6.1 CC GC的工作原理
    • 6.2 Read Barrier技术
    • 6.3 CC GC的并发过程
    • 6.4 分代收集(Android 12+)
  • 七、对象的内存分配策略
    • 7.1 TLAB(Thread Local Allocation Buffer)
    • 7.2 大对象分配
  • 八、Native内存管理
    • 8.1 Native内存分配器
    • 8.2 JNI内存管理的陷阱
  • 九、Bitmap的内存管理演进
    • 9.1 Bitmap内存位置的变迁
    • 9.2 Bitmap内存计算
    • 9.3 NativeAllocationRegistry机制
  • 十、内存泄漏的原理与检测
    • 10.1 常见内存泄漏场景
    • 10.2 LeakCanary的检测原理
    • 10.3 引用链分析的底层原理
    • 10.4 Android Profiler内存分析深入
  • 十一、LowMemoryKiller与进程回收
    • 11.1 LMK的工作原理
    • 11.2 LMKD(Android 9+)
  • 十二、内存优化实战策略
    • 12.1 图片优化
    • 12.2 内存抖动优化
  • 十三、面试高频问题与深度分析
    • 13.1 Java对象的四种引用类型
    • 13.2 finalize()的问题
    • 13.3 如何分析OOM问题?
  • 十四、Android内存监控工具详解
    • 14.1 adb meminfo解读
    • 14.2 使用Android Profiler分析内存
  • 十五、总结

# 一、引言:Android为什么频繁OOM

相比桌面应用可以使用GB级内存,Android应用的堆内存通常限制在256MB~512MB。在这个有限的空间中运行复杂的应用,内存管理的重要性不言而喻。

疑惑:Java有垃圾回收器,为什么Android应用还会OOM?内存泄漏到底是怎么发生的?


# 二、Android进程的内存模型

# 2.1 进程的虚拟内存空间

Android进程的虚拟内存布局(32位):

0x00000000 ─────────────────────── 低地址
│  保留区域                        │
├─────────────────────────────────┤
│  代码段(.text)                 │ ← 可执行代码
├─────────────────────────────────┤
│  数据段(.data/.bss)            │ ← 全局变量
├─────────────────────────────────┤
│  堆(Heap)                     │ ← malloc/new分配
│  ↓ 向高地址增长                  │
│  ...                            │
│  Java Heap                      │ ← ART管理的堆
│  Native Heap                    │ ← jemalloc/scudo管理
│  ...                            │
├─────────────────────────────────┤
│  ↑ 向低地址增长                  │
│  栈(Stack)                    │ ← 线程栈
├─────────────────────────────────┤
│  内核空间(3GB~4GB)             │ ← 用户不可访问
0xFFFFFFFF ─────────────────────── 高地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 2.2 Android的内存组成

一个Android应用的内存组成:

┌────────────────────────────────────┐
│  PSS (Proportional Set Size)       │
│  = 私有内存 + 共享内存/共享进程数   │
├────────────────────────────────────┤
│  Java Heap    │ Dalvik/ART管理的对象堆│
│  (~256MB限制)  │ Activity/View/Bitmap等│
├────────────────────────────────────┤
│  Native Heap  │ C/C++层分配的内存    │
│  (无明确限制)  │ so库、JNI分配等     │
├────────────────────────────────────┤
│  Code         │ .dex/.oat/.so代码   │
├────────────────────────────────────┤
│  Stack        │ 线程栈(默认每个1MB) │
├────────────────────────────────────┤
│  Graphics     │ GPU纹理、Surface等   │
├────────────────────────────────────┤
│  Other        │ 文件映射、ashmem等   │
└────────────────────────────────────┘

查看命令:
adb shell dumpsys meminfo <package_name>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 三、Java堆内存与ART虚拟机

# 3.1 ART堆的结构

ART堆的内存空间划分:

┌──────────────────────────────────────────┐
│  Image Space (Boot Image)                │
│  → 预加载的系统类和资源(只读,与Zygote共享)│
├──────────────────────────────────────────┤
│  Zygote Space                            │
│  → Zygote预加载后fork前的对象             │
│  → 与所有应用进程共享(COW)               │
├──────────────────────────────────────────┤
│  Allocation Space (Main Space)           │
│  → 应用分配的对象                        │
│  → GC主要管理的区域                      │
│  ┌──────────┐ ┌──────────┐              │
│  │ Region0  │ │ Region1  │ ...          │
│  │ (256KB)  │ │ (256KB)  │              │
│  └──────────┘ └──────────┘              │
├──────────────────────────────────────────┤
│  Large Object Space                      │
│  → 大对象(>12KB的原始数组)              │
│  → 单独管理,避免内存碎片                 │
├──────────────────────────────────────────┤
│  Non-Moving Space                        │
│  → 不可移动的对象(如JNI引用的对象)       │
└──────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# 3.2 堆大小限制

Android的堆大小限制由系统属性决定:

dalvik.vm.heapsize     → 每个应用的最大堆大小(如512m)
dalvik.vm.heapstartsize → 初始堆大小(如8m)
dalvik.vm.heapgrowthlimit → 普通应用的堆上限(如256m)
dalvik.vm.heaptargetutilization → 目标堆利用率(如0.75)

// 应用可以请求更大的堆:
// android:largeHeap="true" → 使用heapsize而非heapgrowthlimit
// 但不推荐,因为GC耗时会更长

查看当前设备的限制:
adb shell getprop dalvik.vm.heapgrowthlimit  // 256m
adb shell getprop dalvik.vm.heapsize          // 512m
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 四、ART的垃圾回收器

# 4.1 ART GC的演进

Android版本    GC实现              特点
─────────────────────────────────────────
Android 4.x   Dalvik CMS          Stop-the-world时间长
Android 5-6   ART CMS             减少STW时间
Android 7     ART CMS改进          减少堆碎片
Android 8+    Concurrent Copying  几乎无STW,Region-based
Android 10+   CC改进              更好的压缩,减少碎片
Android 12+   CC + 分代           分代收集,减少扫描范围
1
2
3
4
5
6
7
8

# 4.2 GC触发条件

GC触发的时机:
1. Alloc GC:分配对象时堆空间不足
   → 最常见,会阻塞分配线程
   
2. Concurrent GC:堆利用率超过阈值
   → 后台进行,不阻塞应用线程
   → 阈值由heaptargetutilization控制
   
3. Explicit GC:System.gc()调用
   → 不推荐主动调用
   → ART默认会忽略(除非设置了force)
   
4. NativeAlloc GC:Native内存增长触发
   → Android 8+,Bitmap等Native分配也会触发GC
   
5. Background GC:应用进入后台
   → 更激进的GC,压缩堆减少内存占用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 五、GC算法原理详解

# 5.1 可达性分析

GC判断对象存活的核心算法——可达性分析:

GC Roots(根对象集合):
├── 虚拟机栈中引用的对象(局部变量)
├── 方法区中静态属性引用的对象
├── JNI引用的对象
├── synchronized锁持有的对象
├── Thread对象
└── 已注册的JNI全局引用

可达性判断:
GC Root → A → B → C(可达,存活)
GC Root → D → E(可达,存活)
F → G → H(不可达,回收)

示意图:
GC Roots
  │
  ├──→ A ──→ B ──→ C      (可达链)
  │
  └──→ D ──→ E            (可达链)

       F ──→ G ──→ H      (不可达,将被回收)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 5.2 标记-清除算法

Mark-Sweep算法:

阶段1:标记(Mark)
从GC Roots出发,遍历所有可达对象,标记为存活

阶段2:清除(Sweep)
遍历堆中所有对象,回收未标记的对象

内存状态:
标记前:[A][B][C][D][E][F][G][H]  (A/C/E可达)
标记后:[A*][B][C*][D][E*][F][G][H]  (*表示标记)
清除后:[A][ ][C][ ][E][ ][ ][ ]  (B/D/F/G/H被回收)

问题:内存碎片
[A][空][C][空][E][空][空][空]
→ 空闲空间不连续,可能无法分配大对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 5.3 标记-压缩算法

Mark-Compact算法(ART的CC GC使用的变体):

阶段1:标记
阶段2:压缩(移动存活对象到堆的一端)

内存状态:
标记后:[A*][ ][C*][ ][E*][ ][ ][ ]
压缩后:[A][C][E][ ][ ][ ][ ][ ]
         ↑连续空间          ↑大块空闲空间

优势:
- 消除碎片
- 分配只需移动指针(bump pointer,极快)

代价:
- 需要更新所有引用(A/C/E的地址变了)
- Read Barrier或Write Barrier支持
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 六、Concurrent Copying GC

# 6.1 CC GC的工作原理

Concurrent Copying GC(Android 8+默认):

核心思想:将堆分为多个Region,GC时将存活对象从源Region拷贝到目标Region

Region布局:
┌────────┐┌────────┐┌────────┐┌────────┐┌────────┐
│Region 0││Region 1││Region 2││Region 3││Region 4│
│[A][B]  ││[C][D]  ││[E][F]  ││  空闲  ││  空闲  │
└────────┘└────────┘└────────┘└────────┘└────────┘
  ↓ GC后
┌────────┐┌────────┐┌────────┐┌────────┐┌────────┐
│  空闲  ││  空闲  ││  空闲  ││[A][C]  ││[E]空闲 │
└────────┘└────────┘└────────┘└────────┘└────────┘
// 存活对象(A/C/E)被拷贝到新Region,旧Region整体释放
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 6.2 Read Barrier技术

CC GC的核心技术:Read Barrier(读屏障)

问题:GC在拷贝对象的同时,应用线程可能正在读取这个对象
→ 需要保证应用线程总是读到最新的对象副本

Read Barrier的工作:
每次读取引用时,检查对象是否已被移动

// 伪代码
Object readReference(Object* ref) {
    Object obj = *ref;
    if (obj.forwarding_address != null) {
        // 对象已被GC移动到新位置
        return obj.forwarding_address;  // 返回新地址
    }
    return obj;  // 对象未移动,返回原地址
}

在ART中的实现(ARM64汇编级别):
每次读引用 → 额外1-2条指令检查forwarding pointer
→ 性能开销约1-3%(比STW好得多)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 6.3 CC GC的并发过程

CC GC的三个阶段:

阶段1:初始标记(Pause 1)
  └── 暂停时间:< 1ms
  └── 标记GC Roots直接引用的对象

阶段2:并发标记 + 并发拷贝(Concurrent)
  └── 与应用线程同时运行
  └── 遍历对象图,标记所有可达对象
  └── 将存活对象拷贝到新Region
  └── 通过Read Barrier保证一致性

阶段3:清理(Pause 2 + Concurrent)
  └── 暂停时间:< 1ms
  └── 更新根引用
  └── 释放旧Region
  └── 回收空闲Region给分配器

总暂停时间:通常 < 2ms(相比之前的CMS几十毫秒大幅改善)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 6.4 分代收集(Android 12+)

ART分代GC的设计:

分代假说:大多数对象在创建后很快变成垃圾(朝生夕死)
→ 年轻代GC更频繁但更快(只扫描年轻对象)
→ 老年代GC较少但扫描范围大

ART的分代实现:
┌────────────────────────────────────────┐
│ Young Generation(年轻代)               │
│ ├── 新分配的对象先放在这里               │
│ ├── Minor GC只扫描年轻代                │
│ ├── 存活对象经过多次GC后提升到老年代      │
│ └── 典型大小:堆的1/4到1/3              │
├────────────────────────────────────────┤
│ Old Generation(老年代)                 │
│ ├── 长期存活的对象(如Activity、全局单例) │
│ ├── Major GC扫描整个堆                  │
│ ├── Major GC频率较低                    │
│ └── 典型大小:堆的2/3到3/4              │
└────────────────────────────────────────┘

分代GC的效率提升:
  应用创建大量临时对象(如String拼接、循环变量)
  → 这些对象在Minor GC时就被回收
  → 不需要扫描整个堆
  → GC耗时大幅降低

Write Barrier(写屏障)在分代GC中的作用:
  问题:如果老年代对象引用了年轻代对象
        Minor GC只扫描年轻代
        会误认为该年轻代对象不可达而回收!
  
  解决:Write Barrier
  → 每次修改引用时记录跨代引用
  → 使用Card Table(卡表)标记包含跨代引用的内存区域
  → Minor GC时除了GC Roots还要扫描Card Table中标记的区域
  → 保证不会错误回收被老年代引用的年轻代对象

  Card Table原理:
  ┌──────┬──────┬──────┬──────┐
  │Card 0│Card 1│Card 2│Card 3│ ← 每个Card对应512字节堆内存
  │ clean│ dirty│ clean│ dirty│ ← dirty表示有跨代引用
  └──────┴──────┴──────┴──────┘
  Minor GC时只需要扫描dirty Card中的对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

# 七、对象的内存分配策略

# 7.1 TLAB(Thread Local Allocation Buffer)

ART使用TLAB加速对象分配:

每个线程有自己的TLAB(从堆Region中分配的小块内存):

Thread 1:  [TLAB: ████████░░░░] ← 分配指针
Thread 2:  [TLAB: ██████░░░░░░] ← 分配指针
Thread 3:  [TLAB: ██░░░░░░░░░░] ← 分配指针

分配对象时:
1. 从当前线程的TLAB中分配(无需加锁,极快)
   → 只需移动指针:pointer += objectSize
2. TLAB用完 → 从堆Region申请新的TLAB
3. Region用完 → 向系统申请新内存或触发GC

TLAB的优势:
- 无锁分配:每个线程独享自己的TLAB
- 极快:只是指针移动操作
- 减少碎片:TLAB内部是连续分配
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 7.2 大对象分配

大对象(Large Object)的特殊处理:

小对象(< 12KB):
→ 分配在普通Region中(通过TLAB)

大对象(>= 12KB,原始数组如byte[]、int[]等):
→ 分配在Large Object Space
→ 使用FreeList分配器
→ 不会被CC GC移动(避免拷贝开销过大)

特大对象(如大Bitmap的像素数据):
→ Android 8+:分配在Native堆中
→ 不占用Java堆配额
→ 通过NativeAllocationRegistry触发GC
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 八、Native内存管理

# 8.1 Native内存分配器

Android的Native内存分配器:

Android 7-10:jemalloc
→ 高性能,低碎片
→ 适合服务器场景

Android 11+:Scudo
→ 安全优先的内存分配器
→ 内置缓冲区溢出检测
→ UAF(Use-After-Free)检测
→ 性能略低于jemalloc,但安全性大幅提升

Scudo的安全特性:
┌──────────────┬──────────────────────────────┐
│ 安全检查      │ 说明                          │
├──────────────┼──────────────────────────────┤
│ Chunk Header │ 每个分配块有校验头,检测越界写   │
│ Quarantine   │ 释放的内存先进入隔离区再复用    │
│ RSS Limit    │ 限制进程RSS,防止内存爆炸       │
│ Canary       │ 内存块边界标记,检测缓冲区溢出   │
└──────────────┴──────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 8.2 JNI内存管理的陷阱

JNI层的内存需要手动管理:

// 常见的内存泄漏
JNIEXPORT void JNICALL Java_foo(JNIEnv* env, jobject obj) {
    // 获取Java字符串
    const char* str = env->GetStringUTFChars(jstr, NULL);
    // 使用str...
    
    // 必须释放!否则内存泄漏
    env->ReleaseStringUTFChars(jstr, str);
    
    // 创建全局引用
    jobject globalRef = env->NewGlobalRef(localObj);
    // 必须在不需要时调用DeleteGlobalRef
    // 否则Java对象无法被GC回收
}

// JNI引用类型:
// Local Reference  → 方法结束自动释放(但循环中也需注意上限)
// Global Reference → 手动管理,必须显式释放
// Weak Global Ref  → 不阻止GC回收
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 九、Bitmap的内存管理演进

# 9.1 Bitmap内存位置的变迁

Android版本与Bitmap像素数据存储位置:

Android 2.x (Dalvik):
像素数据 → Native堆
→ 手动recycle()释放
→ 不计入Java堆限制,但占用进程总内存

Android 3.0-7.x (ART):
像素数据 → Java堆
→ GC自动回收
→ 计入Java堆限制(容易OOM)

Android 8.0+ (ART):
像素数据 → Native堆(回归!)
→ NativeAllocationRegistry追踪
→ 不计入Java堆限制
→ 通过Cleaner + GC联动回收

为什么Android 8又改回Native?
→ Java堆有大小限制(256MB),Bitmap是大头消费者
→ 放在Native堆可以突破Java堆限制
→ NativeAllocationRegistry保证了Native内存也能触发GC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 9.2 Bitmap内存计算

Bitmap内存大小计算:

内存 = 宽 × 高 × 每像素字节数

每像素字节数:
├── ALPHA_8    → 1字节
├── RGB_565    → 2字节(无透明通道,省内存)
├── ARGB_4444  → 2字节(已弃用,质量差)
├── ARGB_8888  → 4字节(默认,质量最好)
└── RGBA_F16   → 8字节(HDR内容)

实际占用还受密度缩放影响:
实际宽 = 原始宽 × (targetDensity / sourceDensity)
实际高 = 原始高 × (targetDensity / sourceDensity)

示例:一张1920×1080的图片在xxhdpi设备上
→ 如果图片在drawable-mdpi中:
  实际宽 = 1920 × (480/160) = 5760
  实际高 = 1080 × (480/160) = 3240
  内存 = 5760 × 3240 × 4 = 74,649,600字节 ≈ 71MB!
→ 如果在drawable-xxhdpi中:
  实际宽 = 1920(不缩放)
  内存 = 1920 × 1080 × 4 = 8,294,400字节 ≈ 8MB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 9.3 NativeAllocationRegistry机制

NativeAllocationRegistry(Android 8.0+):

问题:Bitmap像素数据在Native堆,Java GC看不到
→ Java堆可能只占50MB,但Native堆已经500MB
→ GC不知道需要回收,导致OOM

NativeAllocationRegistry解决方案:
  让Java GC感知Native内存的存在

工作原理:
1. Bitmap创建时注册Native内存
   NativeAllocationRegistry registry = new NativeAllocationRegistry(
       classLoader, nativeFinalizer, nativeSize);
   registry.registerNativeAllocation(bitmapObj, nativePtr);
   
   内部实现:
   → 将nativeSize累加到ART的nativeBytes计数器
   → 关联Cleaner到bitmapObj
   → 当nativeBytes超过阈值 → 触发GC

2. GC回收Bitmap的Java对象时
   → Cleaner被触发
   → 调用nativeFinalizer释放Native内存
   → 从nativeBytes中减去nativeSize

3. 触发GC的阈值
   → 初始值:300KB
   → 随着Native分配增长动态调整
   → 当Native分配 > 上次GC后的Native分配 × growthFactor时触发
   
   这样即使Java堆空间充裕
   Native内存增长也能触发GC回收不再使用的Bitmap

Cleaner vs Finalizer:
  Cleaner(推荐):
  ├── PhantomReference + ReferenceQueue实现
  ├── 专用的Cleaner线程处理
  ├── 不会延长对象生命周期
  └── 执行更及时
  
  Finalizer(不推荐):
  ├── Object.finalize()方法
  ├── FinalizerDaemon线程处理
  ├── 对象至少多存活一个GC周期
  └── 可能堆积导致OOM
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

# 十、内存泄漏的原理与检测

# 10.1 常见内存泄漏场景

1. 静态引用持有Activity/Context
   static Activity sActivity;  // 永远不会被GC

2. 非静态内部类持有外部类引用
   new Handler() { ... }  // 隐式持有Activity.this
   匿名内部类Runnable    // 隐式持有外部对象

3. 注册未反注册
   registerReceiver()不配对unregisterReceiver()
   EventBus.register()不配对unregister()
   addOnGlobalLayoutListener()不移除

4. 资源未关闭
   Cursor、InputStream、TypedArray未close
   
5. WebView内存泄漏
   WebView持有Activity引用
   → 解决:在独立进程中使用WebView

6. 集合类引用
   static HashMap持有大量对象引用
   → 对象加入集合后忘记移除
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 10.2 LeakCanary的检测原理

LeakCanary的检测机制:

1. Activity销毁时注册弱引用
   ActivityLifecycleCallbacks.onActivityDestroyed(activity) {
       WeakReference<Activity> ref = new WeakReference<>(activity, queue);
       watchedReferences.put(key, ref);
   }

2. 等待5秒后检查
   mainHandler.postDelayed(() -> {
       // 触发GC
       Runtime.getRuntime().gc();
       Thread.sleep(100);
       System.runFinalization();
       
       // 检查弱引用是否被清除
       if (ref.get() != null) {
           // Activity还活着 → 可能泄漏!
           // 3. dump heap并分析
           dumpHeap();
           analyzeHeap();
       }
   }, 5000);

3. 分析heap dump
   → 使用Shark库解析hprof文件
   → 查找从GC Root到泄漏对象的引用链
   → 展示泄漏路径
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# 10.3 引用链分析的底层原理

GC Root到泄漏对象的引用链查找算法:

Shark库的分析过程:
1. 解析hprof文件
   → 读取所有对象、类、实例的信息
   → 构建对象引用图(Graph)
   
2. 找到泄漏对象
   → 通过key匹配watchedReferences中的对象
   → 确认该对象在GC后仍然存活
   
3. 从泄漏对象反向BFS查找GC Root
   泄漏对象 ← 引用者A ← 引用者B ← ... ← GC Root
   
   BFS(广度优先搜索)过程:
   Queue: [泄漏Activity]
   → 查找所有引用泄漏Activity的对象
   → [Handler$1, Runnable$2, ...]
   → 查找所有引用这些对象的对象
   → [..., Thread-15, static field]
   → 直到找到GC Root

4. 输出引用链
   ┌── GC Root: 静态变量
   │   └── AppManager.sInstance
   │       └── mActivities (ArrayList)
   │           └── element[3]
   │               └── ★ MainActivity (泄漏对象)
   └── Leaking: YES (Activity已destroy但未被GC)
   
   LeakCanary还会标注每个节点是否"正在泄漏":
   ├── Leaking: NO  → 此对象应该存在
   ├── Leaking: YES → 此对象不应该存在(泄漏点)
   └── Leaking: UNKNOWN → 不确定
   
   泄漏源头就是从NO到YES的转变点

5. 内存泄漏的修复验证
   → 修复代码后重新运行
   → 确认LeakCanary不再报告该泄漏
   → 使用heap dump对比前后内存增量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

# 10.4 Android Profiler内存分析深入

Android Studio Profiler的内存分析功能:

1. Allocation Tracking(分配追踪)
   记录每个对象的分配信息:
   ├── 对象类型和大小
   ├── 分配时的堆栈调用链
   ├── 分配的线程
   └── 分配的时间戳
   
   用途:定位内存抖动
   → 短时间内大量同类对象被创建和销毁
   → 通过堆栈找到创建代码
   → 改为对象复用

2. Heap Dump分析
   Dominator Tree(支配树):
   → 如果回收对象A就能回收对象B
   → 则A是B的支配者(Dominator)
   → Dominator Tree展示"谁支配了最多内存"
   
   Retained Size vs Shallow Size:
   ├── Shallow Size:对象自身占用的内存
   │   例如:ArrayList对象自身 ~64字节
   └── Retained Size:回收该对象后能释放的总内存
       例如:ArrayList + 内部数组 + 所有元素 = 数MB
   
   → Retained Size大的对象是优化重点

3. Native内存追踪(Android 10+)
   → 跟踪malloc/free调用
   → 定位Native内存泄漏
   → 需要使用Debug版本的App
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# 十一、LowMemoryKiller与进程回收

# 11.1 LMK的工作原理

Low Memory Killer (LMK):

Android的LMK是Linux OOM Killer的定制版本:

进程优先级(oom_adj值):
┌──────────────────┬──────────┬──────────────────┐
│ 进程类型          │ oom_adj  │ 说明              │
├──────────────────┼──────────┼──────────────────┤
│ 前台Activity      │ 0        │ 最不可能被杀      │
│ 可见进程          │ 100      │ 有可见组件         │
│ 前台Service       │ 200      │ 运行前台Service    │
│ 后台(近期使用)   │ 700      │ 用户最近使用       │
│ 后台(缓存)      │ 900      │ 进程缓存           │
│ 空进程            │ 1000     │ 最可能被杀         │
└──────────────────┴──────────┴──────────────────┘

LMK的minfree配置(示例):
oom_adj:  0,   100,  200,  300,  900,  1000
minfree:  73MB, 92MB, 110MB, 128MB, 183MB, 220MB
// 当可用内存低于minfree值时,杀死对应oom_adj及以上的进程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 11.2 LMKD(Android 9+)

Android 9+用lmkd守护进程替代内核LMK模块:

优势:
1. 在用户空间运行,更灵活
2. 支持PSI(Pressure Stall Information)监控
3. 可以感知内存压力趋势,提前行动
4. 支持更复杂的回收策略

lmkd的工作:
1. 监控/proc/pressure/memory(PSI信息)
2. 当some/full压力超过阈值时
3. 按oom_adj优先级选择进程
4. 发送SIGKILL杀死进程
1
2
3
4
5
6
7
8
9
10
11
12
13

# 十二、内存优化实战策略

# 12.1 图片优化

Bitmap优化策略:

1. 尺寸匹配:加载时按控件大小采样
   BitmapFactory.Options options = new BitmapFactory.Options();
   options.inJustDecodeBounds = true;  // 只读取尺寸
   BitmapFactory.decodeResource(res, R.drawable.large, options);
   options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
   options.inJustDecodeBounds = false;
   Bitmap bitmap = BitmapFactory.decodeResource(res, R.drawable.large, options);

2. 格式选择:
   无透明通道 → RGB_565(节省50%内存)
   options.inPreferredConfig = Bitmap.Config.RGB_565;

3. 内存复用:inBitmap
   options.inMutable = true;
   options.inBitmap = reusableBitmap;  // 复用已分配的Bitmap内存

4. 使用Glide/Coil等图片库
   → 自动管理缓存、采样、复用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 12.2 内存抖动优化

内存抖动:短时间内大量创建和销毁对象,导致频繁GC

典型场景:
// 差:每次onDraw都创建新对象
protected void onDraw(Canvas canvas) {
    Paint paint = new Paint();  // 每帧创建!
    Rect rect = new Rect();     // 每帧创建!
    paint.setColor(Color.RED);
    canvas.drawRect(rect, paint);
}

// 好:复用对象
private Paint mPaint = new Paint();
private Rect mRect = new Rect();

protected void onDraw(Canvas canvas) {
    mPaint.setColor(Color.RED);
    mRect.set(0, 0, getWidth(), getHeight());
    canvas.drawRect(mRect, mPaint);
}

其他优化:
- 使用SparseArray替代HashMap(避免自动装箱)
- 使用StringBuilder替代String拼接
- 对象池模式复用频繁创建的对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# 十三、面试高频问题与深度分析

# 13.1 Java对象的四种引用类型

1. 强引用(Strong Reference)
   Object obj = new Object();
   → GC永不回收(除非不可达)
   → 默认的引用方式

2. 软引用(SoftReference)
   SoftReference<Bitmap> ref = new SoftReference<>(bitmap);
   → 内存不足时才回收
   → 适合做内存敏感的缓存

3. 弱引用(WeakReference)
   WeakReference<Activity> ref = new WeakReference<>(activity);
   → 下次GC时必定回收(不论内存是否充足)
   → 适合解决内存泄漏

4. 虚引用(PhantomReference)
   PhantomReference<Object> ref = new PhantomReference<>(obj, queue);
   → 随时可能被回收
   → 只用于跟踪对象被回收的时机(配合ReferenceQueue)
   → ART内部用于NativeAllocationRegistry
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 13.2 finalize()的问题

为什么不应该依赖finalize():

1. 执行时机不确定(GC后才调用,可能很晚)
2. 可能不会执行(应用退出时不保证调用)
3. 延长对象生命周期(finalize队列中的对象至少多活一个GC周期)
4. 性能差(需要额外的FinalizerDaemon线程处理)
5. 可能导致OOM(finalize执行慢 → 对象堆积 → 内存不足)

替代方案:
→ try-with-resources(Closeable资源)
→ Cleaner(Java 9+)
→ NativeAllocationRegistry(Android 8+)
1
2
3
4
5
6
7
8
9
10
11
12

# 13.3 如何分析OOM问题?

OOM分析步骤:

1. 获取堆转储
   Debug.dumpHprofData("/sdcard/oom.hprof");
   或 adb shell am dumpheap <pid> /data/local/tmp/oom.hprof

2. 使用MAT/Android Studio Profiler分析
   → Dominator Tree:查找占用内存最大的对象
   → Histogram:查看各类型对象数量
   → GC Root查找:分析泄漏引用链

3. 常见OOM类型:
   java.lang.OutOfMemoryError: Failed to allocate a NNN byte allocation
   → Java堆内存不足(检查Bitmap/大数组/内存泄漏)
   
   java.lang.OutOfMemoryError: Failed to allocate NNN bytes with NNN free
   → 碎片化严重(大块连续空间不够)
   
   java.lang.OutOfMemoryError: pthread_create failed
   → 线程数过多(检查线程泄漏,超过/proc/sys/kernel/threads-max限制)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 十四、Android内存监控工具详解

# 14.1 adb meminfo解读

# 获取应用内存信息
adb shell dumpsys meminfo com.example.app

# 输出解读:
#                   Pss      Private   Private   SwapPss   Heap    Heap    Heap
#                   Total    Dirty     Clean     Dirty     Size    Alloc   Free
# ------           ------   ------    ------    ------    ------  ------  ------
# Java Heap:        25600    25400      200        0       65536   52000   13536
# Native Heap:      18000    17800      200        0       32768   28000    4768
# Code:             12000      100    11900        0
# Stack:             1200     1200        0        0
# Graphics:          8000     8000        0        0
# Other:             3000     2800      200        0
# Total:            67800    55300    12500        0

# 关键指标:
# Java Heap Alloc:Java层已分配内存(不能超过heapgrowthlimit)
# Native Heap Alloc:Native层已分配内存(JNI、Bitmap像素等)
# Graphics:GPU相关内存(纹理、Surface Buffer等)
# Total PSS:进程实际物理内存占用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 14.2 使用Android Profiler分析内存

Memory Profiler的关键功能:

1. 实时内存曲线
   → Java/Native/Graphics/Stack/Code各段的变化趋势
   → 快速定位内存增长的时间点

2. Allocation Tracking
   → 记录每个对象的分配堆栈
   → 按类型统计分配数量
   → 定位内存抖动的具体代码

3. Heap Dump
   → 快照当前所有存活对象
   → Dominator Tree查找大对象
   → 引用链追踪定位泄漏

最佳实践:
1. 打开应用 → 操作 → 返回 → 操作 → 返回 → 手动GC
2. 观察Java Heap是否持续增长
3. 如果GC后仍增长 → 有内存泄漏
4. 抓Heap Dump → 对比两次快照找增量对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 十五、总结

Android内存管理知识图谱:

内存模型
├── Java Heap → ART管理,有大小限制
├── Native Heap → malloc/jemalloc/scudo
├── Graphics → GPU缓冲区
└── Stack → 线程栈(默认1MB/线程)

GC机制
├── 可达性分析 → GC Roots遍历
├── CC GC(Android 8+)→ Region-based + Read Barrier
├── 分代收集(Android 12+)→ Young/Old分代
└── TLAB → 无锁快速分配

内存泄漏
├── 静态引用 → 避免持有Activity
├── 内部类 → 使用静态内部类+弱引用
├── 注册/反注册 → 配对使用
└── 检测工具 → LeakCanary/Profiler/MAT

优化策略
├── Bitmap优化 → 采样/格式/复用
├── 内存抖动 → 对象复用
├── Native优化 → JNI引用管理
└── 进程管理 → LMK/oom_adj优先级
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
上次更新: 2026/06/10, 11:13:41
虚拟机与类加载
线程与并发编程

← 虚拟机与类加载 线程与并发编程→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式