内存管理与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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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