在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是必选项哦