Java内存回收机制(GC)完整详解

第一部分:垃圾回收基础

1. 垃圾回收基础

1.1 什么是垃圾回收

定义:

垃圾回收(Garbage Collection,GC)是自动管理内存的机制,能够自动识别和回收不再使用的对象,释放它们占用的内存空间。

作用:

  1. 自动释放内存:不需要开发者手动释放内存,避免忘记释放导致的内存泄漏
  2. 避免内存泄漏:自动回收不再使用的对象,防止内存逐渐被占用
  3. 简化内存管理:开发者只需要关注业务逻辑,不需要关心内存释放
  4. 提高开发效率:减少内存管理相关的bug

Android中的重要性:

  1. 内存有限:Android设备内存有限,必须合理使用
  2. 性能影响:GC性能直接影响应用流畅度
  3. 用户体验:频繁GC会导致卡顿,影响用户体验
  4. 系统稳定性:内存不足可能导致应用被系统杀死

示例:

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场景):

  1. 虚拟机栈中引用的对象
    • 局部变量、方法参数
    • 示例:方法中的局部变量引用的对象
java 复制代码
public void method() {
    Object obj = new Object();  // obj在栈中,引用的对象是GC Root
    // obj引用的对象不会被回收
}
  1. 方法区中静态属性引用的对象
    • static变量
    • 示例:类的静态变量
java 复制代码
public class MyClass {
    private static Object staticObj = new Object();  // staticObj是GC Root
    // staticObj引用的对象不会被回收
}
  1. 方法区中常量引用的对象
    • 常量
    • 示例:字符串常量
java 复制代码
public class MyClass {
    private static final String CONSTANT = "Hello";  // CONSTANT是GC Root
}
  1. 本地方法栈中引用的对象

    • Native方法中的引用
    • 示例:JNI调用中的对象
  2. 同步锁持有的对象

    • synchronized持有的对象
    • 示例:锁对象
java 复制代码
Object lock = new Object();
synchronized (lock) {
    // lock是GC Root,不会被回收
}
  1. 内部引用
    • 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)

算法原理:

分两个阶段:

  1. 标记阶段:从GC Roots开始,标记所有需要回收的对象
  2. 清除阶段:清除所有被标记的对象

算法流程:

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)

算法原理:

分三个阶段:

  1. 标记阶段:标记所有需要回收的对象
  2. 整理阶段:将存活对象向一端移动
  3. 清除阶段:清理边界外的内存

算法流程:

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中的分代收集:

  1. 年轻代用标记-复制算法

    • 快速回收,适合频繁回收
    • 大部分对象很快被回收,复制成本低
  2. 老年代用标记-整理算法

    • 避免内存碎片
    • 对象存活率高,移动成本相对较低
  3. 这样设计的好处:

    • 提高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:

  1. 编译方式不同

    • ART:AOT编译(Ahead-Of-Time),安装时编译
    • JVM:JIT编译(Just-In-Time),运行时编译
  2. GC机制不同

    • ART:只有一种收集器(并发复制收集器)
    • JVM:有多种收集器可选(Serial、Parallel、CMS、G1、ZGC等)
  3. 优化方向不同

    • 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 分代回收策略(回收策略)

内存分为两部分:

  1. 年轻代:存放新创建的对象(生命周期短)
  2. 老年代:存放长期存活的对象(生命周期长)

为什么分代?

因为大部分对象很快就会被回收,分开处理更高效:

  • 年轻代:频繁回收,用快速算法(标记-复制)
  • 老年代:偶尔回收,用避免碎片的算法(标记-整理)

示例:

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(内存分析器)功能:

  1. 实时查看内存使用情况

    • 显示内存使用曲线图
    • 可以看到内存随时间的变化趋势
    • 识别内存泄漏(内存持续增长)
  2. 查看GC事件

    • GC事件会在内存曲线上显示为小图标
    • 点击GC事件可以查看详细信息:
      • GC类型(Partial GC、Full GC、Sticky GC)
      • GC耗时(暂停时间)
      • 回收前后的内存大小
      • 回收了多少内存
  3. 生成堆转储(Heap Dump)

    • 点击"Heap Dump"按钮生成堆转储
    • 可以查看当前所有对象的内存占用
    • 可以导出为hprof文件,用MAT分析
  4. 查看对象分配情况

    • 使用"Allocation Tracking"功能
    • 记录一段时间内的对象分配
    • 可以看到哪些对象被分配,在哪里分配(调用栈)

CPU Profiler(CPU分析器)功能:

  • 查看GC占用的CPU时间
  • 分析GC对性能的影响
  • 查看GC线程的执行情况

详细使用步骤:

  1. 打开Profiler窗口

    • 在Android Studio中,点击底部的"Profiler"标签
    • 或通过菜单:View → Tool Windows → Profiler
  2. 选择要分析的应用进程

    • 在Profiler窗口中选择要分析的应用进程
    • 确保应用正在运行(debug模式)
  3. 查看内存使用情况

    • 点击"Memory"标签
    • 查看内存使用曲线
    • 观察内存是否持续增长(可能的内存泄漏)
  4. 查看GC事件

    • 在内存曲线中,GC事件会显示为小图标
    • 点击GC事件,可以查看详细信息
    • 分析GC频率和耗时
  5. 生成堆转储

    • 点击"Heap Dump"按钮
    • 等待生成完成
    • 可以查看对象列表、内存占用等
    • 可以导出为hprof文件
  6. 查看对象分配情况

    • 点击"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日志:

  1. 查看GC频率

    • 统计一段时间内GC的次数
    • 如果GC过于频繁(比如每秒多次),说明有问题
    • 可能原因:频繁创建对象、内存泄漏
  2. 查看GC耗时

    • 关注paused时间(暂停时间)
    • 如果暂停时间过长(比如>50ms),会导致卡顿
    • Full GC的暂停时间通常比Partial GC长
  3. 查看GC类型

    • Partial GC:正常,快速
    • Full GC:需要关注,可能导致卡顿
    • Sticky GC:正常,几乎感觉不到
  4. 查看内存回收情况

    • freed:释放了多少内存
    • 如果释放的内存很少,说明大部分对象还在使用
    • 如果释放的内存很多,说明有很多垃圾对象

使用场景:

  • 快速查看GC情况
  • 分析GC频率和耗时
  • 排查GC导致的性能问题

5.1.3 Systrace(分析GC对性能的影响)

作用: 分析GC对性能的影响,找出GC导致的卡顿问题

Systrace是什么:

  • Android系统提供的性能分析工具
  • 可以记录系统各个线程的执行情况
  • 可以看到GC在主线程上的执行时间

如何使用:

方法1:使用Android Studio的Systrace工具

  1. 在Android Studio中,点击"Tools" → "Android" → "Device Monitor"
  2. 选择要分析的应用
  3. 点击"Systrace"按钮
  4. 设置记录时间(比如10秒)
  5. 操作应用(比如滑动列表)
  6. 停止记录,查看结果

方法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能分析什么:

  1. GC暂停时间

    • 可以看到GC在主线程上的执行时间
    • 可以看到GC暂停了多久
    • 可以判断GC是否导致卡顿
  2. GC对主线程的影响

    • 可以看到主线程在GC期间被阻塞
    • 可以看到GC导致的帧率下降
    • 可以分析GC对用户体验的影响
  3. 应用卡顿的原因

    • 可以看到卡顿是否由GC引起
    • 可以看到GC和其他操作的时序关系
    • 可以找出性能瓶颈

如何查看Systrace结果:

  1. 查看主线程

    • 找到"main"线程
    • 查看是否有红色的帧(表示掉帧)
    • 查看是否有GC事件
  2. 查看GC事件

    • 找到"GC"相关的事件
    • 查看GC的持续时间
    • 查看GC是否与卡顿时间对应
  3. 分析卡顿原因

    • 如果卡顿时间与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"

  • 查看引用链,比如:

    scss 复制代码
    MyActivity实例
      ↑ (被引用)
    static MyActivity.activity (静态变量)
      ↑ (GC Root)
  • 找出是静态变量持有Activity引用

步骤6:定位代码

  • 根据引用链,定位到代码中的静态变量
  • 修复代码(使用弱引用或及时清理)

MAT的主要功能详细说明:

1. Histogram(直方图)

  • 作用:显示所有类的实例数量和内存占用
  • 使用方法
    1. 打开"Histogram"视图
    2. 在搜索框输入类名,查找特定类
    3. 点击列标题排序(比如按"Retained Heap"排序)
    4. 双击类名,查看这个类的所有实例
  • 适用场景:找出占用内存最多的类

2. Dominator Tree(支配树)

  • 作用:显示对象的引用关系树,找出占用内存最多的对象
  • 使用方法
    1. 打开"Dominator Tree"视图
    2. 点击"Retained Heap"列标题,按内存占用排序
    3. 展开对象,查看引用关系
    4. 找出占用内存最多的对象
  • 适用场景:找出占用内存最多的对象

3. Path to GC Roots(到GC Roots的路径)

  • 作用:显示对象到GC Roots的引用链,找出为什么对象无法被回收
  • 使用方法
    1. 右键点击可疑对象
    2. 选择"Path to GC Roots"
    3. 选择"exclude weak references"(排除弱引用)
    4. 查看引用链,找出泄漏原因
  • 适用场景:分析内存泄漏的原因

4. Leak Suspects(泄漏嫌疑)

  • 作用:MAT自动检测可能的内存泄漏,生成泄漏报告
  • 使用方法
    1. 打开"Leak Suspects"视图
    2. 查看MAT自动检测的泄漏报告
    3. 根据报告分析泄漏原因
  • 适用场景:快速定位内存泄漏

5. OQL(Object Query Language)

  • 作用:使用类似SQL的查询语言查询对象
  • 使用方法
    1. 打开"OQL"视图

    2. 输入查询语句,比如:

      sql 复制代码
      SELECT * FROM java.lang.String WHERE this.value.length > 100
    3. 执行查询,查看结果

  • 适用场景:查找特定条件的对象

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(直方图)

  • 作用:显示所有类的实例数量和内存占用
  • 使用方法
    1. 打开"Histogram"视图
    2. 在搜索框输入类名,查找特定类
    3. 点击列标题排序(比如按"Retained Heap"排序)
    4. 双击类名,查看这个类的所有实例
  • 适用场景:找出占用内存最多的类

2. Dominator Tree(支配树)

  • 作用:显示对象的引用关系树,找出占用内存最多的对象
  • 使用方法
    1. 打开"Dominator Tree"视图
    2. 点击"Retained Heap"列标题,按内存占用排序
    3. 展开对象,查看引用关系
    4. 找出占用内存最多的对象
  • 适用场景:找出占用内存最多的对象

3. Path to GC Roots(到GC Roots的路径)

  • 作用:显示对象到GC Roots的引用链,找出为什么对象无法被回收
  • 使用方法
    1. 右键点击可疑对象
    2. 选择"Path to GC Roots"
    3. 选择"exclude weak references"(排除弱引用)
    4. 查看引用链,找出泄漏原因
  • 适用场景:分析内存泄漏的原因

4. Leak Suspects(泄漏嫌疑)

  • 作用:MAT自动检测可能的内存泄漏,生成泄漏报告
  • 使用方法
    1. 打开"Leak Suspects"视图
    2. 查看MAT自动检测的泄漏报告
    3. 根据报告分析泄漏原因
  • 适用场景:快速定位内存泄漏

5. OQL(Object Query Language)

  • 作用:使用类似SQL的查询语言查询对象
  • 使用方法
    1. 打开"OQL"视图

    2. 输入查询语句,比如:

      sql 复制代码
      SELECT * FROM java.lang.String WHERE this.value.length > 100
    3. 执行查询,查看结果

  • 适用场景:查找特定条件的对象

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"

  • 查看引用链:

    scss 复制代码
    MyActivity实例
      ↑ (被引用)
    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能检测内存泄漏?

原理说明:

  1. 正常情况(没有泄漏)

    • Activity.onDestroy()后,Activity应该不再被引用
    • GC时,Activity会被回收
    • WeakReference会被放入ReferenceQueue
    • 检查ReferenceQueue,发现ref在queue中,说明Activity已被回收
  2. 异常情况(有泄漏)

    • Activity.onDestroy()后,Activity仍然被某个对象引用(比如静态变量)
    • GC时,Activity因为有强引用,不会被回收
    • WeakReference不会被放入ReferenceQueue
    • 检查ReferenceQueue,发现ref不在queue中,说明Activity没有被回收
    • 确认有内存泄漏

引用链分析原理:

当LeakCanary确认有内存泄漏后,会:

  1. 生成堆转储:保存当前所有对象的内存快照
  2. 分析引用链:从泄漏对象开始,向上查找引用链
  3. 找出泄漏路径:找出是哪个对象持有泄漏对象的引用

示例:

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。

主要区别:

  1. 编译方式不同

    • ART:AOT编译(Ahead-Of-Time),应用安装时编译为机器码
    • JVM:JIT编译(Just-In-Time),运行时编译为机器码
  2. GC机制不同

    • ART:只有一种收集器(并发复制收集器),系统自动使用
    • JVM:有多种收集器可选(Serial、Parallel、CMS、G1、ZGC等),开发者需要选择
  3. 优化方向不同

    • ART:针对低延迟优化,适合交互式应用(减少卡顿)
    • JVM:有多种优化方向(吞吐量、延迟、内存占用等)
  4. 内存管理不同

    • 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使用可达性分析算法来判断对象是否已死。

算法原理:

  1. 从一组称为"GC Roots"的对象开始
  2. 向下搜索,标记所有能够从GC Roots到达的对象(存活对象)
  3. 无法从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:

  1. 虚拟机栈中引用的对象

    • 局部变量、方法参数
    • 示例:方法中的局部变量引用的对象
  2. 方法区中静态属性引用的对象

    • static变量
    • 示例:类的静态变量
  3. 方法区中常量引用的对象

    • 常量
    • 示例:字符串常量
  4. 本地方法栈中引用的对象

    • Native方法中的引用
    • 示例:JNI调用中的对象
  5. 同步锁持有的对象

    • synchronized持有的对象
    • 示例:锁对象
  6. 内部引用

    • 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. 标记-清除算法的优缺点?

完整答案:

算法原理: 分两个阶段:

  1. 标记阶段:从GC Roots开始,标记所有需要回收的对象
  2. 清除阶段:清除所有被标记的对象

优点:

  • 实现简单
  • 不需要移动对象

缺点:

  • 产生内存碎片:清除后留下不连续的内存空间,影响后续内存分配效率
  • 效率较低:需要两次遍历(标记和清除)

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. 标记-整理算法的优缺点?

完整答案:

算法原理: 分三个阶段:

  1. 标记阶段:标记所有需要回收的对象
  2. 整理阶段:将存活对象向一端移动
  3. 清除阶段:清理边界外的内存

优点:

  • 没有内存碎片:整理后内存连续
  • 不浪费内存:不需要两块内存

缺点:

  • 效率较低:需要移动对象,更新所有引用
  • 适合老年代:对象存活率高,移动成本相对较低

Android中的使用:

  • Android在某些场景下使用
  • 主要用于老年代回收(避免内存碎片)

为什么老年代用标记-整理?

  • 老年代对象存活率高,移动成本相对较低
  • 避免内存碎片,提高内存利用率
  • 不浪费内存(不像标记-复制需要两块内存)

5. 分代收集算法的原理?

完整答案:

理论基础: 根据对象生命周期不同,将内存分为不同的代:

  • 大部分对象生命周期很短:创建后很快被回收
  • 少数对象生命周期很长:长期存活

分代策略:

年轻代(Young Generation):

  • 特点:存放新创建的对象,生命周期短
  • 回收算法:标记-复制算法(快速高效)
  • 回收频率:高(频繁回收)
  • 回收类型:Partial GC(部分回收)
  • 回收时间:短(几毫秒)

老年代(Old Generation):

  • 特点:存放长期存活的对象,生命周期长
  • 回收算法:标记-整理算法(避免碎片)
  • 回收频率:低(偶尔回收)
  • 回收类型:Full GC(完全回收)
  • 回收时间:长(可能几十到几百毫秒)

Android中的分代收集:

  1. 年轻代用标记-复制算法:快速回收,适合频繁回收
  2. 老年代用标记-整理算法:避免内存碎片
  3. 这样设计的好处:提高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?

完整答案:

触发条件:整个堆内存不足

具体场景:

  1. 老年代空间满了

    • 长期存活的对象占满了老年代
    • 无法继续分配新对象
  2. 堆内存整体不足

    • 年轻代和老年代都满了
    • 无法分配新对象
  3. 显式调用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类型。

特点:

  • 只清理新对象:只清理刚刚创建的新对象
  • 非常快速:几乎感觉不到
  • 暂停时间极短:几毫秒甚至更短
  • 专门优化频繁分配对象的场景

适用场景:

  1. 列表快速滑动时

    • RecyclerView滚动时频繁创建ViewHolder、临时对象
    • Sticky GC快速清理这些短生命周期对象
  2. 动画播放时

    • 动画过程中不断创建临时对象
    • Sticky GC快速清理
  3. 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. 预先创建一些对象,放在池中
  2. 需要时从池中获取,使用完后归还到池中
  3. 避免频繁创建和销毁对象

实现方式:

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引用

    java 复制代码
    private 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)实现。

核心内容:

  1. 垃圾回收基础:GC的概念、对象判断方法、引用类型
  2. 垃圾回收算法:标记-清除、标记-复制(Android主要用)、标记-整理、分代收集
  3. Android ART垃圾回收器:只有一种收集器(并发复制收集器),分代回收、并发回收、增量回收、压缩回收
  4. GC优化与实践:减少对象分配、避免内存泄漏、处理内存溢出
  5. GC检测与诊断:Profiler、logcat、Systrace、MAT、LeakCanary等工具
  6. 面试题:涵盖基础和高级题目,完全应对面试
相关推荐
用户03048059126321 小时前
Spring Boot 配置文件加载大揭秘:优先级覆盖与互补合并机制详解
java·后端
parade岁月21 小时前
把 Git 提交变成“可执行规范”:Commit 规范体系与 Husky/Commitlint/Commitizen/Lint-staged 全链路介绍
前端·代码规范
pas13621 小时前
29-mini-vue element搭建更新
前端·javascript·vue.js
CRUD酱1 天前
微服务分模块后怎么跨模块访问资源
java·分布式·微服务·中间件·java-ee
IT=>小脑虎1 天前
2026版 React 零基础小白进阶知识点【衔接基础·企业级实战】
前端·react.js·前端框架
gAlAxy...1 天前
5 种 SpringBoot 项目创建方式
java·spring boot·后端
IT=>小脑虎1 天前
2026版 React 零基础小白入门知识点【基础完整版】
前端·react.js·前端框架
lalala_lulu1 天前
什么是事务,事务有什么特性?
java·开发语言·数据库
CCPC不拿奖不改名1 天前
python基础:python语言中的函数与模块+面试习题
开发语言·python·面试·职场和发展·蓝桥杯