第一部分:垃圾回收基础
1. 垃圾回收基础
1.1 什么是垃圾回收
定义:
垃圾回收(Garbage Collection,GC)是自动管理内存的机制,能够自动识别和回收不再使用的对象,释放它们占用的内存空间。
作用:
- 自动释放内存:不需要开发者手动释放内存,避免忘记释放导致的内存泄漏
- 避免内存泄漏:自动回收不再使用的对象,防止内存逐渐被占用
- 简化内存管理:开发者只需要关注业务逻辑,不需要关心内存释放
- 提高开发效率:减少内存管理相关的bug
Android中的重要性:
- 内存有限:Android设备内存有限,必须合理使用
- 性能影响:GC性能直接影响应用流畅度
- 用户体验:频繁GC会导致卡顿,影响用户体验
- 系统稳定性:内存不足可能导致应用被系统杀死
示例:
java
public class GarbageCollectionExample {
public void createObjects() {
// 创建对象
Object obj1 = new Object();
Object obj2 = new Object();
// obj1和obj2不再被引用后,GC会自动回收它们占用的内存
// 开发者不需要手动释放
}
}
1.2 如何判断对象已死(哪些对象可以被回收)
核心问题: GC需要知道哪些对象是"垃圾"(不再使用),哪些对象还在使用。
1.2.1 引用计数算法(Android不使用,了解即可)
原理:
每个对象有一个引用计数器,当对象被引用时计数器+1,取消引用时计数器-1。当计数器为0时,对象可以被回收。
示例:
java
Object obj = new Object(); // 引用计数 = 1
Object ref = obj; // 引用计数 = 2
obj = null; // 引用计数 = 1
ref = null; // 引用计数 = 0,对象可以被回收
优点:
- 实现简单
- 回收及时,对象不再被引用时立即回收
缺点:
- 无法处理循环引用:两个对象相互引用,引用计数永远不为0,导致内存泄漏
循环引用示例:
java
class Node {
Node next;
}
Node node1 = new Node();
Node node2 = new Node();
node1.next = node2; // node1引用node2
node2.next = node1; // node2引用node1
// 即使node1和node2都不再被外部引用
// 它们的引用计数都是1(相互引用),无法被回收
// 这就是内存泄漏
为什么Android不用:
Android不使用引用计数算法,因为无法处理循环引用,会导致内存泄漏。Android使用可达性分析算法。
1.2.2 可达性分析算法(Android使用这个)
原理:
从一组称为"GC Roots"的对象开始,向下搜索,所有能够从GC Roots到达的对象都是"存活"的,无法到达的对象就是"垃圾",可以被回收。
算法流程:
markdown
1. 从GC Roots开始
2. 标记所有能到达的对象(存活对象)
3. 未标记的对象就是垃圾,可以被回收
GC Roots对象有哪些(Android场景):
- 虚拟机栈中引用的对象
- 局部变量、方法参数
- 示例:方法中的局部变量引用的对象
java
public void method() {
Object obj = new Object(); // obj在栈中,引用的对象是GC Root
// obj引用的对象不会被回收
}
- 方法区中静态属性引用的对象
- static变量
- 示例:类的静态变量
java
public class MyClass {
private static Object staticObj = new Object(); // staticObj是GC Root
// staticObj引用的对象不会被回收
}
- 方法区中常量引用的对象
- 常量
- 示例:字符串常量
java
public class MyClass {
private static final String CONSTANT = "Hello"; // CONSTANT是GC Root
}
-
本地方法栈中引用的对象
- Native方法中的引用
- 示例:JNI调用中的对象
-
同步锁持有的对象
- synchronized持有的对象
- 示例:锁对象
java
Object lock = new Object();
synchronized (lock) {
// lock是GC Root,不会被回收
}
- 内部引用
- Class对象、异常对象等
- 示例:类的Class对象
优点:
- 能处理循环引用,准确判断对象是否可回收
- 不会因为循环引用导致内存泄漏
缺点:
- 需要暂停应用(STW - Stop The World),但Android优化了这个问题,使用并发回收减少暂停时间
循环引用示例(可达性分析可以处理):
java
class Node {
Node next;
}
Node node1 = new Node();
Node node2 = new Node();
node1.next = node2;
node2.next = node1;
// 如果node1和node2都不再被GC Roots引用
// 即使它们相互引用,也无法从GC Roots到达
// 所以它们都是垃圾,可以被回收
1.2.3 引用类型(Android开发中常用)
1. 强引用(Strong Reference)
特点:
- 最常见的引用类型
- 只要强引用存在,对象就不会被GC回收
- 即使内存不足,也不会回收
Android使用场景:
- 普通对象引用
- Activity引用
- 大部分对象都是强引用
示例:
java
Object obj = new Object(); // 强引用
// obj引用的对象不会被回收,除非obj = null
2. 软引用(Soft Reference)
特点:
- 内存不足时才回收
- 适合缓存场景
- 可以配合引用队列使用
Android使用场景:
- Android不推荐使用软引用
- 因为Android设备内存有限,软引用可能很快被回收
- 建议使用LruCache等有大小限制的缓存
示例:
java
SoftReference<Bitmap> softRef = new SoftReference<>(bitmap);
Bitmap bitmap = softRef.get(); // 可能返回null(如果被回收了)
3. 弱引用(Weak Reference)
特点:
- 只要GC就会回收,不会阻止对象被回收
- 适合避免内存泄漏的场景
Android使用场景:
- 避免内存泄漏(Handler、静态变量持有对象时)
- 缓存场景(WeakHashMap)
示例:
java
// 避免Handler内存泄漏
private static class MyHandler extends Handler {
private WeakReference<Activity> activityRef;
MyHandler(Activity activity) {
activityRef = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
Activity activity = activityRef.get();
if (activity != null) {
// 使用activity
}
}
}
4. 虚引用(Phantom Reference)
特点:
- 最弱的引用类型
- 主要用于对象回收前的清理工作
- 无法通过虚引用获取对象
Android使用场景:
- 很少使用
- 主要用于特殊场景(如直接内存的清理)
1.3 Android中的对象回收
堆内存回收:
- 主要回收区域
- 回收对象实例(通过new创建的对象)
- 这是GC的主要工作
方法区回收:
- 回收类信息、常量等
- Android中较少,因为类信息通常不会频繁变化
回收时机:
- 系统自动判断,开发者不需要手动触发
- 不建议调用System.gc(),因为会强制触发Full GC,可能导致卡顿
第二部分:垃圾回收算法(Android ART使用的算法)
说明:这部分介绍GC算法的基本原理,Android ART主要使用标记-复制算法
2. 垃圾回收算法
2.1 标记-清除算法(Mark-Sweep)
算法原理:
分两个阶段:
- 标记阶段:从GC Roots开始,标记所有需要回收的对象
- 清除阶段:清除所有被标记的对象
算法流程:
markdown
1. 从GC Roots开始遍历
2. 标记所有可达对象(存活对象)
3. 清除所有未标记的对象(垃圾对象)
图示:
css
标记前:
[对象1][对象2][对象3][对象4][对象5]
✓ ✗ ✓ ✗ ✓
存活 垃圾 存活 垃圾 存活
清除后:
[对象1][空闲][对象3][空闲][对象5]
✓ ✓ ✓
优点:
- 实现简单
- 不需要移动对象
缺点:
- 产生内存碎片:清除后留下不连续的内存空间
- 效率较低:需要两次遍历(标记和清除)
Android中的使用:
- Android不主要使用这个算法
- 因为会产生内存碎片,影响后续内存分配效率
2.2 标记-复制算法(Mark-Copy)- Android主要使用这个
算法原理:
将内存分为两块,每次只使用一块。回收时,将存活对象复制到另一块,然后清空当前块。
算法流程:
css
1. 将内存分为From区和To区
2. 对象分配在From区
3. 回收时:
- 标记From区中的存活对象
- 将存活对象复制到To区
- 清空From区
4. From区和To区角色互换
图示:
ini
回收前:
From区: [对象1][对象2][对象3][对象4]
存活 垃圾 存活 垃圾
To区: [空闲][空闲][空闲][空闲]
回收后:
From区: [空闲][空闲][空闲][空闲]
To区: [对象1][对象3]
存活 存活
优点:
- 没有内存碎片:复制后内存连续
- 回收效率高:只需要复制存活对象
- 适合年轻代:大部分对象很快被回收,存活对象少
缺点:
- 浪费一半内存:需要两块内存,但只使用一块
- 对象存活率高时效率低:需要复制很多对象
Android中的使用:
- Android ART主要使用这个算法
- 适合年轻代回收(大部分对象很快被回收)
- 通过优化,只浪费10%的内存(而不是50%)
改进算法(Appel式回收):
将内存分为Eden区和两个Survivor区:
- Eden区:新对象分配在这里
- Survivor0和Survivor1:用于存放存活对象
默认比例:
- Eden : Survivor0 : Survivor1 = 8 : 1 : 1
- 只浪费10%的内存,而不是50%
工作流程:
markdown
1. 新对象分配在Eden区
2. Eden区满时,触发Minor GC
3. 将Eden区和Survivor0中的存活对象复制到Survivor1
4. 清空Eden区和Survivor0
5. Survivor0和Survivor1角色互换
2.3 标记-整理算法(Mark-Compact)
算法原理:
分三个阶段:
- 标记阶段:标记所有需要回收的对象
- 整理阶段:将存活对象向一端移动
- 清除阶段:清理边界外的内存
算法流程:
markdown
1. 标记所有存活对象
2. 将存活对象移动到一端(整理)
3. 清理边界外的内存
图示:
css
整理前:
[对象1][空闲][对象2][空闲][对象3]
✓ ✓ ✓
整理后:
[对象1][对象2][对象3][空闲][空闲]
✓ ✓ ✓
优点:
- 没有内存碎片:整理后内存连续
- 不浪费内存:不需要两块内存
缺点:
- 效率较低:需要移动对象,更新所有引用
- 适合老年代:对象存活率高,移动成本相对较低
Android中的使用:
- Android在某些场景下使用
- 主要用于老年代回收(避免内存碎片)
2.4 分代收集算法(Android使用这个策略)
理论基础:
根据对象生命周期不同,将内存分为不同的代:
- 大部分对象生命周期很短:创建后很快被回收
- 少数对象生命周期很长:长期存活
分代策略:
年轻代(Young Generation):
- 特点:存放新创建的对象,生命周期短
- 回收算法:标记-复制算法(快速高效)
- 回收频率:高(频繁回收)
- 回收类型:Partial GC(部分回收)
- 回收时间:短(几毫秒)
老年代(Old Generation):
- 特点:存放长期存活的对象,生命周期长
- 回收算法:标记-整理算法(避免碎片)
- 回收频率:低(偶尔回收)
- 回收类型:Full GC(完全回收)
- 回收时间:长(可能几十到几百毫秒)
Android中的分代收集:
-
年轻代用标记-复制算法
- 快速回收,适合频繁回收
- 大部分对象很快被回收,复制成本低
-
老年代用标记-整理算法
- 避免内存碎片
- 对象存活率高,移动成本相对较低
-
这样设计的好处:
- 提高GC效率
- 减少GC暂停时间
- 减少内存碎片
对象晋升过程:
markdown
1. 新对象 → 年轻代(Eden区)
2. 年轻代GC → 存活对象 → Survivor区
3. 多次GC后仍存活 → 晋升到老年代
4. 老年代GC → 回收长期存活的对象
第三部分:Android ART垃圾回收器
3. Android ART垃圾回收器(Garbage Collector)
核心说明:Android ART只有一种垃圾回收器,叫做"并发复制收集器"
3.1 ART与JVM的区别
ART(Android Runtime)不是JVM:
-
编译方式不同:
- ART:AOT编译(Ahead-Of-Time),安装时编译
- JVM:JIT编译(Just-In-Time),运行时编译
-
GC机制不同:
- ART:只有一种收集器(并发复制收集器)
- JVM:有多种收集器可选(Serial、Parallel、CMS、G1、ZGC等)
-
优化方向不同:
- ART:针对低延迟优化,适合交互式应用
- JVM:有多种优化方向(吞吐量、延迟等)
ART的GC专门为Android应用优化:
- 减少卡顿:低延迟设计
- 提升流畅度:并发回收,减少暂停时间
- 适合交互式应用:优化用户滑动、点击等操作
3.2 Android的垃圾回收器是什么
只有一个主要的垃圾回收器:并发复制收集器(Concurrent Copying,简称CC)
特点:
- Android只有这一种收集器
- 没有多种收集器可选(不像JVM有Serial、Parallel、CMS、G1、ZGC等)
- Android开发者不需要选择收集器,系统自动使用这个收集器
为什么只有一个:
- 针对Android场景优化:低延迟、交互式应用
- 不需要开发者选择:系统自动管理
- 简化开发:开发者不需要了解多种收集器的区别
3.3 这个垃圾回收器是怎么工作的(都是同一个收集器的特性,不是步骤,也不是不同的回收方式)
说明:这些都是"并发复制收集器"的特性,它们同时发挥作用,让回收更高效、更少卡顿
3.3.1 分代回收策略(回收策略)
内存分为两部分:
- 年轻代:存放新创建的对象(生命周期短)
- 老年代:存放长期存活的对象(生命周期长)
为什么分代?
因为大部分对象很快就会被回收,分开处理更高效:
- 年轻代:频繁回收,用快速算法(标记-复制)
- 老年代:偶尔回收,用避免碎片的算法(标记-整理)
示例:
java
public void createObjects() {
// 这些对象在年轻代
for (int i = 0; i < 1000; i++) {
Object obj = new Object(); // 大部分很快被回收
}
// 这个对象可能晋升到老年代
Object longLived = new Object();
// 如果多次GC后仍存活,会进入老年代
}
3.3.2 并发回收(执行方式)
在后台线程执行垃圾回收:
- 尽量不暂停主线程(应用继续运行)
- 这样可以减少卡顿
并发回收的优势:
ini
传统回收(串行):
主线程: [运行][暂停GC][运行][暂停GC][运行]
↓ ↓ ↓ ↓ ↓
卡顿 卡顿 卡顿 卡顿 卡顿
并发回收(ART):
主线程: [运行][运行][运行][运行][运行]
↓
不卡顿
GC线程: [GC][GC][GC]
示例:
java
// 用户滑动列表时
recyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
// 即使此时触发GC,也不会卡顿
// 因为GC在后台线程执行
}
});
3.3.3 复制算法(回收算法)
回收时:把存活的对象复制到新位置 然后:直接清空旧位置
优点:
- 简单高效
- 没有内存碎片
工作流程:
markdown
1. 标记存活对象
2. 复制到新位置
3. 清空旧位置
示例:
css
回收前(From区):
[对象1][对象2][对象3][对象4]
存活 垃圾 存活 垃圾
回收后(To区):
[对象1][对象3]
存活 存活
3.3.4 增量回收(优化特性)
把大量的回收工作分成小份 分批执行,每次只做一点点 避免一次性卡顿太久
增量回收的优势:
css
传统回收:
[一次性回收所有对象]
暂停时间:50ms
用户感觉:明显卡顿
增量回收:
[回收一点][回收一点][回收一点]
每次暂停:5ms
用户感觉:几乎感觉不到
示例:
java
// 用户操作时,GC不会一次性暂停太久
// 而是分成多次,每次暂停很短时间
button.setOnClickListener(v -> {
// 即使此时GC,也不会明显卡顿
// 因为GC是增量执行的
});
3.3.5 压缩回收(优化特性)
回收后整理内存 把分散的内存碎片合并成连续空间 避免内存浪费,方便后续分配
压缩回收的优势:
css
压缩前:
[对象1][空闲][对象2][空闲][对象3]
内存碎片多,分配效率低
压缩后:
[对象1][对象2][对象3][空闲][空闲]
内存连续,分配效率高
总结:这些都是"并发复制收集器"的特性,它们同时发挥作用,让回收更高效、更少卡顿
3.4 Android GC的设计目标
低延迟 :尽量减少GC导致的卡顿 并发处理 :尽量在后台执行,不阻塞主线程 适合交互式应用:优化用户滑动、点击等操作的流畅度
3.4 ART GC的触发条件(什么时候会触发垃圾回收)
说明:触发条件就是"什么情况下系统会自动开始清理垃圾"
3.4.1 Partial GC(部分回收)的触发条件
什么时候触发:年轻代空间满了
具体场景:
- 你创建了很多新对象,年轻代内存快用完了
- 系统检测到年轻代空间不足,自动触发Partial GC
- 只清理年轻代,不清理老年代
特点:
- 触发快,执行快
- 暂停时间短(几毫秒)
- 对应用影响小,用户基本感觉不到
示例:
java
public void createManyObjects() {
List<Object> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
list.add(new Object()); // 创建大量对象
// 当年轻代快满时,触发Partial GC
}
}
3.4.2 Full GC(完全回收)的触发条件
什么时候触发:整个堆内存不足
具体场景:
- 老年代空间满了
- 堆内存整体不足,无法分配新对象
- 显式调用System.gc()(Android不推荐,因为会强制触发Full GC)
特点:
- 执行慢,暂停时间长(可能几十到几百毫秒)
- 会清理整个堆(年轻代+老年代)
- 可能导致应用卡顿,用户能感觉到
示例:
java
// 不推荐:会强制触发Full GC
System.gc(); // Android不推荐使用
// 应该让系统自动管理
// 通过优化代码,减少Full GC的触发
3.4.3 Sticky GC(粘性回收)的触发条件
什么时候触发:频繁创建短生命周期对象时
具体场景:
- 列表快速滑动时(RecyclerView滚动)
- 动画播放时(不断创建临时对象)
- UI渲染时(频繁创建View相关对象)
特点:
- 只清理刚刚创建的新对象
- 非常快速,几乎感觉不到
- 专门优化频繁分配对象的场景
示例:
java
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
// 滑动时频繁创建对象
// 触发Sticky GC,快速清理
}
});
3.4.4 如何查看和预防GC
查看GC日志:
- 使用logcat查看GC事件
- 使用Android Studio Profiler监控GC
- 使用Systrace分析GC对性能的影响
预防频繁GC:
- 减少对象分配(复用对象、使用对象池)
- 合理使用缓存(及时清理不需要的缓存)
- 避免内存泄漏(正确释放资源)
3.5 ART内存分配策略(对象在内存中是怎么分配的)
说明:分配策略就是"新创建的对象放在内存的哪个位置"
3.5.1 新对象优先在年轻代分配
分配规则:新创建的对象先放在年轻代
为什么这样设计:
- 大部分对象生命周期很短,很快就会被回收
- 放在年轻代,回收快,效率高
具体流程:
- 你写代码:
Object obj = new Object() - 系统分配:对象放在年轻代
- 如果年轻代满了:触发Partial GC,清理后再分配
分配失败怎么办:
- 如果年轻代满了,先触发Partial GC
- GC后如果还是不够,可能触发Full GC
- 如果Full GC后还是不够,可能抛出OutOfMemoryError
示例:
java
public void allocateObjects() {
// 新对象分配在年轻代
Object obj1 = new Object(); // 年轻代
Object obj2 = new Object(); // 年轻代
// 如果年轻代满了,触发Partial GC
// GC后继续分配
}
3.5.2 大对象的处理
什么是大对象:
- 占用内存很大的对象(比如大图片、大数组)
- Android场景:Bitmap、大数组等
大对象怎么分配:
- 如果对象太大,可能直接放在老年代
- 避免在年轻代之间复制大对象(复制成本高)
大对象对GC的影响:
- 大对象占用内存多,容易导致内存不足
- 大对象回收慢,可能影响GC性能
- 建议:及时释放大对象,避免内存压力
示例:
java
// 大对象可能直接进入老年代
Bitmap largeBitmap = Bitmap.createBitmap(4000, 4000, Bitmap.Config.ARGB_8888);
// 这个Bitmap很大,可能直接放在老年代
// 建议:及时释放
largeBitmap.recycle();
largeBitmap = null;
3.5.3 长期存活的对象晋升到老年代
晋升规则:对象在年轻代存活足够久,就移到老年代
年龄计数机制:
- 每次Partial GC后,存活的对象年龄+1
- 年龄达到阈值(比如15次),就晋升到老年代
为什么需要晋升:
- 长期存活的对象放在年轻代,每次GC都要检查,浪费效率
- 移到老年代,减少检查次数,提高效率
晋升时机:
- 对象在年轻代经历多次GC后仍存活
- 年龄达到阈值时自动晋升
示例:
java
public class LongLivedObject {
private static List<Object> cache = new ArrayList<>();
public void addToCache(Object obj) {
cache.add(obj); // 这个对象被cache引用,长期存活
// 经过多次GC后,会晋升到老年代
}
}
3.5.4 Android内存限制
不同设备的堆内存限制:
- 不同Android设备有不同的堆内存上限
- 低端设备可能只有几十MB
- 高端设备可能有几百MB到几GB
低内存设备的处理:
- 系统会限制每个应用的内存使用
- 内存不足时,系统可能杀死后台应用
- 你的应用可能收到onLowMemory()回调
内存优化的重要性:
- Android设备内存有限,必须合理使用
- 内存占用过高,可能导致应用被系统杀死
- 优化内存使用,提升应用性能和稳定性
示例:
java
public class MyActivity extends Activity {
@Override
public void onLowMemory() {
super.onLowMemory();
// 系统内存不足时调用
// 应该释放不必要的资源
clearCache();
}
private void clearCache() {
// 清理缓存,释放内存
}
}
第四部分:Android GC优化与实践
4. Android GC优化(如何减少GC,提升性能)
4.1 Android GC优化的目标
降低GC暂停时间 :最重要,减少卡顿 减少内存占用 :降低内存压力 提升应用流畅度 :优化用户体验 避免ANR:防止应用无响应
4.2 Android开发中的GC优化策略
4.2.1 减少对象分配
对象复用(使用对象池,避免频繁创建):
java
// 不好的做法:频繁创建对象
for (int i = 0; i < 1000; i++) {
String str = new String("Hello"); // 每次都创建新对象
}
// 好的做法:复用对象
String str = "Hello";
for (int i = 0; i < 1000; i++) {
// 复用同一个对象
}
避免不必要的对象创建:
java
// 不好的做法:字符串拼接
String result = "";
for (int i = 0; i < 100; i++) {
result += i; // 每次都创建新String对象
}
// 好的做法:使用StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100; i++) {
sb.append(i); // 复用StringBuilder
}
String result = sb.toString();
使用基本类型替代包装类型:
java
// 不好的做法:使用包装类型
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
list.add(new Integer(i)); // 创建Integer对象
}
// 好的做法:使用基本类型数组
int[] array = new int[1000];
for (int i = 0; i < 1000; i++) {
array[i] = i; // 不创建对象
}
4.2.2 合理使用缓存
避免内存泄漏:
java
// 不好的做法:无限制缓存
private Map<String, Bitmap> cache = new HashMap<>();
public void addToCache(String key, Bitmap bitmap) {
cache.put(key, bitmap); // 可能无限增长
}
// 好的做法:使用LruCache
private LruCache<String, Bitmap> cache = new LruCache<>(10 * 1024 * 1024);
public void addToCache(String key, Bitmap bitmap) {
cache.put(key, bitmap); // 自动限制大小
}
及时清理不再使用的缓存:
java
@Override
protected void onDestroy() {
super.onDestroy();
// 清理缓存
cache.clear();
}
4.2.3 优化数据结构
选择合适的集合类:
java
// 如果需要随机访问,用ArrayList
List<String> list = new ArrayList<>(); // 随机访问快
// 如果需要频繁插入删除,用LinkedList
List<String> list = new LinkedList<>(); // 插入删除快
避免内存浪费:
java
// 不好的做法:不清理集合
List<Object> list = new ArrayList<>();
// ... 使用list
// list不再使用,但没有清理
// 好的做法:及时清理
list.clear();
list = null;
4.2.4 避免内存泄漏
正确处理生命周期:
java
public class MyActivity extends Activity {
private Handler handler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
handler = new MyHandler(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
// 清理Handler,避免内存泄漏
handler.removeCallbacksAndMessages(null);
handler = null;
}
}
避免持有Activity/Context的引用:
java
// 不好的做法:静态变量持有Activity
private static Activity activity; // 可能导致内存泄漏
// 好的做法:使用Application Context
private static Context appContext; // 使用Application Context
使用弱引用:
java
// 使用弱引用避免内存泄漏
private static class MyHandler extends Handler {
private WeakReference<Activity> activityRef;
MyHandler(Activity activity) {
activityRef = new WeakReference<>(activity);
}
}
4.3 GC优化实践场景
4.3.1 列表滑动优化
RecyclerView的ViewHolder复用:
java
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// ViewHolder复用,避免频繁创建
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
// 避免在这里创建新对象
// 复用ViewHolder中的View
}
}
避免在onBindViewHolder中创建新对象:
java
// 不好的做法
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.textView.setText(new String("Item " + position)); // 创建新对象
}
// 好的做法
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.textView.setText("Item " + position); // 复用字符串
}
4.3.2 动画性能优化
复用动画对象:
java
// 不好的做法:每次创建新动画
button.setOnClickListener(v -> {
ObjectAnimator animator = new ObjectAnimator(); // 每次都创建
animator.start();
});
// 好的做法:复用动画对象
private ObjectAnimator animator;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
animator = new ObjectAnimator();
}
button.setOnClickListener(v -> {
animator.start(); // 复用
});
4.3.3 图片加载优化
使用图片缓存:
java
// 使用Glide等库,自动缓存
Glide.with(context)
.load(url)
.into(imageView);
及时释放不用的Bitmap:
java
@Override
protected void onDestroy() {
super.onDestroy();
if (bitmap != null) {
bitmap.recycle();
bitmap = null;
}
}
避免加载过大的图片:
java
// 加载时压缩图片
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 2; // 压缩2倍
Bitmap bitmap = BitmapFactory.decodeFile(path, options);
4.3.4 大对象处理优化
及时释放大对象:
java
private Bitmap largeBitmap;
public void loadImage() {
largeBitmap = loadLargeBitmap();
// 使用完后及时释放
if (largeBitmap != null) {
largeBitmap.recycle();
largeBitmap = null;
}
}
4.2 内存泄漏(Memory Leak)
4.2.1 什么是内存泄漏
定义: 对象已经不再使用,但因为被引用无法被GC回收,导致内存浪费
与内存溢出的区别:
- 内存泄漏:对象无法回收,内存逐渐被占用
- 内存溢出:内存真的不够用了,无法分配新对象
危害:
- 内存逐渐被占用,最终可能导致OOM
- 应用变慢,可能被系统杀死
示例:
java
// 内存泄漏示例
public class MemoryLeak {
private static List<Object> cache = new ArrayList<>();
public void addToCache(Object obj) {
cache.add(obj); // 对象被cache引用,无法回收
// 即使obj不再使用,也无法被GC回收
}
}
4.2.2 Android中常见的内存泄漏场景
4.2.2.1 Activity/Context泄漏
静态变量持有Activity引用:
java
// 不好的做法:内存泄漏
public class MyActivity extends Activity {
private static Activity activity; // 静态变量持有Activity
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
activity = this; // Activity无法被回收
}
}
// 好的做法:使用弱引用
public class MyActivity extends Activity {
private static WeakReference<Activity> activityRef;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
activityRef = new WeakReference<>(this);
}
}
单例持有Context引用:
java
// 不好的做法:持有Activity Context
public class MySingleton {
private static MySingleton instance;
private Context context;
private MySingleton(Context context) {
this.context = context; // 如果是Activity Context,会泄漏
}
public static MySingleton getInstance(Context context) {
if (instance == null) {
instance = new MySingleton(context);
}
return instance;
}
}
// 好的做法:使用Application Context
public class MySingleton {
private static MySingleton instance;
private Context context;
private MySingleton(Context context) {
this.context = context.getApplicationContext(); // 使用Application Context
}
}
内部类持有外部类引用:
java
// 不好的做法:Handler内存泄漏
public class MyActivity extends Activity {
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
// Handler持有Activity引用
}
};
}
// 好的做法:使用静态内部类+弱引用
public class MyActivity extends Activity {
private static class MyHandler extends Handler {
private WeakReference<Activity> activityRef;
MyHandler(Activity activity) {
activityRef = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
Activity activity = activityRef.get();
if (activity != null) {
// 使用activity
}
}
}
}
4.2.2.2 监听器泄漏
EventBus未取消注册:
java
// 不好的做法:忘记取消注册
public class MyActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EventBus.getDefault().register(this); // 注册
// 忘记取消注册,EventBus持有Activity引用,导致内存泄漏
}
}
// 好的做法:在onDestroy中取消注册
@Override
protected void onDestroy() {
super.onDestroy();
EventBus.getDefault().unregister(this); // 取消注册
}
广播接收器未取消注册:
java
// 不好的做法:忘记取消注册
public class MyActivity extends Activity {
private BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
// 处理广播
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
registerReceiver(receiver, new IntentFilter("ACTION")); // 注册
// 忘记取消注册,导致内存泄漏
}
}
// 好的做法:在onDestroy中取消注册
@Override
protected void onDestroy() {
super.onDestroy();
if (receiver != null) {
unregisterReceiver(receiver); // 取消注册
}
}
其他监听器未取消注册:
java
// 不好的做法:忘记取消注册监听器
public class MyActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
someView.setOnClickListener(listener); // 注册
someObject.setOnDataChangedListener(listener); // 注册
// 如果这些监听器持有Activity引用,忘记取消注册会导致泄漏
}
}
// 好的做法:在onDestroy中取消注册
@Override
protected void onDestroy() {
super.onDestroy();
someView.setOnClickListener(null); // 取消注册
someObject.setOnDataChangedListener(null); // 取消注册
}
4.2.2.3 集合类泄漏
集合中保存了对象,但忘记清理:
java
// 不好的做法:集合泄漏
private List<Object> list = new ArrayList<>();
public void addObject(Object obj) {
list.add(obj); // 对象被list引用
// 即使obj不再使用,也无法被回收
}
// 好的做法:及时清理
public void removeObject(Object obj) {
list.remove(obj);
}
@Override
protected void onDestroy() {
super.onDestroy();
list.clear(); // 清理集合
}
4.2.2.4 线程泄漏
线程持有对象引用,线程不结束,对象无法回收:
java
// 不好的做法:线程泄漏
public class MyActivity extends Activity {
private Thread thread;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
thread = new Thread(() -> {
// 线程持有Activity引用
while (true) {
// 无限循环,线程不结束
}
});
thread.start();
}
}
// 好的做法:正确管理线程
@Override
protected void onDestroy() {
super.onDestroy();
if (thread != null) {
thread.interrupt(); // 中断线程
}
}
4.2.2.5 资源未关闭
文件流未关闭:
java
// 不好的做法:文件流未关闭
public void readFile() {
FileInputStream fis = new FileInputStream("file.txt");
// 使用fis
// 忘记关闭,导致资源泄漏
}
// 好的做法:使用try-with-resources
public void readFile() {
try (FileInputStream fis = new FileInputStream("file.txt")) {
// 使用fis
// 自动关闭
} catch (IOException e) {
e.printStackTrace();
}
}
数据库连接未关闭:
java
// 不好的做法:数据库连接未关闭
public void queryDatabase() {
SQLiteDatabase db = getWritableDatabase();
Cursor cursor = db.query("table", null, null, null, null, null, null);
// 使用cursor
// 忘记关闭cursor和db,导致资源泄漏
}
// 好的做法:及时关闭
public void queryDatabase() {
SQLiteDatabase db = getWritableDatabase();
try (Cursor cursor = db.query("table", null, null, null, null, null, null)) {
// 使用cursor
// 自动关闭cursor
} finally {
db.close(); // 关闭数据库连接
}
}
网络连接未关闭:
java
// 不好的做法:网络连接未关闭
public void downloadFile() {
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
// 使用connection
// 忘记关闭,导致资源泄漏
}
// 好的做法:及时关闭
public void downloadFile() {
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
try {
// 使用connection
} finally {
connection.disconnect(); // 关闭连接
}
}
Bitmap未释放:
java
// 不好的做法:Bitmap未释放
public void loadImage() {
Bitmap bitmap = BitmapFactory.decodeFile("image.jpg");
imageView.setImageBitmap(bitmap);
// 忘记释放bitmap,导致内存泄漏
}
// 好的做法:及时释放
@Override
protected void onDestroy() {
super.onDestroy();
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle(); // 释放Bitmap
bitmap = null;
}
}
AsyncTask泄漏:
java
// 不好的做法:AsyncTask持有Activity引用
public class MyActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
new MyAsyncTask().execute(); // AsyncTask持有Activity引用
}
private class MyAsyncTask extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... params) {
// 长时间运行
return null;
}
}
}
// 好的做法:使用静态内部类+弱引用
private static class MyAsyncTask extends AsyncTask<Void, Void, Void> {
private WeakReference<Activity> activityRef;
MyAsyncTask(Activity activity) {
activityRef = new WeakReference<>(activity);
}
@Override
protected Void doInBackground(Void... params) {
Activity activity = activityRef.get();
if (activity != null) {
// 使用activity
}
return null;
}
}
4.2.3 如何避免内存泄漏
正确处理生命周期:
java
@Override
protected void onDestroy() {
super.onDestroy();
// 清理资源
handler.removeCallbacksAndMessages(null);
EventBus.getDefault().unregister(this);
cache.clear();
}
使用弱引用:
java
// 对于可能泄漏的引用,使用WeakReference
private WeakReference<Activity> activityRef;
避免循环引用:
java
// 注意对象之间的相互引用
// 及时断开不需要的引用
合理使用缓存:
java
// 使用有大小限制的缓存(LruCache)
private LruCache<String, Bitmap> cache = new LruCache<>(10 * 1024 * 1024);
4.3 内存溢出(OOM - OutOfMemoryError)
4.3.1 什么是内存溢出
定义: 应用需要的内存超过了系统分配的限制,无法分配新对象
Android堆内存溢出: 最常见,堆内存不足
危害: 应用崩溃,用户体验差
示例:
java
public class OOMExample {
public void createTooManyObjects() {
List<Object> list = new ArrayList<>();
while (true) {
list.add(new Object()); // 最终导致OOM
}
}
}
4.3.2 内存溢出的原因
内存泄漏导致:
- 对象无法回收,内存逐渐被占用
- 最终导致内存不足
创建了太多对象:
- 一次性加载大量数据
- 循环中创建大量对象
大对象占用过多内存:
- 加载过大的图片
- 创建过大的数组
Android设备内存限制:
- 不同设备有不同的堆内存上限
- 低端设备内存更小
4.3.3 如何解决内存溢出
排查内存泄漏:
- 使用LeakCanary检测
- 使用MAT分析堆转储
- 修复内存泄漏问题
优化内存使用:
- 减少对象创建
- 及时释放不需要的对象
- 使用对象池复用对象
处理大对象:
- 压缩图片大小
- 分批加载数据
- 及时释放大对象
增加内存限制(治标不治本):
- 在AndroidManifest.xml中设置largeHeap="true"
- 但这不是根本解决方案,还是要优化内存使用
第五部分:Android GC检测与诊断(工具和方法)
5.1 Android GC检测工具和方法
说明:这部分都是用来检测、分析、诊断GC和内存问题的工具和方法
5.1.1 Android Studio Profiler(最常用的工具)
作用: 实时监控应用的内存、CPU、网络等性能数据
Memory Profiler(内存分析器)功能:
-
实时查看内存使用情况
- 显示内存使用曲线图
- 可以看到内存随时间的变化趋势
- 识别内存泄漏(内存持续增长)
-
查看GC事件
- GC事件会在内存曲线上显示为小图标
- 点击GC事件可以查看详细信息:
- GC类型(Partial GC、Full GC、Sticky GC)
- GC耗时(暂停时间)
- 回收前后的内存大小
- 回收了多少内存
-
生成堆转储(Heap Dump)
- 点击"Heap Dump"按钮生成堆转储
- 可以查看当前所有对象的内存占用
- 可以导出为hprof文件,用MAT分析
-
查看对象分配情况
- 使用"Allocation Tracking"功能
- 记录一段时间内的对象分配
- 可以看到哪些对象被分配,在哪里分配(调用栈)
CPU Profiler(CPU分析器)功能:
- 查看GC占用的CPU时间
- 分析GC对性能的影响
- 查看GC线程的执行情况
详细使用步骤:
-
打开Profiler窗口
- 在Android Studio中,点击底部的"Profiler"标签
- 或通过菜单:View → Tool Windows → Profiler
-
选择要分析的应用进程
- 在Profiler窗口中选择要分析的应用进程
- 确保应用正在运行(debug模式)
-
查看内存使用情况
- 点击"Memory"标签
- 查看内存使用曲线
- 观察内存是否持续增长(可能的内存泄漏)
-
查看GC事件
- 在内存曲线中,GC事件会显示为小图标
- 点击GC事件,可以查看详细信息
- 分析GC频率和耗时
-
生成堆转储
- 点击"Heap Dump"按钮
- 等待生成完成
- 可以查看对象列表、内存占用等
- 可以导出为hprof文件
-
查看对象分配情况
- 点击"Record allocations"按钮开始记录
- 操作应用(比如滑动列表)
- 点击"Stop recording"停止记录
- 查看哪些对象被分配,在哪里分配
使用场景:
- 日常开发时监控内存使用
- 分析内存泄漏
- 分析GC性能
- 优化内存使用
5.1.2 logcat(查看GC日志)
作用: 查看GC的日志信息,了解GC的执行情况
GC日志的作用:
- 了解GC何时触发
- 了解GC的类型和耗时
- 分析GC频率是否过高
- 分析GC对性能的影响
如何使用:
方法1:使用adb logcat(命令行)
bash
# 查看所有GC日志
adb logcat | grep GC
# 查看特定应用的GC日志
adb logcat | grep -E "GC|dalvikvm|art"
方法2:在Android Studio的Logcat窗口中查看
- 打开Logcat窗口(底部标签)
- 在过滤框中输入"GC"或"dalvikvm"或"art"
- 查看GC相关的日志
GC日志格式示例:
c
GC: AllocSpace concurrent mark sweep freed
12345(456KB) AllocSpace bytes,
12(234KB) LOS objects,
50% free, 5MB/10MB,
paused 2.3ms total 15.6ms
GC日志包含的信息:
- GC类型:Partial GC、Full GC、Sticky GC
- GC耗时:paused(暂停时间)、total(总耗时)
- 回收前后的内存大小:freed(释放了多少内存)
- 内存使用情况:free(空闲内存)、total(总内存)
- GC频率:多久触发一次GC
如何分析GC日志:
-
查看GC频率
- 统计一段时间内GC的次数
- 如果GC过于频繁(比如每秒多次),说明有问题
- 可能原因:频繁创建对象、内存泄漏
-
查看GC耗时
- 关注paused时间(暂停时间)
- 如果暂停时间过长(比如>50ms),会导致卡顿
- Full GC的暂停时间通常比Partial GC长
-
查看GC类型
- Partial GC:正常,快速
- Full GC:需要关注,可能导致卡顿
- Sticky GC:正常,几乎感觉不到
-
查看内存回收情况
- freed:释放了多少内存
- 如果释放的内存很少,说明大部分对象还在使用
- 如果释放的内存很多,说明有很多垃圾对象
使用场景:
- 快速查看GC情况
- 分析GC频率和耗时
- 排查GC导致的性能问题
5.1.3 Systrace(分析GC对性能的影响)
作用: 分析GC对性能的影响,找出GC导致的卡顿问题
Systrace是什么:
- Android系统提供的性能分析工具
- 可以记录系统各个线程的执行情况
- 可以看到GC在主线程上的执行时间
如何使用:
方法1:使用Android Studio的Systrace工具
- 在Android Studio中,点击"Tools" → "Android" → "Device Monitor"
- 选择要分析的应用
- 点击"Systrace"按钮
- 设置记录时间(比如10秒)
- 操作应用(比如滑动列表)
- 停止记录,查看结果
方法2:使用命令行工具
bash
# 开始记录
python systrace.py -t 10 -o trace.html sched freq idle am wm gfx view binder_driver hal dalvik camera input res
# 在设备上操作应用
# 停止记录后,用浏览器打开trace.html查看
Systrace能分析什么:
-
GC暂停时间
- 可以看到GC在主线程上的执行时间
- 可以看到GC暂停了多久
- 可以判断GC是否导致卡顿
-
GC对主线程的影响
- 可以看到主线程在GC期间被阻塞
- 可以看到GC导致的帧率下降
- 可以分析GC对用户体验的影响
-
应用卡顿的原因
- 可以看到卡顿是否由GC引起
- 可以看到GC和其他操作的时序关系
- 可以找出性能瓶颈
如何查看Systrace结果:
-
查看主线程
- 找到"main"线程
- 查看是否有红色的帧(表示掉帧)
- 查看是否有GC事件
-
查看GC事件
- 找到"GC"相关的事件
- 查看GC的持续时间
- 查看GC是否与卡顿时间对应
-
分析卡顿原因
- 如果卡顿时间与GC时间对应,说明GC导致卡顿
- 如果GC频繁,需要优化代码减少GC
- 如果GC暂停时间长,需要避免Full GC
使用场景:
- 分析应用卡顿问题
- 分析GC对性能的影响
- 优化GC性能
5.1.4 MAT(Memory Analyzer Tool,分析堆转储)
作用: 分析堆转储文件,详细分析内存问题,找出内存泄漏和大对象
MAT是什么:
- Eclipse Memory Analyzer Tool
- 专门用于分析Java堆转储文件的工具
- 可以详细分析对象的内存占用和引用关系
MAT下载和安装:
1. 下载MAT
- 访问Eclipse官网:www.eclipse.org/mat/
- 下载MAT安装包(Windows/Mac/Linux)
- 或者下载独立版本(不需要Eclipse)
2. 安装MAT
- Windows:解压下载的文件,运行MemoryAnalyzer.exe
- Mac/Linux:解压下载的文件,运行MemoryAnalyzer可执行文件
- 确保已安装Java(MAT需要Java运行环境)
Android Studio生成堆转储的详细步骤:
方法1:使用Android Studio Profiler(推荐)
步骤1:打开Profiler
- 在Android Studio中,点击底部工具栏的"Profiler"标签
- 或通过菜单:View → Tool Windows → Profiler
- 确保应用正在运行(debug模式)
步骤2:选择应用进程
- 在Profiler窗口中,选择要分析的应用进程
- 确保应用正在运行
步骤3:打开Memory Profiler
- 点击"Memory"标签
- 查看内存使用情况
步骤4:生成堆转储
- 点击内存曲线图下方的"Heap Dump"按钮(垃圾桶图标)
- 等待生成完成(可能需要几秒到几十秒,取决于堆大小)
- 生成完成后,会在下方显示堆转储信息
步骤5:导出hprof文件
- 在堆转储信息区域,点击"Export heap dump"按钮
- 选择保存位置,保存为.hprof文件
- 比如:
heap.hprof
方法2:使用命令行生成堆转储
步骤1:找到应用进程ID
bash
# 查看所有应用进程
adb shell ps | grep 你的应用包名
# 或者
adb shell ps -A | grep 你的应用包名
步骤2:生成堆转储
bash
# 方法1:使用am命令(需要root权限或debug版本)
adb shell am dumpheap <pid> /data/local/tmp/heap.hprof
# 方法2:使用kill命令(发送信号)
adb shell kill -10 <pid>
# 方法3:在代码中生成(开发时)
Debug.dumpHprofData("/data/local/tmp/heap.hprof");
步骤3:从设备拉取hprof文件
bash
# 从设备拉取到本地
adb pull /data/local/tmp/heap.hprof ./
转换hprof文件格式的详细步骤:
为什么需要转换:
- Android的hprof文件格式与标准Java不同
- MAT无法直接打开Android的hprof文件
- 需要使用Android SDK的hprof-conv工具转换
步骤1:找到hprof-conv工具
- hprof-conv工具位于Android SDK的platform-tools目录
- Windows:
%LOCALAPPDATA%\Android\Sdk\platform-tools\hprof-conv.exe - Mac/Linux:
~/Library/Android/sdk/platform-tools/hprof-conv
步骤2:转换hprof文件
bash
# Windows
hprof-conv.exe heap.hprof heap-converted.hprof
# Mac/Linux
./hprof-conv heap.hprof heap-converted.hprof
# 或者使用完整路径
C:\Users\你的用户名\AppData\Local\Android\Sdk\platform-tools\hprof-conv.exe heap.hprof heap-converted.hprof
步骤3:验证转换结果
- 转换完成后,会生成heap-converted.hprof文件
- 文件大小应该与原始文件相似
- 现在可以用MAT打开了
MAT打开和分析的详细步骤:
步骤1:打开MAT
- 运行MAT(MemoryAnalyzer.exe或MemoryAnalyzer)
- 等待MAT启动完成
步骤2:打开hprof文件
- 点击"File" → "Open Heap Dump"
- 或者直接拖拽hprof文件到MAT窗口
- 选择转换后的hprof文件(heap-converted.hprof)
- 点击"Open"
步骤3:等待MAT分析
- MAT会自动分析堆转储文件
- 分析时间取决于堆大小(可能需要几分钟)
- 分析完成后,会显示分析结果
步骤4:查看Leak Suspects(泄漏嫌疑)
- MAT会自动检测可能的内存泄漏
- 点击"Leak Suspects"标签
- 查看MAT自动生成的泄漏报告
- 报告会显示可疑的泄漏对象和引用链
步骤5:使用Histogram(直方图)分析
- 点击"Histogram"标签
- 查看所有类的实例数量和内存占用
- 按内存占用排序 :
- 点击"Retained Heap"列标题,按内存占用排序
- 找出占用内存最多的类
- 查看类的实例 :
- 双击某个类,查看这个类的所有实例
- 可以看到每个实例占用的内存
步骤6:使用Dominator Tree(支配树)分析
- 点击"Dominator Tree"标签
- 查看对象的引用关系树
- 按内存占用排序 :
- 点击"Retained Heap"列标题,按内存占用排序
- 找出占用内存最多的对象
- 查看对象的引用关系 :
- 展开对象,可以看到它引用的其他对象
- 可以找到哪些对象持有大量内存
步骤7:使用Path to GC Roots(到GC Roots的路径)分析引用链
- 选择对象 :
- 在Histogram或Dominator Tree中,右键点击可疑对象
- 选择"Path to GC Roots" → "exclude weak references"(排除弱引用)
- 查看引用链 :
- MAT会显示对象到GC Roots的完整引用链
- 可以看到是哪个对象持有泄漏对象的引用
- 找出泄漏的原因
MAT分析内存泄漏的实际操作示例:
场景:Activity泄漏
步骤1:生成堆转储
- 在Android Studio Profiler中生成堆转储
- 导出为heap.hprof
步骤2:转换文件
bash
hprof-conv heap.hprof heap-converted.hprof
步骤3:用MAT打开
- 用MAT打开heap-converted.hprof
步骤4:查找泄漏的Activity
- 打开Histogram
- 在搜索框输入"Activity"
- 找到MyActivity类
- 查看实例数量(如果有多个实例,可能有泄漏)
步骤5:分析引用链
-
右键点击MyActivity实例
-
选择"Path to GC Roots" → "exclude weak references"
-
查看引用链,比如:
scssMyActivity实例 ↑ (被引用) static MyActivity.activity (静态变量) ↑ (GC Root) -
找出是静态变量持有Activity引用
步骤6:定位代码
- 根据引用链,定位到代码中的静态变量
- 修复代码(使用弱引用或及时清理)
MAT的主要功能详细说明:
1. Histogram(直方图)
- 作用:显示所有类的实例数量和内存占用
- 使用方法 :
- 打开"Histogram"视图
- 在搜索框输入类名,查找特定类
- 点击列标题排序(比如按"Retained Heap"排序)
- 双击类名,查看这个类的所有实例
- 适用场景:找出占用内存最多的类
2. Dominator Tree(支配树)
- 作用:显示对象的引用关系树,找出占用内存最多的对象
- 使用方法 :
- 打开"Dominator Tree"视图
- 点击"Retained Heap"列标题,按内存占用排序
- 展开对象,查看引用关系
- 找出占用内存最多的对象
- 适用场景:找出占用内存最多的对象
3. Path to GC Roots(到GC Roots的路径)
- 作用:显示对象到GC Roots的引用链,找出为什么对象无法被回收
- 使用方法 :
- 右键点击可疑对象
- 选择"Path to GC Roots"
- 选择"exclude weak references"(排除弱引用)
- 查看引用链,找出泄漏原因
- 适用场景:分析内存泄漏的原因
4. Leak Suspects(泄漏嫌疑)
- 作用:MAT自动检测可能的内存泄漏,生成泄漏报告
- 使用方法 :
- 打开"Leak Suspects"视图
- 查看MAT自动检测的泄漏报告
- 根据报告分析泄漏原因
- 适用场景:快速定位内存泄漏
5. OQL(Object Query Language)
- 作用:使用类似SQL的查询语言查询对象
- 使用方法 :
-
打开"OQL"视图
-
输入查询语句,比如:
sqlSELECT * FROM java.lang.String WHERE this.value.length > 100 -
执行查询,查看结果
-
- 适用场景:查找特定条件的对象
MAT分析技巧:
1. 对比多个堆转储
- 生成多个时间点的堆转储
- 对比分析,找出内存增长的原因
- 可以看到哪些对象在增长
2. 使用OQL查询
- 使用OQL查询特定对象
- 比如:查询所有Activity实例
- 分析这些对象是否应该存在
3. 关注Retained Heap
- Retained Heap表示对象及其引用的对象占用的总内存
- 关注Retained Heap大的对象
- 这些对象可能是内存泄漏的根源
4. 排除弱引用
- 在Path to GC Roots时,选择"exclude weak references"
- 弱引用不会阻止对象被回收,可以排除
- 只关注强引用,找出真正的泄漏原因
常见问题:
1. MAT无法打开hprof文件
- 原因:Android的hprof格式与标准Java不同
- 解决:使用hprof-conv工具转换后再打开
2. MAT分析很慢
- 原因:堆转储文件很大
- 解决:等待分析完成,或者减小堆转储大小
3. 找不到泄漏对象
- 原因:泄漏对象可能不在当前堆转储中
- 解决:在合适的时机生成堆转储,或者使用LeakCanary
4. 引用链太复杂
- 原因:对象引用关系复杂
- 解决:从最可疑的对象开始分析,逐步缩小范围
MAT的主要功能详细说明:
1. Histogram(直方图)
- 作用:显示所有类的实例数量和内存占用
- 使用方法 :
- 打开"Histogram"视图
- 在搜索框输入类名,查找特定类
- 点击列标题排序(比如按"Retained Heap"排序)
- 双击类名,查看这个类的所有实例
- 适用场景:找出占用内存最多的类
2. Dominator Tree(支配树)
- 作用:显示对象的引用关系树,找出占用内存最多的对象
- 使用方法 :
- 打开"Dominator Tree"视图
- 点击"Retained Heap"列标题,按内存占用排序
- 展开对象,查看引用关系
- 找出占用内存最多的对象
- 适用场景:找出占用内存最多的对象
3. Path to GC Roots(到GC Roots的路径)
- 作用:显示对象到GC Roots的引用链,找出为什么对象无法被回收
- 使用方法 :
- 右键点击可疑对象
- 选择"Path to GC Roots"
- 选择"exclude weak references"(排除弱引用)
- 查看引用链,找出泄漏原因
- 适用场景:分析内存泄漏的原因
4. Leak Suspects(泄漏嫌疑)
- 作用:MAT自动检测可能的内存泄漏,生成泄漏报告
- 使用方法 :
- 打开"Leak Suspects"视图
- 查看MAT自动检测的泄漏报告
- 根据报告分析泄漏原因
- 适用场景:快速定位内存泄漏
5. OQL(Object Query Language)
- 作用:使用类似SQL的查询语言查询对象
- 使用方法 :
-
打开"OQL"视图
-
输入查询语句,比如:
sqlSELECT * FROM java.lang.String WHERE this.value.length > 100 -
执行查询,查看结果
-
- 适用场景:查找特定条件的对象
MAT分析内存泄漏的详细步骤:
步骤1:打开堆转储文件
- 用MAT打开转换后的hprof文件
- 等待MAT分析完成(可能需要几分钟)
步骤2:查看Leak Suspects(推荐先看这个)
- 打开"Leak Suspects"标签
- 查看MAT自动检测的泄漏报告
- 报告会显示可疑的泄漏对象和引用链
- 根据报告快速定位问题
步骤3:使用Histogram找出大对象
- 打开"Histogram"视图
- 点击"Retained Heap"列标题,按内存占用排序
- 找出占用内存最多的类
- 双击类名,查看这个类的所有实例
步骤4:使用Dominator Tree分析对象关系
- 打开"Dominator Tree"视图
- 点击"Retained Heap"列标题,按内存占用排序
- 查看占用内存最多的对象
- 展开对象,查看引用关系
步骤5:使用Path to GC Roots分析引用链
- 在Histogram或Dominator Tree中,右键点击可疑对象
- 选择"Path to GC Roots" → "exclude weak references"
- 查看引用链,找出是哪个对象持有泄漏对象的引用
- 分析为什么对象无法被回收
步骤6:分析泄漏原因
- 根据引用链找出泄漏原因
- 常见原因:
- 静态变量持有
- 监听器未取消注册
- 集合类泄漏
- 其他原因
步骤7:定位代码并修复
- 根据引用链,定位到代码中的问题
- 修复代码(使用弱引用、及时清理等)
MAT分析技巧:
1. 对比多个堆转储
- 生成多个时间点的堆转储
- 对比分析,找出内存增长的原因
- 可以看到哪些对象在增长
2. 使用OQL查询
- 使用OQL查询特定对象
- 比如:查询所有Activity实例
- 分析这些对象是否应该存在
3. 关注Retained Heap
- Retained Heap表示对象及其引用的对象占用的总内存
- 关注Retained Heap大的对象
- 这些对象可能是内存泄漏的根源
4. 排除弱引用
- 在Path to GC Roots时,选择"exclude weak references"
- 弱引用不会阻止对象被回收,可以排除
- 只关注强引用,找出真正的泄漏原因
MAT实际操作示例:
场景:分析Activity泄漏
步骤1:在Android Studio中生成堆转储
- 打开Profiler
- 选择应用进程
- 点击"Heap Dump"按钮
- 等待生成完成
- 点击"Export heap dump"导出为heap.hprof
步骤2:转换hprof文件
bash
# 找到hprof-conv工具(在Android SDK的platform-tools目录)
# Windows
C:\Users\你的用户名\AppData\Local\Android\Sdk\platform-tools\hprof-conv.exe heap.hprof heap-converted.hprof
# Mac/Linux
~/Library/Android/sdk/platform-tools/hprof-conv heap.hprof heap-converted.hprof
步骤3:用MAT打开
- 打开MAT
- 点击"File" → "Open Heap Dump"
- 选择heap-converted.hprof
- 等待分析完成
步骤4:查找泄漏的Activity
- 打开"Histogram"视图
- 在搜索框输入"Activity"
- 找到MyActivity类
- 查看实例数量(正常应该是0或1,如果有多个,可能有泄漏)
步骤5:分析引用链
-
右键点击MyActivity实例
-
选择"Path to GC Roots" → "exclude weak references"
-
查看引用链:
scssMyActivity实例 ↑ (被引用) static MyActivity.activity (静态变量) ↑ (GC Root) -
找出是静态变量持有Activity引用
步骤6:定位代码
-
根据引用链,定位到代码中的静态变量
-
修复代码:
java// 不好的做法 private static Activity activity; // 好的做法:使用弱引用 private static WeakReference<Activity> activityRef;
使用场景:
- 详细分析内存泄漏
- 找出占用内存最多的对象
- 分析对象引用关系
- 排查OOM问题
- 优化内存使用
5.1.5 LeakCanary(自动检测内存泄漏)
作用: 自动检测应用中的内存泄漏,无需手动分析
LeakCanary是什么:
- Square公司开发的内存泄漏检测库
- 专门用于Android应用的内存泄漏检测
- 在开发阶段自动检测,发现泄漏时显示通知
核心检测原理(详细说明):
LeakCanary的检测原理基于**WeakReference(弱引用)和ReferenceQueue(引用队列)**的机制。
1. WeakReference(弱引用)的工作原理
什么是WeakReference:
- WeakReference是Java中的一种引用类型
- 弱引用不会阻止对象被GC回收
- 当对象只有弱引用时,GC会回收这个对象
WeakReference的特点:
java
// 创建弱引用
WeakReference<Activity> ref = new WeakReference<>(activity);
// 获取对象(可能返回null,如果对象已被回收)
Activity activity = ref.get();
// 如果activity被GC回收,ref.get()会返回null
2. ReferenceQueue(引用队列)的工作原理
什么是ReferenceQueue:
- ReferenceQueue是Java中的引用队列
- 当WeakReference引用的对象被GC回收时,WeakReference会被放入ReferenceQueue
- 通过检查ReferenceQueue,可以知道对象是否被回收
ReferenceQueue的工作流程:
arduino
1. 创建WeakReference时,可以指定ReferenceQueue
WeakReference<Activity> ref = new WeakReference<>(activity, queue);
2. 当activity被GC回收时,ref会被自动放入queue
3. 检查queue,如果ref在queue中,说明activity已被回收
Reference<?> polled = queue.poll();
if (polled != null) {
// 对象已被回收
}
3. LeakCanary的完整检测流程
第一步:监控对象生命周期
- LeakCanary通过Application.ActivityLifecycleCallbacks监控Activity的生命周期
- 当Activity的onDestroy()被调用时,LeakCanary开始检测
第二步:创建弱引用和引用队列
java
// LeakCanary内部实现(简化)
ReferenceQueue<Activity> queue = new ReferenceQueue<>();
WeakReference<Activity> ref = new WeakReference<>(activity, queue);
第三步:等待GC
- LeakCanary等待5秒(给GC时间)
- 然后触发一次GC(System.gc())
- 再等待一段时间,让GC完成
第四步:检查对象是否被回收
java
// 检查ReferenceQueue
Reference<?> polled = queue.poll();
if (polled != null) {
// 对象已被回收,没有内存泄漏
} else {
// 对象没有被回收,可能有内存泄漏
// 需要进一步确认
}
第五步:确认内存泄漏
- 如果对象没有被回收,LeakCanary会再次触发GC
- 如果多次GC后对象仍然没有被回收,确认有内存泄漏
第六步:生成堆转储并分析
- 如果确认有内存泄漏,LeakCanary会生成堆转储
- 分析对象的引用链,找出是哪个对象持有泄漏对象的引用
- 显示泄漏路径,帮助开发者快速定位问题
完整检测流程(时间线):
arduino
时间点0:Activity.onDestroy()被调用
↓
时间点1:LeakCanary创建WeakReference指向Activity
↓
时间点2:等待5秒(给GC时间)
↓
时间点3:触发GC(System.gc())
↓
时间点4:等待GC完成
↓
时间点5:检查ReferenceQueue
↓
├─ 如果ref在queue中 → Activity已被回收 → 没有泄漏
└─ 如果ref不在queue中 → Activity没有被回收 → 可能有泄漏
↓
时间点6:如果可能有泄漏,再次触发GC确认
↓
时间点7:如果确认泄漏,生成堆转储并分析
↓
时间点8:显示泄漏通知和泄漏路径
核心代码原理(详细版):
java
// LeakCanary的核心原理(详细)
public class LeakCanary {
private ReferenceQueue<Object> queue = new ReferenceQueue<>();
public void watch(Object object) {
// 1. 创建弱引用,指定引用队列
WeakReference<Object> ref = new WeakReference<>(object, queue);
// 2. 等待一段时间,给GC时间
try {
Thread.sleep(5000); // 等待5秒
} catch (InterruptedException e) {
e.printStackTrace();
}
// 3. 触发GC
System.gc();
// 4. 再等待一段时间,让GC完成
try {
Thread.sleep(1000); // 等待1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
// 5. 检查ReferenceQueue
Reference<?> polled = queue.poll();
if (polled != null) {
// 对象已被回收,没有内存泄漏
return;
}
// 6. 如果对象没有被回收,再次确认
if (ref.get() != null) {
// 对象没有被回收,确认有内存泄漏
// 生成堆转储并分析
analyzeLeak(object);
}
}
private void analyzeLeak(Object object) {
// 生成堆转储
HeapDump heapDump = generateHeapDump();
// 分析引用链
LeakTrace leakTrace = findLeakTrace(heapDump, object);
// 显示泄漏通知
showLeakNotification(leakTrace);
}
}
为什么WeakReference能检测内存泄漏?
原理说明:
-
正常情况(没有泄漏):
- Activity.onDestroy()后,Activity应该不再被引用
- GC时,Activity会被回收
- WeakReference会被放入ReferenceQueue
- 检查ReferenceQueue,发现ref在queue中,说明Activity已被回收
-
异常情况(有泄漏):
- Activity.onDestroy()后,Activity仍然被某个对象引用(比如静态变量)
- GC时,Activity因为有强引用,不会被回收
- WeakReference不会被放入ReferenceQueue
- 检查ReferenceQueue,发现ref不在queue中,说明Activity没有被回收
- 确认有内存泄漏
引用链分析原理:
当LeakCanary确认有内存泄漏后,会:
- 生成堆转储:保存当前所有对象的内存快照
- 分析引用链:从泄漏对象开始,向上查找引用链
- 找出泄漏路径:找出是哪个对象持有泄漏对象的引用
示例:
java
// 内存泄漏示例
public class MyActivity extends Activity {
private static Activity activity; // 静态变量持有Activity
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
activity = this; // 内存泄漏
}
}
// LeakCanary分析的引用链:
// MyActivity实例
// ↑ (被引用)
// static MyActivity.activity (静态变量)
// ↑ (GC Root)
//
// 泄漏路径:static MyActivity.activity -> MyActivity实例
如何使用:
1. 添加依赖
gradle
dependencies {
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12'
}
2. 运行应用
- LeakCanary会自动集成到应用中
- 不需要额外配置
- 会自动监控Activity和Fragment
3. 自动检测
- LeakCanary会自动检测内存泄漏
- 发现泄漏时会显示通知
- 点击通知可以查看泄漏路径
4. 查看泄漏报告
- LeakCanary会显示泄漏路径
- 可以清楚地看到是哪个对象持有泄漏对象的引用
- 根据泄漏路径快速定位问题
优点:
- 自动检测:不需要手动分析,自动检测内存泄漏
- 快速发现:能快速发现内存泄漏,提高开发效率
- 详细路径:显示泄漏路径,方便定位问题
- 适合开发阶段:在开发阶段使用,及时发现问题
注意事项:
- LeakCanary只在debug版本中使用
- 不要在生产版本中使用(会影响性能)
- LeakCanary会消耗一定的内存和CPU资源
5.2 GC问题排查方法
说明:这部分介绍如何排查常见的GC问题,包括识别、原因分析、排查步骤和解决方案
5.2.1 GC频繁问题排查
问题描述: GC触发过于频繁,影响应用性能
如何识别:
方法1:通过logcat查看GC日志
- 使用
adb logcat | grep GC查看GC日志 - 统计一段时间内GC的次数
- 如果GC过于频繁(比如每秒多次),说明有问题
方法2:通过Profiler查看GC事件
- 在Android Studio Profiler中查看内存曲线
- 观察GC事件的频率
- 如果GC事件密集,说明GC频繁
判断标准:
- 正常情况:GC频率较低(比如几秒一次)
- 异常情况:GC频率很高(比如每秒多次)
- 严重情况:GC几乎不间断,严重影响性能
可能的原因:
1. 频繁创建对象
- 代码中频繁创建对象
- 比如:循环中创建对象、列表滑动时创建对象
- 导致年轻代快速填满,频繁触发Partial GC
2. 内存泄漏导致内存不足
- 内存泄漏导致内存逐渐被占用
- 内存不足时,系统频繁触发GC尝试回收内存
- 但泄漏的对象无法被回收,GC无效
3. 年轻代设置太小(系统自动管理,通常不是问题)
- 年轻代太小,容易填满
- 填满后频繁触发Partial GC
4. 大对象频繁分配
- 频繁创建大对象(比如大图片)
- 大对象可能直接进入老年代
- 导致老年代快速填满,频繁触发Full GC
详细排查步骤:
步骤1:查看GC日志,确认GC频率
- 使用logcat查看GC日志
- 统计GC频率(比如:10秒内触发了几次GC)
- 记录GC类型(Partial GC还是Full GC)
- 记录GC耗时
步骤2:使用Profiler查看对象分配情况
- 使用Profiler的"Allocation Tracking"功能
- 记录一段时间内的对象分配
- 查看哪些对象被频繁分配
- 查看对象分配的调用栈
步骤3:找出频繁创建对象的地方
- 根据Profiler的分配记录,找出频繁创建对象的代码位置
- 常见场景:
- 循环中创建对象
- 列表滑动时创建对象(onBindViewHolder)
- 动画播放时创建对象
- 网络请求时创建对象
步骤4:分析代码,找出原因
- 分析代码,找出为什么频繁创建对象
- 检查是否有不必要的对象创建
- 检查是否有内存泄漏
步骤5:优化代码,减少对象创建
- 根据分析结果,优化代码
- 使用对象池复用对象
- 避免不必要的对象创建
- 修复内存泄漏
优化方案:
1. 使用对象池复用对象
- 对于频繁创建的对象,使用对象池
- 示例:RecyclerView的ViewHolder池、自定义对象池
2. 减少不必要的对象创建
- 使用StringBuilder而不是字符串拼接
- 避免在循环中创建对象
- 复用字符串、避免创建临时对象
3. 修复内存泄漏
- 使用LeakCanary检测内存泄漏
- 修复内存泄漏,释放被占用的内存
4. 优化数据结构
- 选择合适的集合类
- 及时清理集合,避免集合泄漏
5. 优化大对象
- 避免频繁创建大对象
- 及时释放大对象
- 使用对象池管理大对象
5.2.2 GC暂停时间长问题排查
问题描述: GC暂停时间过长,导致应用卡顿
如何识别:
方法1:通过Systrace分析
- 使用Systrace记录应用运行情况
- 查看主线程的执行情况
- 如果主线程在GC期间被阻塞,说明GC暂停时间长
- 查看GC的持续时间
方法2:通过Profiler查看GC耗时
- 在Profiler中查看GC事件
- 点击GC事件,查看详细信息
- 查看GC的暂停时间(paused time)
方法3:通过logcat查看GC日志
- 查看GC日志中的paused时间
- 如果paused时间过长(比如>50ms),会导致卡顿
判断标准:
- Partial GC:暂停时间通常几毫秒,用户基本感觉不到
- Full GC:暂停时间可能几十到几百毫秒,用户能感觉到卡顿
- 严重情况:暂停时间>100ms,明显卡顿
可能的原因:
1. Full GC频繁触发
- Full GC的暂停时间比Partial GC长得多
- 如果Full GC频繁触发,会导致频繁卡顿
- 原因:内存泄漏、老年代对象太多
2. 堆内存太大,回收时间长
- 堆内存越大,GC需要处理的对象越多
- 回收时间越长,暂停时间越长
3. 老年代对象太多
- 老年代对象多,Full GC时需要处理的对象多
- 回收时间长,暂停时间长
4. 大对象太多
- 大对象占用内存多,回收慢
- 影响GC性能,导致暂停时间长
详细排查步骤:
步骤1:查看GC日志,确认GC类型
- 查看GC日志,确认是Partial GC还是Full GC
- Partial GC暂停时间短,Full GC暂停时间长
- 如果Full GC频繁,需要重点关注
步骤2:分析GC耗时
- 查看GC日志中的paused时间
- 如果paused时间过长,分析原因
- 记录GC耗时,分析趋势
步骤3:使用Systrace分析GC对主线程的影响
- 使用Systrace记录应用运行情况
- 查看主线程在GC期间的执行情况
- 查看GC是否导致主线程阻塞
- 查看GC是否导致帧率下降
步骤4:检查是否有内存泄漏
- 使用LeakCanary检测内存泄漏
- 内存泄漏会导致老年代满,频繁触发Full GC
- 修复内存泄漏可以减少Full GC
步骤5:分析老年代对象
- 使用Profiler生成堆转储
- 分析老年代中的对象
- 找出占用内存最多的对象
- 分析为什么这些对象在老年代
步骤6:优化内存使用
- 根据分析结果,优化内存使用
- 减少老年代对象
- 及时释放不需要的对象
- 修复内存泄漏
优化方案:
1. 避免触发Full GC(最重要)
- Full GC暂停时间长,应该尽量避免
- 方法:
- 避免内存泄漏(内存泄漏会导致老年代满)
- 减少老年代对象
- 不要显式调用System.gc()
2. 减少老年代对象
- 减少长期存活的对象
- 及时释放不需要的对象
- 避免大对象直接进入老年代
3. 修复内存泄漏
- 使用LeakCanary检测内存泄漏
- 修复内存泄漏,释放被占用的内存
- 减少Full GC的触发
4. 优化大对象
- 避免频繁创建大对象
- 及时释放大对象
- 使用对象池管理大对象
5. 优化GC性能
- 减少对象创建,减少GC频率
- 让Partial GC更高效
- 避免Full GC
5.2.3 内存泄漏问题排查
问题描述: 内存持续增长,GC无法回收,最终可能导致OOM
如何识别:
方法1:通过Profiler观察内存曲线
- 在Profiler中查看内存使用曲线
- 如果内存持续增长,不下降,可能有内存泄漏
- 正常情况:内存会周期性上升和下降(GC回收)
- 异常情况:内存持续上升,不下降
方法2:使用LeakCanary自动检测
- LeakCanary会自动检测内存泄漏
- 发现泄漏时会显示通知
- 这是最直接的方法
方法3:通过GC日志分析
- 查看GC日志,如果GC频繁但内存不下降
- 说明GC无法回收对象,可能有内存泄漏
判断标准:
- 正常情况:内存会周期性上升和下降
- 异常情况:内存持续增长,不下降
- 严重情况:内存持续增长,最终OOM
详细排查步骤:
步骤1:使用LeakCanary自动检测(推荐)
- 在开发阶段,LeakCanary会自动检测内存泄漏
- 如果发现泄漏,会显示通知和泄漏路径
- 根据泄漏路径,可以快速定位问题
- 优点:自动检测,快速发现,显示泄漏路径
步骤2:使用Profiler观察内存趋势
- 在Profiler中观察内存使用曲线
- 如果内存持续增长,记录内存增长的时间点
- 分析在什么操作后内存开始增长
- 找出可能导致内存泄漏的操作
步骤3:生成堆转储
- 在内存增长后,生成堆转储
- 可以生成多个堆转储,对比分析
- 导出为hprof文件
步骤4:使用MAT分析堆转储
- 用MAT打开堆转储文件
- 使用"Histogram"找出占用内存最多的类
- 使用"Dominator Tree"查看对象引用关系
- 使用"Path to GC Roots"找出引用链
步骤5:找出无法回收的对象
- 根据MAT的分析结果,找出无法回收的对象
- 分析这些对象为什么无法被回收
- 找出是哪个对象持有这些对象的引用
步骤6:分析引用链,找出泄漏原因
- 使用MAT的"Path to GC Roots"功能
- 查看对象的引用链
- 找出泄漏的原因:
- 静态变量持有?
- 监听器未取消注册?
- 集合类泄漏?
- 其他原因?
步骤7:修复代码
- 根据分析结果,修复代码
- 常见修复方法:
- 使用弱引用(WeakReference)
- 及时清理资源(onDestroy中清理)
- 取消注册监听器
- 清理集合
常见泄漏场景:
1. Activity/Context泄漏
- 静态变量持有Activity引用
- 单例持有Activity Context引用
- Handler持有Activity引用
2. 监听器未取消注册
- EventBus未取消注册
- 广播接收器未取消注册
- 其他监听器未取消注册
3. 集合类泄漏
- List、Map中保存了不再使用的对象
- 集合持有对象引用,对象无法被回收
4. 线程泄漏
- 线程持有对象引用,线程不结束,对象无法回收
- AsyncTask、HandlerThread等
5. 资源未关闭
- 文件、数据库连接、网络连接等未关闭
- 导致资源泄漏
排查工具:
- LeakCanary:自动检测,快速发现泄漏
- MAT:详细分析,找出泄漏原因
- Android Studio Profiler:实时监控,观察内存趋势
5.2.4 内存溢出(OOM)问题排查
问题描述: 应用崩溃,日志显示OutOfMemoryError
如何识别:
方法1:应用崩溃,查看日志
- 应用崩溃时,查看logcat日志
- 如果看到OutOfMemoryError,说明发生了OOM
- 查看错误信息,了解OOM的类型
方法2:通过Profiler观察内存
- 在Profiler中观察内存使用情况
- 如果内存持续增长,接近内存上限,可能发生OOM
- 在OOM前生成堆转储,分析原因
OOM的类型:
- Java heap space:堆内存溢出(最常见)
- PermGen space:方法区溢出(Android不使用)
- Direct buffer memory:直接内存溢出(较少见)
详细排查步骤:
步骤1:生成堆转储
- 在OOM时:系统可能自动生成堆转储(如果配置了)
- 在OOM前:如果发现内存持续增长,提前生成堆转储
- 手动生成:使用Profiler手动生成堆转储
- 导出为hprof文件
步骤2:使用MAT分析堆转储
- 用MAT打开hprof文件
- 等待MAT分析完成
- 查看MAT的"Leak Suspects"报告(如果有)
步骤3:找出占用内存最多的对象
- 使用MAT的"Histogram"功能
- 按"Retained Heap"排序
- 找出占用内存最多的类
- 分析这些类为什么占用这么多内存
步骤4:分析对象引用关系
- 使用MAT的"Dominator Tree"功能
- 查看对象的引用关系
- 找出哪些对象持有大量内存
步骤5:分析引用链,找出无法回收的原因
- 使用MAT的"Path to GC Roots"功能
- 查看对象的引用链
- 分析为什么对象无法被回收:
- 内存泄漏:对象被引用,无法回收
- 大对象:对象本身很大
- 对象太多:创建了太多对象
步骤6:修复问题
- 根据分析结果,修复问题:
- 如果是内存泄漏:修复泄漏(使用弱引用、及时清理)
- 如果是大对象:优化大对象(压缩、及时释放)
- 如果是对象太多:减少对象创建(使用对象池、分批加载)
常见原因和解决方案:
1. 内存泄漏(最常见)
- 原因:对象无法回收,内存逐渐被占用
- 解决方案 :
- 使用LeakCanary检测内存泄漏
- 使用MAT分析堆转储,找出泄漏对象
- 修复内存泄漏(使用弱引用、及时清理)
2. 一次性加载太多数据
- 原因:一次性加载大量数据到内存
- 解决方案 :
- 分批加载数据
- 使用分页加载
- 及时释放不需要的数据
3. 大对象占用过多内存
- 原因:加载过大的图片、创建过大的数组
- 解决方案 :
- 压缩图片大小
- 及时释放大对象
- 使用对象池管理大对象
4. 频繁创建对象
- 原因:代码中频繁创建对象
- 解决方案 :
- 使用对象池复用对象
- 避免不必要的对象创建
- 优化代码,减少对象创建
5. Android设备内存限制
- 原因:不同设备有不同的堆内存上限
- 解决方案 :
- 优化内存使用
- 避免内存泄漏
- 合理使用缓存
- 在AndroidManifest.xml中设置largeHeap="true"(治标不治本)
排查工具:
- MAT:分析堆转储,找出占用内存最多的对象
- LeakCanary:检测内存泄漏
- Android Studio Profiler:实时监控内存使用
5.2.5 应用卡顿问题排查
问题描述: 应用运行卡顿,用户体验差
如何识别:
方法1:用户反馈
- 用户反馈应用卡顿
- 观察应用运行情况,发现卡顿
方法2:通过Systrace分析
- 使用Systrace记录应用运行情况
- 查看主线程的执行情况
- 如果主线程有红色帧(掉帧),说明有卡顿
- 查看卡顿的时间段
方法3:通过Profiler观察
- 在Profiler中观察CPU使用情况
- 如果CPU使用率突然下降,可能有卡顿
- 查看GC事件是否与卡顿时间对应
判断标准:
- 正常情况:应用运行流畅,帧率稳定
- 异常情况:应用偶尔卡顿,用户能感觉到
- 严重情况:应用频繁卡顿,严重影响用户体验
卡顿的可能原因:
1. GC导致的卡顿(常见)
- GC暂停主线程,导致卡顿
- Full GC暂停时间长,更容易导致卡顿
- GC频繁触发,导致频繁卡顿
2. 主线程执行耗时操作
- 主线程执行耗时操作(比如网络请求、文件IO)
- 导致主线程阻塞,应用卡顿
3. 布局复杂
- 布局层级太深,渲染耗时
- 导致UI渲染慢,应用卡顿
4. 其他原因
- 动画性能问题
- 图片加载问题
- 其他性能问题
详细排查步骤(针对GC导致的卡顿):
步骤1:使用Systrace分析卡顿
- 使用Systrace记录应用运行情况
- 操作应用,触发卡顿
- 停止记录,查看结果
步骤2:查看主线程执行情况
- 在Systrace中找到"main"线程
- 查看主线程的执行情况
- 查看是否有红色帧(掉帧)
- 查看卡顿的时间段
步骤3:查看GC事件是否与卡顿时间对应
- 在Systrace中查找GC事件
- 查看GC事件是否与卡顿时间对应
- 如果对应,说明GC导致了卡顿
步骤4:分析GC原因
- 查看GC类型:是Partial GC还是Full GC
- 查看GC耗时:GC暂停时间是否过长
- 查看GC频率:GC是否过于频繁
- 分析GC原因:为什么触发GC
步骤5:使用Profiler进一步分析
- 使用Profiler查看内存使用情况
- 查看GC事件和内存曲线
- 分析内存使用是否正常
- 检查是否有内存泄漏
步骤6:优化代码,减少GC
- 根据分析结果,优化代码:
- 减少对象创建
- 避免触发Full GC
- 修复内存泄漏
- 优化列表滑动、动画等场景
优化方案:
1. 减少对象创建(最重要)
- 使用对象池复用对象
- 避免不必要的对象创建
- 使用基本类型替代包装类型
- 示例:列表滑动时,避免在onBindViewHolder中创建对象
2. 避免触发Full GC
- Full GC暂停时间长,应该尽量避免
- 方法:
- 避免内存泄漏
- 减少老年代对象
- 不要显式调用System.gc()
3. 优化列表滑动
- ViewHolder复用
- 避免在onBindViewHolder中创建对象
- 使用对象池
- 减少GC频率
4. 优化动画
- 复用动画对象
- 避免频繁创建动画对象
- 减少GC频率
5. 修复内存泄漏
- 使用LeakCanary检测内存泄漏
- 修复内存泄漏,减少Full GC
排查工具:
- Systrace:分析卡顿,查看GC对主线程的影响
- Android Studio Profiler:实时监控内存和GC
- logcat:查看GC日志,分析GC频率和耗时
第六部分:面试题
6.1 基础概念题(必考)
6.1.1 GC基础概念
1. 什么是垃圾回收?
完整答案:
垃圾回收(Garbage Collection,GC)是自动管理内存的机制,能够自动识别和回收不再使用的对象,释放它们占用的内存空间。
核心特点:
- 自动管理:不需要开发者手动释放内存,避免忘记释放导致的内存泄漏
- 自动识别:系统自动判断哪些对象是"垃圾"(不再使用)
- 自动回收:自动释放垃圾对象占用的内存空间
在Android中的重要性:
- Android设备内存有限,必须合理使用
- GC性能直接影响应用流畅度
- 频繁GC会导致卡顿,影响用户体验
- 内存不足可能导致应用被系统杀死
示例:
java
public void createObjects() {
Object obj1 = new Object();
Object obj2 = new Object();
// obj1和obj2不再被引用后,GC会自动回收它们占用的内存
// 开发者不需要手动释放
}
2. Android使用的是什么运行时?与JVM的区别?
完整答案:
Android使用的是ART(Android Runtime),不是JVM。
主要区别:
-
编译方式不同:
- ART:AOT编译(Ahead-Of-Time),应用安装时编译为机器码
- JVM:JIT编译(Just-In-Time),运行时编译为机器码
-
GC机制不同:
- ART:只有一种收集器(并发复制收集器),系统自动使用
- JVM:有多种收集器可选(Serial、Parallel、CMS、G1、ZGC等),开发者需要选择
-
优化方向不同:
- ART:针对低延迟优化,适合交互式应用(减少卡顿)
- JVM:有多种优化方向(吞吐量、延迟、内存占用等)
-
内存管理不同:
- ART:针对Android设备内存有限的特点优化
- JVM:通常运行在服务器上,内存相对充足
3. ART的GC与JVM的GC有什么区别?
完整答案:
ART的GC特点:
- 只有一种收集器:并发复制收集器(Concurrent Copying,CC)
- 系统自动使用:开发者不需要选择,系统自动管理
- 低延迟设计:针对交互式应用优化,减少卡顿
- 并发回收:在后台线程执行,尽量不暂停主线程
- 增量回收:把大量回收工作分成小份,避免一次性卡顿太久
JVM的GC特点:
- 多种收集器可选:Serial、Parallel、CMS、G1、ZGC、Shenandoah等
- 需要开发者选择:根据应用特点选择合适的收集器
- 多种优化方向:吞吐量优先、延迟优先、内存占用优先等
为什么ART只有一种收集器?
- 针对Android场景优化(低延迟、交互式应用)
- 不需要开发者选择,简化开发
- 系统自动管理,提高效率
4. 如何判断对象已死?
完整答案:
Android使用可达性分析算法来判断对象是否已死。
算法原理:
- 从一组称为"GC Roots"的对象开始
- 向下搜索,标记所有能够从GC Roots到达的对象(存活对象)
- 无法从GC Roots到达的对象就是"垃圾",可以被回收
为什么不用引用计数算法?
- 引用计数算法无法处理循环引用
- 两个对象相互引用时,引用计数永远不为0,导致内存泄漏
- 可达性分析算法可以处理循环引用,准确判断对象是否可回收
示例:
java
// 可达性分析可以处理循环引用
class Node {
Node next;
}
Node node1 = new Node();
Node node2 = new Node();
node1.next = node2;
node2.next = node1; // 循环引用
// 如果node1和node2都不再被GC Roots引用
// 即使它们相互引用,也无法从GC Roots到达
// 所以它们都是垃圾,可以被回收
5. GC Roots对象有哪些?
完整答案:
GC Roots是可达性分析算法的起点,以下对象可以作为GC Roots:
-
虚拟机栈中引用的对象
- 局部变量、方法参数
- 示例:方法中的局部变量引用的对象
-
方法区中静态属性引用的对象
- static变量
- 示例:类的静态变量
-
方法区中常量引用的对象
- 常量
- 示例:字符串常量
-
本地方法栈中引用的对象
- Native方法中的引用
- 示例:JNI调用中的对象
-
同步锁持有的对象
- synchronized持有的对象
- 示例:锁对象
-
内部引用
- Class对象、异常对象等
- 示例:类的Class对象
为什么这些是GC Roots?
- 这些对象是程序运行的基础,不能被回收
- 从这些对象开始,可以找到所有正在使用的对象
- 无法从GC Roots到达的对象,说明程序不再使用,可以回收
6. 引用类型有哪些?
完整答案:
Java中有四种引用类型,强度从强到弱:
1. 强引用(Strong Reference)
- 特点:最常见的引用类型,只要强引用存在,对象就不会被GC回收
- 使用场景:普通对象引用、Activity引用等
- 示例:
java
Object obj = new Object(); // 强引用
// obj引用的对象不会被回收,除非obj = null
2. 软引用(Soft Reference)
- 特点:内存不足时才回收,适合缓存场景
- Android使用:Android不推荐使用,因为设备内存有限,软引用可能很快被回收
- 建议:使用LruCache等有大小限制的缓存
- 示例:
java
SoftReference<Bitmap> softRef = new SoftReference<>(bitmap);
Bitmap bitmap = softRef.get(); // 可能返回null(如果被回收了)
3. 弱引用(Weak Reference)
- 特点:只要GC就会回收,不会阻止对象被回收
- Android使用场景:避免内存泄漏(Handler、静态变量持有对象时)
- 示例:
java
// 避免Handler内存泄漏
private static class MyHandler extends Handler {
private WeakReference<Activity> activityRef;
MyHandler(Activity activity) {
activityRef = new WeakReference<>(activity);
}
}
4. 虚引用(Phantom Reference)
- 特点:最弱的引用类型,主要用于对象回收前的清理工作
- Android使用场景:很少使用,主要用于特殊场景
- 特点:无法通过虚引用获取对象
引用强度对比: 强引用 > 软引用 > 弱引用 > 虚引用
6.1.2 垃圾回收算法
1. 垃圾回收算法有哪些?
完整答案:
垃圾回收算法主要有四种:
1. 标记-清除算法(Mark-Sweep)
- 原理:先标记所有需要回收的对象,然后清除
- 特点:会产生内存碎片
- Android使用:不主要使用
2. 标记-复制算法(Mark-Copy)
- 原理:将内存分为两块,回收时将存活对象复制到另一块
- 特点:没有内存碎片,但浪费内存
- Android使用 :主要使用这个算法,适合年轻代
3. 标记-整理算法(Mark-Compact)
- 原理:标记后,将存活对象向一端移动,然后清理边界外的内存
- 特点:没有内存碎片且不浪费内存,但效率较低
- Android使用:在某些场景使用,用于老年代
4. 分代收集算法(Generational Collection)
- 原理:根据对象生命周期分为年轻代和老年代,不同代用不同算法
- 特点:结合多种算法的优点
- Android使用:使用这个策略
为什么Android主要用标记-复制算法?
- 适合年轻代:大部分对象很快被回收,存活对象少,复制成本低
- 没有内存碎片:复制后内存连续,分配效率高
- 通过优化(Appel式回收),只浪费10%的内存,而不是50%
2. 标记-清除算法的优缺点?
完整答案:
算法原理: 分两个阶段:
- 标记阶段:从GC Roots开始,标记所有需要回收的对象
- 清除阶段:清除所有被标记的对象
优点:
- 实现简单
- 不需要移动对象
缺点:
- 产生内存碎片:清除后留下不连续的内存空间,影响后续内存分配效率
- 效率较低:需要两次遍历(标记和清除)
Android中的使用:
- Android不主要使用这个算法
- 因为会产生内存碎片,影响后续内存分配效率
示例:
css
标记前:[对象1][对象2][对象3][对象4][对象5]
存活 垃圾 存活 垃圾 存活
清除后:[对象1][空闲][对象3][空闲][对象5]
碎片多,分配效率低
3. 标记-复制算法的优缺点?
完整答案:
算法原理: 将内存分为两块,每次只使用一块。回收时,将存活对象复制到另一块,然后清空当前块。
优点:
- 没有内存碎片:复制后内存连续,分配效率高
- 回收效率高:只需要复制存活对象,大部分对象很快被回收,复制成本低
- 适合年轻代:大部分对象很快被回收,存活对象少
缺点:
- 浪费内存:需要两块内存,但只使用一块
- 对象存活率高时效率低:需要复制很多对象
Android中的使用:
- Android ART主要使用这个算法
- 适合年轻代回收(大部分对象很快被回收)
- 通过优化(Appel式回收),只浪费10%的内存(而不是50%)
改进算法(Appel式回收):
- 将内存分为Eden区和两个Survivor区
- 默认比例:Eden : Survivor0 : Survivor1 = 8 : 1 : 1
- 只浪费10%的内存,而不是50%
4. 标记-整理算法的优缺点?
完整答案:
算法原理: 分三个阶段:
- 标记阶段:标记所有需要回收的对象
- 整理阶段:将存活对象向一端移动
- 清除阶段:清理边界外的内存
优点:
- 没有内存碎片:整理后内存连续
- 不浪费内存:不需要两块内存
缺点:
- 效率较低:需要移动对象,更新所有引用
- 适合老年代:对象存活率高,移动成本相对较低
Android中的使用:
- Android在某些场景下使用
- 主要用于老年代回收(避免内存碎片)
为什么老年代用标记-整理?
- 老年代对象存活率高,移动成本相对较低
- 避免内存碎片,提高内存利用率
- 不浪费内存(不像标记-复制需要两块内存)
5. 分代收集算法的原理?
完整答案:
理论基础: 根据对象生命周期不同,将内存分为不同的代:
- 大部分对象生命周期很短:创建后很快被回收
- 少数对象生命周期很长:长期存活
分代策略:
年轻代(Young Generation):
- 特点:存放新创建的对象,生命周期短
- 回收算法:标记-复制算法(快速高效)
- 回收频率:高(频繁回收)
- 回收类型:Partial GC(部分回收)
- 回收时间:短(几毫秒)
老年代(Old Generation):
- 特点:存放长期存活的对象,生命周期长
- 回收算法:标记-整理算法(避免碎片)
- 回收频率:低(偶尔回收)
- 回收类型:Full GC(完全回收)
- 回收时间:长(可能几十到几百毫秒)
Android中的分代收集:
- 年轻代用标记-复制算法:快速回收,适合频繁回收
- 老年代用标记-整理算法:避免内存碎片
- 这样设计的好处:提高GC效率,减少GC暂停时间,减少内存碎片
对象晋升过程:
markdown
1. 新对象 → 年轻代(Eden区)
2. 年轻代GC → 存活对象 → Survivor区
3. 多次GC后仍存活 → 晋升到老年代
4. 老年代GC → 回收长期存活的对象
6.1.3 Android GC基础
1. Android使用的垃圾回收器是什么?
完整答案:
Android使用的垃圾回收器是并发复制收集器(Concurrent Copying,简称CC)。
特点:
- 只有一个收集器:Android只有这一种收集器
- 系统自动使用:开发者不需要选择,系统自动使用这个收集器
- 没有多种可选:不像JVM有Serial、Parallel、CMS、G1、ZGC等多种可选
为什么只有一个?
- 针对Android场景优化:低延迟、交互式应用
- 不需要开发者选择:系统自动管理
- 简化开发:开发者不需要了解多种收集器的区别
收集器的特性:
- 分代回收:年轻代和老年代
- 并发回收:在后台线程执行,减少主线程暂停
- 复制算法:适合年轻代,没有内存碎片
- 增量回收:把大量回收工作分成小份,避免一次性卡顿太久
- 压缩回收:回收后整理内存,避免内存碎片
2. Android有几种垃圾回收器可选?
完整答案:
Android没有多种收集器可选,只有一种。
- 只有一种:并发复制收集器(Concurrent Copying,CC)
- 系统自动使用:开发者不需要选择,系统自动使用
- 不需要配置:不像JVM需要配置-XX:+UseXXX参数
与JVM的区别:
- JVM:有多种收集器可选(Serial、Parallel、CMS、G1、ZGC、Shenandoah等),需要开发者根据应用特点选择
- Android:只有一种,系统自动使用,针对Android场景优化
为什么不需要多种可选?
- Android应用的特点相似:都是交互式应用,需要低延迟
- 系统自动优化:针对Android场景已经优化好了
- 简化开发:开发者不需要了解多种收集器的区别和选择
3. Partial GC和Full GC的区别?
完整答案:
Partial GC(部分回收):
- 回收范围:只回收年轻代
- 触发条件:年轻代空间满了
- 回收算法:标记-复制算法
- 回收时间:短(几毫秒)
- 对应用影响:小,用户基本感觉不到
- 频率:高(频繁触发)
Full GC(完全回收):
- 回收范围:回收整个堆(年轻代+老年代)
- 触发条件:堆内存不足、老年代满了、显式调用System.gc()
- 回收算法:标记-整理算法(老年代)
- 回收时间:长(可能几十到几百毫秒)
- 对应用影响:大,可能导致应用卡顿,用户能感觉到
- 频率:低(偶尔触发)
为什么需要区分Partial GC和Full GC?
- 大部分对象在年轻代,Partial GC快速高效
- Full GC回收整个堆,耗时长,应该尽量避免
- 区分后可以优化GC策略,减少Full GC的频率
4. 什么时候会触发Partial GC?
完整答案:
触发条件:年轻代空间满了
具体场景:
- 创建了很多新对象,年轻代内存快用完了
- 分配新对象时,年轻代空间不够
- 系统检测到年轻代空间不足,自动触发Partial GC
触发流程:
markdown
1. 新对象分配在年轻代
2. 年轻代空间逐渐被占用
3. 年轻代快满时,系统检测到空间不足
4. 自动触发Partial GC
5. 只清理年轻代,不清理老年代
特点:
- 触发快,执行快
- 暂停时间短(几毫秒)
- 对应用影响小,用户基本感觉不到
示例:
java
public void createManyObjects() {
List<Object> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
list.add(new Object()); // 创建大量对象
// 当年轻代快满时,触发Partial GC
}
}
5. 什么时候会触发Full GC?
完整答案:
触发条件:整个堆内存不足
具体场景:
-
老年代空间满了
- 长期存活的对象占满了老年代
- 无法继续分配新对象
-
堆内存整体不足
- 年轻代和老年代都满了
- 无法分配新对象
-
显式调用System.gc()
- Android不推荐使用
- 会强制触发Full GC,可能导致卡顿
触发流程:
markdown
1. 堆内存不足,无法分配新对象
2. 系统检测到内存不足
3. 触发Full GC
4. 回收整个堆(年轻代+老年代)
5. 如果GC后还是不够,可能抛出OutOfMemoryError
特点:
- 执行慢,暂停时间长(可能几十到几百毫秒)
- 会清理整个堆(年轻代+老年代)
- 可能导致应用卡顿,用户能感觉到
如何避免Full GC?
- 避免内存泄漏
- 减少老年代对象
- 不要显式调用System.gc()
- 优化内存使用
6. Sticky GC是什么?适用场景?
完整答案:
**Sticky GC(粘性回收)**是一种快速清理新分配的短生命周期对象的GC类型。
特点:
- 只清理新对象:只清理刚刚创建的新对象
- 非常快速:几乎感觉不到
- 暂停时间极短:几毫秒甚至更短
- 专门优化频繁分配对象的场景
适用场景:
-
列表快速滑动时
- RecyclerView滚动时频繁创建ViewHolder、临时对象
- Sticky GC快速清理这些短生命周期对象
-
动画播放时
- 动画过程中不断创建临时对象
- Sticky GC快速清理
-
UI渲染时
- UI渲染时频繁创建View相关对象
- Sticky GC快速清理
工作原理:
markdown
1. 应用运行期间频繁创建对象
2. 系统检测到频繁分配
3. 触发Sticky GC
4. 只清理刚刚创建的新对象(短生命周期)
5. 快速完成,几乎不影响应用
与其他GC类型的区别:
- Partial GC:清理整个年轻代,暂停时间几毫秒
- Full GC:清理整个堆,暂停时间几十到几百毫秒
- Sticky GC:只清理新对象,暂停时间极短,几乎感觉不到
示例:
java
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
// 滑动时频繁创建对象
// 触发Sticky GC,快速清理
// 几乎感觉不到卡顿
}
});
6.2 Android GC实践题(重点)
6.2.1 GC优化
1. Android GC优化的目标是什么?
完整答案:
Android GC优化的主要目标有四个:
1. 降低GC暂停时间(最重要)
- GC暂停会导致应用卡顿,影响用户体验
- 目标:尽量减少GC暂停时间,特别是Full GC
- 方法:避免触发Full GC,减少对象创建
2. 减少内存占用
- Android设备内存有限,必须合理使用
- 目标:减少内存占用,降低内存压力
- 方法:及时释放不需要的对象,避免内存泄漏
3. 提升应用流畅度
- GC会影响应用性能,导致卡顿
- 目标:提升应用流畅度,优化用户体验
- 方法:减少GC频率,优化GC性能
4. 避免ANR(Application Not Responding)
- 长时间GC可能导致ANR
- 目标:避免ANR,保证应用响应
- 方法:减少Full GC,优化GC性能
优化优先级: 降低GC暂停时间 > 减少内存占用 > 提升流畅度 > 避免ANR
2. 如何减少GC频率?
完整答案:
减少GC频率的核心是减少对象分配。
1. 对象复用(使用对象池)
- 复用对象,避免频繁创建销毁
- 示例:RecyclerView的ViewHolder池、自定义对象池
java
// 不好的做法:频繁创建对象
for (int i = 0; i < 1000; i++) {
String str = new String("Hello"); // 每次都创建新对象
}
// 好的做法:复用对象
String str = "Hello";
for (int i = 0; i < 1000; i++) {
// 复用同一个对象
}
2. 避免不必要的对象创建
- 使用StringBuilder而不是字符串拼接
- 避免在循环中创建对象
- 示例:字符串拼接、临时对象
java
// 不好的做法:字符串拼接
String result = "";
for (int i = 0; i < 100; i++) {
result += i; // 每次都创建新String对象
}
// 好的做法:使用StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100; i++) {
sb.append(i); // 复用StringBuilder
}
3. 使用基本类型替代包装类型
- 基本类型不创建对象,包装类型会创建对象
- 示例:int而不是Integer,boolean而不是Boolean
java
// 不好的做法:使用包装类型
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
list.add(new Integer(i)); // 创建Integer对象
}
// 好的做法:使用基本类型数组
int[] array = new int[1000];
for (int i = 0; i < 1000; i++) {
array[i] = i; // 不创建对象
}
4. 优化数据结构
- 选择合适的集合类
- 及时清理集合,避免集合泄漏
3. 如何减少GC停顿时间?
完整答案:
减少GC停顿时间的核心是避免Full GC。
1. 避免触发Full GC
- Full GC暂停时间长(几十到几百毫秒),会导致明显卡顿
- 方法:
- 避免内存泄漏(内存泄漏会导致老年代满,触发Full GC)
- 减少老年代对象(减少Full GC的触发)
- 不要显式调用System.gc()(会强制触发Full GC)
2. 减少对象创建
- 减少对象创建可以减少GC频率
- 方法:对象复用、使用对象池、避免不必要的对象创建
3. 修复内存泄漏
- 内存泄漏会导致内存逐渐被占用,最终触发Full GC
- 方法:使用LeakCanary检测,修复内存泄漏
4. 优化GC性能
- 让Partial GC更高效
- 方法:减少年轻代对象,让Partial GC快速完成
示例:
java
// 不好的做法:可能导致Full GC
private static List<Object> cache = new ArrayList<>(); // 内存泄漏
// 好的做法:避免Full GC
private LruCache<String, Bitmap> cache = new LruCache<>(10 * 1024 * 1024); // 有大小限制
4. 如何优化列表滑动性能?
完整答案:
列表滑动时频繁创建对象,容易触发GC,导致卡顿。
1. ViewHolder复用
- RecyclerView的ViewHolder会自动复用,避免频繁创建
- 确保ViewHolder正确复用,不要每次都创建新的
java
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// ViewHolder复用,避免频繁创建
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item, parent, false);
return new ViewHolder(view);
}
}
2. 避免在onBindViewHolder中创建对象
- onBindViewHolder在滑动时频繁调用,不要在这里创建对象
- 复用字符串、避免创建临时对象
java
// 不好的做法
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.textView.setText(new String("Item " + position)); // 创建新对象
}
// 好的做法
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.textView.setText("Item " + position); // 复用字符串
}
3. 使用对象池
- 对于需要频繁创建的对象,使用对象池
- 示例:自定义对象池管理对象复用
4. 减少GC
- 通过以上方法减少对象创建,减少GC频率
- 让列表滑动更流畅
5. 如何使用对象池减少对象分配?
完整答案:
对象池的核心思想是复用对象,避免频繁创建和销毁。
原理:
- 预先创建一些对象,放在池中
- 需要时从池中获取,使用完后归还到池中
- 避免频繁创建和销毁对象
实现方式:
1. RecyclerView的ViewHolder池(系统自带)
- RecyclerView自动管理ViewHolder池
- 开发者只需要正确实现Adapter
2. 自定义对象池
java
public class ObjectPool<T> {
private Queue<T> pool = new LinkedList<>();
private int maxSize;
public ObjectPool(int maxSize) {
this.maxSize = maxSize;
}
public T acquire() {
T obj = pool.poll();
if (obj == null) {
obj = createObject(); // 池为空时创建新对象
}
return obj;
}
public void release(T obj) {
if (pool.size() < maxSize) {
resetObject(obj); // 重置对象状态
pool.offer(obj); // 归还到池中
}
}
}
使用场景:
- 频繁创建的对象(如动画对象、临时对象)
- 创建成本高的对象(如Bitmap、大对象)
优点:
- 减少对象创建,减少GC频率
- 提高性能,减少内存分配开销
6. 如何避免Full GC?
完整答案:
Full GC暂停时间长,应该尽量避免。
1. 避免内存泄漏
- 内存泄漏会导致对象无法回收,内存逐渐被占用
- 最终导致老年代满,触发Full GC
- 方法:使用LeakCanary检测,修复内存泄漏
2. 减少老年代对象
- 老年代对象多,容易触发Full GC
- 方法:
- 减少长期存活的对象
- 及时释放不需要的对象
- 避免大对象直接进入老年代
3. 不要显式调用System.gc()
- System.gc()会强制触发Full GC
- Android不推荐使用
- 应该让系统自动管理GC
4. 优化内存使用
- 减少对象创建
- 及时释放不需要的对象
- 合理使用缓存(LruCache)
5. 监控和调优
- 使用Profiler监控GC
- 查看GC日志,分析Full GC的原因
- 根据实际情况优化
示例:
java
// 不好的做法:可能导致Full GC
private static List<Object> cache = new ArrayList<>(); // 无限制缓存,内存泄漏
// 好的做法:避免Full GC
private LruCache<String, Bitmap> cache = new LruCache<>(10 * 1024 * 1024); // 有大小限制
6.2.2 内存泄漏
1. 什么是内存泄漏?
完整答案:
定义: 内存泄漏是指对象已经不再使用,但因为被引用无法被GC回收,导致内存逐渐被占用。
与内存溢出的区别:
- 内存泄漏:对象无法回收,内存逐渐被占用,最终可能导致OOM
- 内存溢出:内存真的不够用了,无法分配新对象,立即抛出OutOfMemoryError
危害:
- 内存逐渐被占用,最终可能导致OOM
- 应用变慢,可能被系统杀死
- 频繁触发Full GC,导致卡顿
示例:
java
// 内存泄漏示例
public class MemoryLeak {
private static List<Object> cache = new ArrayList<>();
public void addToCache(Object obj) {
cache.add(obj); // 对象被cache引用,无法回收
// 即使obj不再使用,也无法被GC回收
// 内存逐渐被占用,最终可能导致OOM
}
}
2. Android中常见的内存泄漏场景有哪些?
完整答案:
1. Activity/Context泄漏
-
静态变量持有Activity引用
javaprivate static Activity activity; // 静态变量持有Activity,导致泄漏 -
单例持有Context引用
java// 单例持有Activity Context,导致泄漏 private Context context; // 如果是Activity Context,会泄漏 -
内部类持有外部类引用
java// Handler持有Activity引用 private Handler handler = new Handler() { // Handler持有Activity引用,导致泄漏 };
2. 内部类持有外部类引用
- Handler内存泄漏:Handler持有Activity引用
- 匿名类泄漏:匿名内部类持有外部类引用
- 示例:Handler、Runnable、监听器等
3. 监听器未取消注册
- EventBus未取消注册:注册了但忘记取消注册
- 广播接收器未取消注册:注册了但忘记取消注册
- 其他监听器:注册了但忘记取消注册
4. 集合类泄漏
- List、Map中保存了不再使用的对象
- 集合持有对象引用,对象无法被回收
- 示例:缓存、集合等
5. 线程泄漏
- 线程持有对象引用,线程不结束,对象无法回收
- 示例:AsyncTask、HandlerThread等
6. 资源未关闭
- 文件、数据库连接、网络连接等未关闭
- 导致资源泄漏
3. 如何检测内存泄漏?
完整答案:
1. LeakCanary自动检测(推荐)
- 原理:使用WeakReference和ReferenceQueue,监控对象是否被回收
- 优点:自动检测,不需要手动分析,能快速发现内存泄漏
- 使用方法:添加依赖,运行应用,自动检测
2. MAT分析堆转储
- 原理:分析堆转储文件,找出无法回收的对象和引用链
- 优点:能详细分析内存泄漏的原因
- 使用方法:生成堆转储,导出hprof文件,用MAT打开分析
3. Android Studio Profiler
- 原理:实时监控内存使用,查看对象分配情况
- 优点:实时监控,方便查看内存变化
- 使用方法:打开Profiler窗口,查看内存使用情况
检测流程:
markdown
1. 使用LeakCanary自动检测(开发阶段)
2. 如果发现泄漏,使用MAT分析堆转储
3. 找出泄漏对象和引用链
4. 修复代码
4. 如何避免内存泄漏?
完整答案:
1. 正确处理生命周期
- 在onDestroy中清理资源
- 取消注册监听器
- 关闭文件、数据库连接
java
@Override
protected void onDestroy() {
super.onDestroy();
// 清理资源
handler.removeCallbacksAndMessages(null);
EventBus.getDefault().unregister(this);
cache.clear();
}
2. 及时取消注册监听器
- 注册的监听器要在onDestroy中取消注册
- 示例:EventBus、广播接收器等
3. 使用弱引用(WeakReference)
- 对于可能泄漏的引用,使用WeakReference
- 示例:Handler、静态变量持有对象时
java
private static class MyHandler extends Handler {
private WeakReference<Activity> activityRef;
MyHandler(Activity activity) {
activityRef = new WeakReference<>(activity);
}
}
4. 避免循环引用
- 注意对象之间的相互引用
- 及时断开不需要的引用
5. 合理使用缓存
- 使用有大小限制的缓存(LruCache)
- 及时清理不需要的缓存
java
private LruCache<String, Bitmap> cache = new LruCache<>(10 * 1024 * 1024);
6.2.3 内存溢出(OOM)
1. 什么是内存溢出(OOM)?
完整答案:
定义: 内存溢出(OutOfMemoryError,OOM)是指应用需要的内存超过了系统分配的限制,无法分配新对象。
Android堆内存溢出:
- 最常见的是堆内存溢出(OutOfMemoryError: Java heap space)
- 堆内存不足,无法分配新对象
危害:
- 应用崩溃,用户体验差
- 可能导致数据丢失
示例:
java
public class OOMExample {
public void createTooManyObjects() {
List<Object> list = new ArrayList<>();
while (true) {
list.add(new Object()); // 最终导致OOM
}
}
}
2. Android堆内存溢出如何解决?
完整答案:
1. 排查内存泄漏
- 内存泄漏是导致OOM的主要原因
- 使用LeakCanary检测内存泄漏
- 使用MAT分析堆转储,找出泄漏对象
- 修复内存泄漏问题
2. 减少对象创建
- 减少不必要的对象创建
- 使用对象池复用对象
- 避免在循环中创建大量对象
3. 优化大对象
- 及时释放大对象(大图片、大数组)
- 压缩图片大小
- 避免加载过大的图片
4. 及时释放资源
- 在onDestroy中释放资源
- 关闭文件、数据库连接
- 清理缓存
5. 分批加载数据
- 不要一次性加载大量数据
- 分批加载,减少内存压力
6. 增加内存限制(治标不治本)
- 在AndroidManifest.xml中设置largeHeap="true"
- 但这不是根本解决方案,还是要优化内存使用
解决流程:
markdown
1. 生成堆转储(OOM时自动生成或手动生成)
2. 使用MAT分析堆转储
3. 找出占用内存最多的对象
4. 分析为什么这些对象无法回收(内存泄漏?)
5. 修复问题(修复内存泄漏、优化内存使用)
3. 如何排查内存溢出问题?
完整答案:
排查步骤:
1. 生成堆转储
- 在OOM时自动生成堆转储
- 或使用Android Studio Profiler手动生成
- 导出为hprof文件
2. 使用MAT分析堆转储
- 用MAT打开hprof文件
- 查看占用内存最多的对象
- 分析对象的引用链
3. 找出占用内存最多的对象
- 使用MAT的"Histogram"功能
- 找出占用内存最多的类
- 分析这些对象为什么占用这么多内存
4. 分析引用链,找出无法回收的原因
- 使用MAT的"Path to GC Roots"功能
- 找出对象的引用链
- 分析为什么对象无法被回收(内存泄漏?)
5. 修复问题
- 如果是内存泄漏,修复泄漏
- 如果是大对象,优化大对象
- 如果是对象太多,减少对象创建
常见原因:
- 内存泄漏:对象无法回收,内存逐渐被占用
- 一次性加载太多数据:一次性加载大量数据到内存
- 大对象占用过多内存:加载过大的图片、创建过大的数组
4. Android内存限制是多少?
完整答案:
不同设备有不同的堆内存上限:
低端设备:
- 可能只有几十MB(如32MB、48MB、64MB)
- 内存非常有限,必须合理使用
中端设备:
- 通常有128MB、192MB、256MB等
- 内存相对充足,但仍需合理使用
高端设备:
- 可能有512MB、1GB、2GB甚至更多
- 内存相对充足,但仍需优化
如何查看应用的内存限制:
java
ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
activityManager.getMemoryInfo(memoryInfo);
long maxMemory = memoryInfo.totalMem; // 总内存
long availableMemory = memoryInfo.availMem; // 可用内存
内存限制的影响:
- 内存有限,必须合理使用
- GC更频繁(内存不足时)
- 需要优化内存使用,避免OOM
6.2.4 GC检测与诊断
1. 常用的Android内存监控工具有哪些?
完整答案:
1. Android Studio Profiler(最常用)
- 功能:实时监控内存使用、查看GC事件、生成堆转储
- 优点:集成在Android Studio中,使用方便
- 适用场景:日常开发、性能分析
2. logcat(查看GC日志)
- 功能:查看GC的日志信息
- 优点:简单直接,可以实时查看
- 适用场景:查看GC频率、GC耗时
3. Systrace(分析GC对性能的影响)
- 功能:分析GC导致的卡顿问题
- 优点:能详细分析GC对主线程的影响
- 适用场景:分析卡顿问题
4. MAT(Memory Analyzer Tool)
- 功能:分析堆转储文件,找出内存问题
- 优点:功能强大,能详细分析内存泄漏
- 适用场景:分析内存泄漏、OOM问题
5. LeakCanary(自动检测内存泄漏)
- 功能:自动检测应用中的内存泄漏
- 优点:自动检测,不需要手动分析
- 适用场景:开发阶段自动检测内存泄漏
2. 如何使用Android Studio Profiler?
完整答案:
使用步骤:
1. 打开Profiler窗口
- 在Android Studio中,点击底部的"Profiler"标签
- 或通过菜单:View → Tool Windows → Profiler
2. 选择要分析的应用进程
- 在Profiler窗口中选择要分析的应用进程
- 确保应用正在运行
3. 查看内存使用情况
- 点击"Memory"标签
- 查看内存使用曲线
- 查看GC事件(什么时候触发GC)
4. 查看GC事件
- 在内存曲线中,GC事件会显示为小图标
- 点击GC事件,可以查看详细信息(GC类型、耗时等)
5. 生成堆转储
- 点击"Heap Dump"按钮
- 生成堆转储文件
- 可以导出为hprof文件,用MAT分析
6. 查看对象分配情况
- 使用"Allocation Tracking"功能
- 查看哪些对象被分配,在哪里分配
3. 如何查看Android GC日志?
完整答案:
方法1:使用adb logcat
bash
adb logcat | grep GC
- 使用命令行工具查看GC日志
- 可以过滤GC相关的日志
方法2:在Android Studio的Logcat窗口中查看
- 打开Logcat窗口
- 过滤GC相关的日志
- 查看GC类型和耗时
GC日志包含什么:
- GC类型:Partial GC、Full GC、Sticky GC
- GC耗时:GC执行的时间
- 回收前后的内存大小:回收了多少内存
- GC频率:多久触发一次GC
如何分析GC日志:
- 查看GC频率:是否频繁触发GC
- 查看GC耗时:是否暂停时间过长
- 查看GC类型:是Partial GC还是Full GC
- 如果Full GC频繁,需要优化
4. 如何使用LeakCanary检测内存泄漏?
完整答案:
使用步骤:
1. 添加依赖
gradle
dependencies {
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12'
}
2. 运行应用
- LeakCanary会自动集成到应用中
- 不需要额外配置
3. 自动检测
- LeakCanary会监控Activity、Fragment等对象的生命周期
- 当对象应该被回收时,LeakCanary会检查对象是否真的被回收
4. 发现泄漏时显示通知
- 如果发现内存泄漏,LeakCanary会显示通知
- 点击通知,可以查看泄漏路径
- 泄漏路径会显示是哪个对象持有泄漏对象的引用
检测原理:
- 使用WeakReference和ReferenceQueue
- 当对象被GC回收时,WeakReference会被放入ReferenceQueue
- 如果对象应该被回收但没有进入ReferenceQueue,说明有内存泄漏
5. 如何使用MAT分析内存?
完整答案:
使用步骤:
1. 生成堆转储
- 在Android Studio Profiler中生成堆转储
- 或使用jmap命令生成(Android不常用)
2. 导出hprof文件
- 在Profiler中,点击"Export heap dump"
- 导出为hprof文件
3. 用MAT打开
- 下载并安装MAT(Memory Analyzer Tool)
- 用MAT打开hprof文件
4. 分析大对象
- 使用MAT的"Histogram"功能
- 查看占用内存最多的类
- 找出占用内存最多的对象
5. 分析引用链
- 使用MAT的"Path to GC Roots"功能
- 找出对象的引用链
- 分析为什么对象无法被回收(内存泄漏?)
MAT的主要功能:
- Histogram:查看占用内存最多的类
- Dominator Tree:查看对象引用关系
- Path to GC Roots:查看对象的引用链
- Leak Suspects:自动检测可能的内存泄漏
6. 如何排查GC频繁问题?
完整答案:
排查步骤:
1. 查看GC日志确认频率
- 使用logcat查看GC日志
- 统计GC频率(多久触发一次GC)
- 如果GC过于频繁(比如每秒多次),说明有问题
2. 使用Profiler查看对象分配情况
- 使用Profiler的"Allocation Tracking"功能
- 查看哪些对象被频繁分配
- 找出频繁创建对象的地方
3. 找出频繁创建对象的地方
- 分析代码,找出频繁创建对象的地方
- 常见场景:循环中创建对象、列表滑动时创建对象
4. 优化代码,减少对象创建
- 使用对象池复用对象
- 避免不必要的对象创建
- 使用基本类型替代包装类型
可能的原因:
- 频繁创建对象:代码中频繁创建对象
- 内存泄漏:内存泄漏导致内存不足,频繁触发GC
- 年轻代设置太小:年轻代太小,容易满,频繁触发GC
优化方案:
- 使用对象池复用对象
- 减少不必要的对象创建
- 修复内存泄漏
7. 如何排查应用卡顿问题?
完整答案:
排查步骤:
1. 使用Systrace分析卡顿
- 使用Android Studio的Systrace工具
- 或使用命令行工具
- 分析卡顿的时间段
2. 查看GC事件是否与卡顿时间对应
- 在Systrace中查看GC事件
- 检查GC事件是否与卡顿时间对应
- 如果对应,说明GC导致了卡顿
3. 如果是GC导致的,分析GC原因
- 查看GC类型:是Partial GC还是Full GC
- 查看GC耗时:GC暂停时间是否过长
- 分析GC原因:为什么触发GC
4. 优化代码,减少GC
- 减少对象创建
- 避免触发Full GC
- 优化列表滑动、动画等场景
优化方案:
- 减少对象创建:使用对象池、避免不必要的对象创建
- 避免触发Full GC:避免内存泄漏、减少老年代对象
- 优化列表滑动:ViewHolder复用、避免在onBindViewHolder中创建对象
- 优化动画:复用动画对象、避免频繁创建对象
6.3 综合应用题(高级)
6.3.1 场景分析题
1. 如何优化Android应用的GC性能?
完整答案:
优化GC性能需要从多个方面入手:
1. 减少对象分配(最重要)
- 对象复用:使用对象池,避免频繁创建销毁
- 避免不必要的对象创建:使用StringBuilder、避免在循环中创建对象
- 使用基本类型:使用int而不是Integer,boolean而不是Boolean
2. 避免内存泄漏
- 正确处理生命周期:在onDestroy中清理资源
- 及时取消注册:取消注册监听器(EventBus、广播接收器)
- 使用弱引用:对于可能泄漏的引用,使用WeakReference
3. 优化列表和动画
- ViewHolder复用:确保RecyclerView的ViewHolder正确复用
- 避免频繁创建对象:避免在onBindViewHolder中创建对象
- 复用动画对象:不要每次创建新的动画对象
4. 合理使用缓存
- 使用LruCache:使用有大小限制的缓存
- 及时清理:及时清理不需要的缓存
- 避免无限制缓存:不要无限制地缓存数据
5. 优化大对象
- 及时释放:及时释放大对象(大图片、大数组)
- 压缩图片:加载时压缩图片大小
- 分批加载:不要一次性加载大量数据
优化效果:
- 减少GC频率:减少对象创建,减少GC触发
- 降低GC暂停时间:避免Full GC,减少GC暂停时间
- 提升应用流畅度:减少GC,提升用户体验
2. 如何排查Android内存泄漏?
完整答案:
排查流程:
1. 使用LeakCanary自动检测(第一步)
- 在开发阶段,LeakCanary会自动检测内存泄漏
- 如果发现泄漏,会显示通知和泄漏路径
- 根据泄漏路径,可以快速定位问题
2. 生成堆转储用MAT分析(详细分析)
- 如果LeakCanary发现泄漏,或需要详细分析
- 在Android Studio Profiler中生成堆转储
- 导出为hprof文件,用MAT打开分析
3. 找出泄漏对象和引用链
- 使用MAT的"Path to GC Roots"功能
- 找出泄漏对象的引用链
- 分析是哪个对象持有泄漏对象的引用
4. 修复代码
- 根据分析结果,修复代码
- 常见修复方法:
- 使用弱引用(WeakReference)
- 及时清理资源(onDestroy中清理)
- 取消注册监听器
- 清理集合
排查工具:
- LeakCanary:自动检测,快速发现泄漏
- MAT:详细分析,找出泄漏原因
- Android Studio Profiler:实时监控,查看内存变化
3. 如何排查Android OOM问题?
完整答案:
排查流程:
1. 生成堆转储
- 在OOM时,系统可能自动生成堆转储
- 或使用Android Studio Profiler手动生成堆转储
- 导出为hprof文件
2. 分析占用内存最多的对象
- 使用MAT打开hprof文件
- 使用MAT的"Histogram"功能
- 查看占用内存最多的类
- 找出占用内存最多的对象
3. 找出无法回收的原因
- 使用MAT的"Path to GC Roots"功能
- 分析对象的引用链
- 找出为什么对象无法被回收:
- 内存泄漏:对象被引用,无法回收
- 大对象:对象本身很大
- 对象太多:创建了太多对象
4. 优化内存使用
- 根据分析结果,优化内存使用:
- 如果是内存泄漏,修复泄漏
- 如果是大对象,优化大对象(压缩、及时释放)
- 如果是对象太多,减少对象创建
常见原因和解决方案:
- 内存泄漏:使用LeakCanary检测,修复泄漏
- 一次性加载太多数据:分批加载数据
- 大对象占用过多内存:压缩图片、及时释放大对象
4. 如何排查GC频繁导致的卡顿问题?
完整答案:
排查流程:
1. 查看GC日志
- 使用logcat查看GC日志
- 统计GC频率(多久触发一次GC)
- 如果GC过于频繁(比如每秒多次),说明有问题
2. 使用Systrace分析
- 使用Systrace分析卡顿
- 查看GC事件是否与卡顿时间对应
- 如果对应,说明GC导致了卡顿
3. 找出频繁创建对象的地方
- 使用Profiler的"Allocation Tracking"功能
- 查看哪些对象被频繁分配
- 分析代码,找出频繁创建对象的地方
4. 优化代码减少GC
- 根据分析结果,优化代码:
- 使用对象池复用对象
- 避免不必要的对象创建
- 使用基本类型替代包装类型
- 修复内存泄漏
优化方案:
- 减少对象创建:使用对象池、避免不必要的对象创建
- 避免触发Full GC:避免内存泄漏、减少老年代对象
- 优化列表滑动:ViewHolder复用、避免在onBindViewHolder中创建对象
5. 如何排查列表滑动卡顿问题?
完整答案:
排查流程:
1. 检查ViewHolder复用
- 确保RecyclerView的ViewHolder正确复用
- 检查Adapter的实现是否正确
- 确保ViewHolder不会被频繁创建
2. 避免在onBindViewHolder中创建对象
- onBindViewHolder在滑动时频繁调用
- 不要在这里创建新对象
- 复用字符串、避免创建临时对象
3. 使用对象池
- 对于需要频繁创建的对象,使用对象池
- 示例:自定义对象池管理对象复用
4. 减少GC
- 通过以上方法减少对象创建
- 减少GC频率,让列表滑动更流畅
5. 使用Systrace分析
- 使用Systrace分析卡顿
- 查看GC事件是否与卡顿对应
- 如果对应,优化GC
优化示例:
java
// 不好的做法:导致卡顿
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.textView.setText(new String("Item " + position)); // 创建新对象
holder.imageView.setImageBitmap(new Bitmap()); // 创建新对象
}
// 好的做法:避免卡顿
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.textView.setText("Item " + position); // 复用字符串
// 使用图片缓存,避免频繁创建Bitmap
Glide.with(context).load(url).into(holder.imageView);
}
6.3.2 设计题
1. 如何设计一个对象池?
完整答案:
对象池的核心是复用对象,避免频繁创建和销毁。
设计要点:
1. 定义对象池接口
java
public interface ObjectPool<T> {
T acquire(); // 获取对象
void release(T obj); // 归还对象
void clear(); // 清空池
}
2. 实现对象复用逻辑
- 使用队列(Queue)存储可复用的对象
- 获取时从队列中取出,归还时放回队列
- 如果队列为空,创建新对象
3. 设置池大小限制
- 避免池无限增长
- 设置最大池大小
- 超过大小时,不再归还对象
4. 提供获取和归还方法
- acquire():从池中获取对象,如果池为空则创建新对象
- release():归还对象到池中,如果池已满则不归还
- clear():清空池,释放所有对象
完整实现示例:
java
public class SimpleObjectPool<T> implements ObjectPool<T> {
private Queue<T> pool = new LinkedList<>();
private int maxSize;
private Supplier<T> factory; // 对象工厂
public SimpleObjectPool(int maxSize, Supplier<T> factory) {
this.maxSize = maxSize;
this.factory = factory;
}
@Override
public T acquire() {
T obj = pool.poll();
if (obj == null) {
obj = factory.get(); // 池为空时创建新对象
}
return obj;
}
@Override
public void release(T obj) {
if (obj != null && pool.size() < maxSize) {
resetObject(obj); // 重置对象状态
pool.offer(obj); // 归还到池中
}
}
@Override
public void clear() {
pool.clear();
}
private void resetObject(T obj) {
// 重置对象状态,准备复用
}
}
使用场景:
- 频繁创建的对象(如动画对象、临时对象)
- 创建成本高的对象(如Bitmap、大对象)
2. 如何设计一个内存监控系统?
完整答案:
内存监控系统需要实时监控内存使用情况,及时发现问题。
设计要点:
1. 实时监控内存使用
- 使用Runtime.getRuntime()获取内存信息
- 定期采集内存使用数据
- 记录内存使用曲线
2. 记录GC事件
- 监听GC事件(通过GC日志或Profiler)
- 记录GC类型、GC耗时、GC频率
- 分析GC对性能的影响
3. 分析内存趋势
- 分析内存使用趋势
- 识别内存泄漏(内存持续增长)
- 预测OOM风险
4. 设置告警阈值
- 设置内存使用告警阈值
- 设置GC频率告警阈值
- 超过阈值时发送告警
实现示例:
java
public class MemoryMonitor {
private static final long MEMORY_WARNING_THRESHOLD = 80 * 1024 * 1024; // 80MB
private static final int GC_FREQUENCY_THRESHOLD = 10; // 每秒10次
public void monitor() {
Runtime runtime = Runtime.getRuntime();
long usedMemory = runtime.totalMemory() - runtime.freeMemory();
long maxMemory = runtime.maxMemory();
// 检查内存使用
if (usedMemory > MEMORY_WARNING_THRESHOLD) {
sendWarning("内存使用过高: " + usedMemory);
}
// 检查GC频率
int gcFrequency = getGCFrequency();
if (gcFrequency > GC_FREQUENCY_THRESHOLD) {
sendWarning("GC过于频繁: " + gcFrequency + "次/秒");
}
}
}
3. 如何设计一个高内存效率的Android应用?
完整答案:
高内存效率的应用需要从多个方面优化内存使用。
设计要点:
1. 减少对象创建(使用对象池)
- 对于频繁创建的对象,使用对象池
- 复用对象,避免频繁创建销毁
- 示例:动画对象、临时对象
2. 避免内存泄漏(正确管理生命周期)
- 正确处理Activity、Fragment的生命周期
- 在onDestroy中清理资源
- 使用弱引用避免内存泄漏
3. 合理使用缓存(LruCache)
- 使用LruCache等有大小限制的缓存
- 及时清理不需要的缓存
- 避免无限制缓存
4. 及时释放资源
- 及时释放大对象(大图片、大数组)
- 关闭文件、数据库连接
- 清理集合、取消注册监听器
5. 优化数据结构
- 选择合适的集合类
- 避免内存浪费
- 及时清理集合
6. 监控和调优
- 使用Profiler监控内存使用
- 使用LeakCanary检测内存泄漏
- 根据实际情况优化
设计原则:
- 最小化对象创建:减少对象分配,减少GC
- 及时释放资源:避免内存泄漏,避免OOM
- 合理使用缓存:使用有大小限制的缓存
- 监控和优化:持续监控,持续优化
6.3.3 深入原理题(高级)
1. ART并发复制收集器是怎么工作的?
完整答案:
ART并发复制收集器的工作流程:
1. 并发标记(Concurrent Marking)
- 在后台线程执行标记工作
- 从GC Roots开始,标记所有存活对象
- 主线程继续运行,不暂停
2. 并发复制(Concurrent Copying)
- 在后台线程执行复制工作
- 将存活对象复制到新位置
- 主线程继续运行,不暂停
3. 减少主线程暂停时间
- 大部分工作在后台线程执行
- 主线程只在必要时短暂暂停
- 减少卡顿,提升用户体验
4. 适合交互式应用
- Android应用是交互式应用,需要低延迟
- 并发回收减少暂停时间,适合交互式应用
- 优化用户滑动、点击等操作的流畅度
工作流程:
markdown
1. 触发GC
2. 后台线程:并发标记存活对象
3. 后台线程:并发复制存活对象
4. 主线程:短暂暂停,完成最后的同步工作
5. 主线程:继续运行
优势:
- 减少主线程暂停时间
- 提升应用流畅度
- 适合交互式应用
2. 增量回收是如何减少卡顿的?
完整答案:
增量回收的核心思想是把大量回收工作分成小份,分批执行。
工作原理:
1. 分批执行
- 把大量回收工作分成多个小任务
- 每次只执行一小部分
- 避免一次性执行所有回收工作
2. 每次暂停时间短
- 每次只执行一小部分,暂停时间短
- 用户几乎感觉不到卡顿
- 多次短暂停比一次长暂停体验更好
3. 避免一次性卡顿太久
- 传统回收:一次性回收所有对象,暂停时间长(可能50ms)
- 增量回收:分成多次,每次暂停时间短(可能5ms)
- 用户感觉:几乎感觉不到
对比:
css
传统回收:
[一次性回收所有对象]
暂停时间:50ms
用户感觉:明显卡顿
增量回收:
[回收一点][回收一点][回收一点]
每次暂停:5ms
用户感觉:几乎感觉不到
优势:
- 减少单次暂停时间
- 提升用户体验
- 适合交互式应用
3. 压缩回收是如何避免内存碎片的?
完整答案:
压缩回收的核心是回收后整理内存,把分散的内存块合并。
工作原理:
1. 标记阶段
- 标记所有需要回收的对象
- 标记所有存活对象
2. 整理阶段
- 将存活对象向一端移动
- 把分散的内存块合并成连续空间
- 更新所有对象的引用
3. 清除阶段
- 清理边界外的内存
- 释放垃圾对象占用的内存
效果:
css
压缩前:
[对象1][空闲][对象2][空闲][对象3]
内存碎片多,分配效率低
压缩后:
[对象1][对象2][对象3][空闲][空闲]
内存连续,分配效率高
优势:
- 没有内存碎片:整理后内存连续
- 提高内存利用率:避免内存浪费
- 提高分配效率:连续内存分配更快
为什么老年代用压缩回收?
- 老年代对象存活率高,移动成本相对较低
- 避免内存碎片,提高内存利用率
- 不浪费内存(不像标记-复制需要两块内存)
4. 为什么Android只用一种垃圾回收器?
完整答案:
1. 针对Android场景优化
- Android应用都是交互式应用,需要低延迟
- 并发复制收集器针对低延迟优化
- 不需要多种收集器,一种就够了
2. 不需要开发者选择
- 系统自动使用,开发者不需要了解收集器的区别
- 简化开发,提高效率
- 避免开发者选择错误的收集器
3. 系统自动管理
- 系统根据应用特点自动优化
- 不需要开发者配置参数
- 减少开发负担
4. 简化开发
- 开发者不需要了解多种收集器的区别
- 不需要根据应用特点选择收集器
- 专注于业务逻辑
与JVM的区别:
- JVM:有多种收集器可选,需要开发者根据应用特点选择
- Android:只有一种,系统自动使用,针对Android场景优化
5. Android内存限制对GC的影响?
完整答案:
1. 内存有限必须合理使用
- Android设备内存有限(低端设备可能只有几十MB)
- 必须合理使用内存,避免浪费
- 内存不足时,系统可能杀死应用
2. GC更频繁
- 内存有限,容易触发GC
- 内存不足时,GC更频繁
- 需要优化内存使用,减少GC频率
3. 需要优化内存使用
- 减少对象创建
- 避免内存泄漏
- 及时释放资源
- 合理使用缓存
4. 避免OOM
- 内存有限,容易发生OOM
- 必须避免内存泄漏
- 必须优化内存使用
- 必须及时释放资源
5. 影响GC策略
- 内存有限,GC策略更激进
- 更频繁地触发GC
- 更早地回收对象
优化建议:
- 减少对象创建:使用对象池、避免不必要的对象创建
- 避免内存泄漏:使用LeakCanary检测,修复泄漏
- 及时释放资源:在onDestroy中清理资源
- 合理使用缓存:使用LruCache,及时清理
总结
本文详细介绍了Android平台的Java内存回收机制(GC),基于ART(Android Runtime)实现。
核心内容:
- 垃圾回收基础:GC的概念、对象判断方法、引用类型
- 垃圾回收算法:标记-清除、标记-复制(Android主要用)、标记-整理、分代收集
- Android ART垃圾回收器:只有一种收集器(并发复制收集器),分代回收、并发回收、增量回收、压缩回收
- GC优化与实践:减少对象分配、避免内存泄漏、处理内存溢出
- GC检测与诊断:Profiler、logcat、Systrace、MAT、LeakCanary等工具
- 面试题:涵盖基础和高级题目,完全应对面试