在Android开发中,内存泄露发生的场景其实主要就两点,一是数据过大的问题,而是调用与被调用生命周期不一致问题,对于对象生命周期不一致导致的泄漏问题占90%,最常见的也不好分析的当属匿名内部类的内存泄漏,在文章《内存泄漏大集结:安卓开发者不可错过的性能优化技巧》 中我大概进行了总结,最近在开发时遇到了一个问题,就是LeakCannry 检测到的内存泄漏,LeakCannry检测的原理大概就是GC 可达性算法实现的,我们产品中最多的一个问题就是匿名内部类导致的。
匿名内部类如何导致内存泄漏
在Java体系中,内部类有多种,最常见的就是静态内部类、匿名内部类,一般情况下,都推荐使用静态内部类,那这是为什么呢,先看一个例子:
public class Test {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
}
}).start();
}
}
匿名内部类的泄漏原因:内部类持有外部类的引用,上述场景中,当外部类销毁时,匿名内部类Runnable 会导致内存泄漏,
验证这个结论
上述代码的class 文件通过Javap -c 查看后是这样的
Compiled from "Test.java"
public class Test {
public Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #2 // class java/lang/Thread
3: dup
4: new #3 // class Test$1
7: dup
8: invokespecial #4 // Method Test$1."<init>":()V
11: invokespecial #5 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
14: invokevirtual #6 // Method java/lang/Thread.start:()V
17: return
}
我们直接看main 方法中的指令:
0: new #2 // 创建一个新的 Thread 对象
3: dup // 复制栈顶的对象引用
4: new #3 // 创建一个匿名内部类 Test$1 的实例
7: dup // 复制栈顶的对象引用
8: invokespecial #4 // 调用匿名内部类 Test$1 的构造方法
11: invokespecial #5 // 调用 Thread 类的构造方法,传入匿名内部类对象
14: invokevirtual #6 // 调用 Thread 类的 start 方法,启动线程
17: return // 返回
我们可以看到,在第4步中 使用new 指令创建了一个Test$1的实例,并且在第8步中,通过invokespecial 指令调用匿名内部类的构造方法,这样一来生成的内部类就会持有外部类的引用,从而外部类不能回收,将导致内存泄漏。
Lambda为什么不泄漏
刚开始,我以为Lambda只是语法糖,不会有其他的作用,然而,哈哈 大家估计已经想到了,
匿名内部类使用Lambda 时不会造成内存泄漏。
看代码:
public class Test {
public static void main(String[] args) {
new Thread(() -> {
}).start();
}
}
将上面的代码改为Lambda 格式
class 文件:
Compiled from "Test.java"
public class Test {
public Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #2 // class java/lang/Thread
3: dup
4: invokedynamic #3, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
9: invokespecial #4 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
12: invokevirtual #5 // Method java/lang/Thread.start:()V
15: return
}
第一眼看上去就已经知道了答案,在这份字节码中没有生成内部类,
在Lambda格式中,没有生成内部类,而是直接使用invokedynamic 指令动态调用run方法,生成一个Runnable对象。再调用调用Thread类的构造方法,将生成的Runnable对象传入。从而避免了持有外部类的引用,也就避免了内存泄漏的发生。
在开发中,了解字节码知识还是非常有必要的,在关键时刻,我们查看字节码,确实能帮助自己解答一些疑惑,下面是常见的一些字节码指令
常见的字节码指令
Java 字节码指令是一组在 Java 虚拟机中执行的操作码,用于执行特定的计算、加载、存储、控制流等操作。以下是 Java 字节码指令的一些常见指令及其功能:
- 加载和存储指令:
aload
:从局部变量表中加载引用类型到操作数栈。astore
:将引用类型存储到局部变量表中。iload
:从局部变量表中加载 int 类型到操作数栈。istore
:将 int 类型存储到局部变量表中。fload
:从局部变量表中加载 float 类型到操作数栈。fstore
:将 float 类型存储到局部变量表中。
- 算术和逻辑指令:
iadd
:将栈顶两个 int 类型数值相加。isub
:将栈顶两个 int 类型数值相减。imul
:将栈顶两个 int 类型数值相乘。idiv
:将栈顶两个 int 类型数值相除。iand
:将栈顶两个 int 类型数值进行按位与操作。ior
:将栈顶两个 int 类型数值进行按位或操作。
- 类型转换指令:
i2l
:将 int 类型转换为 long 类型。l2i
:将 long 类型转换为 int 类型。f2d
:将 float 类型转换为 double 类型。d2i
:将 double 类型转换为 int 类型。
- 控制流指令:
if_icmpeq
:如果两个 int 类型数值相等,则跳转到指定位置。goto
:无条件跳转到指定位置。tableswitch
:根据索引值跳转到不同位置的指令。
- 方法调用和返回指令:
invokevirtual
:调用实例方法。invokestatic
:调用静态方法。invokeinterface
:调用接口方法。ireturn
:从方法中返回 int 类型值。invokedynamic
: 运行时动态解析并绑定方法调用
详细的字节码指令列表和说明可参考 Java 虚拟机规范(Java Virtual Machine Specification)
总结
为了解决问题而储备知识,是最快的学习方式。
在开发中,也不要刻意去设计invokedynamic的代码,但是Java开发的同学,Lambda是必选项哦
最后
如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。
如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。
全套视频资料:
一、面试合集
二、源码解析合集
三、开源框架合集
欢迎大家一键三连支持,若需要文中资料,直接扫描文末CSDN官方认证微信卡片免费领取↓↓↓
PS:群里还设有ChatGPT机器人,可以解答大家在工作上或者是技术上的问题