⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 blog.csdn.net/hbdatouerzi/article/details/122534134 「我是黄大仙」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

1 背景

匿名内部类会持有外部类的引用,因此有造成内存泄漏的风险;

那么Lambda 表达式是否会造成内存泄漏呢?

2 匿名内部类 VS Lambda 表达式

我们新建一个类TestInner,其中test方法里面包含一个Lambda表达式,test1方法里面包含一个匿名内部类

public class TestInner {

public void test(){
new Thread(()->{
Log.i("测试","dddd");
}).start();
}

public void test1(){
new Thread(new Runnable() {
@Override
public void run() {
Log.i("测试","dddd1");
}
}).start();
}
}

我们将其编译成apk,然后查看编译后的产物

匿名内部类

首先来看匿名内部类。

我们发现test1方法编译产物如下:

.method public test1()V
.registers 3

.line 14
new-instance v0, Ljava/lang/Thread;

new-instance v1, Lcom/example/jnihelper/TestInner$1;

invoke-direct {v1, p0}, Lcom/example/jnihelper/TestInner$1;-><init>(Lcom/example/jnihelper/TestInner;)V

invoke-direct {v0, v1}, Ljava/lang/Thread;-><init>(Ljava/lang/Runnable;)V

.line 19
invoke-virtual {v0}, Ljava/lang/Thread;->start()V

.line 20
return-void
.end method

从中可以发现,test1方法里面调用了TestInner$1的init方法,接着我们可以从编译产物当中找到TestInner$1

接着我们查看TestInner$1的内容,init方法会将TestInner以参数的形式传入

# direct methods
.method constructor <init>(Lcom/example/jnihelper/TestInner;)V
.registers 2
.param p1, "this$0" # Lcom/example/jnihelper/TestInner;

.line 14
iput-object p1, p0, Lcom/example/jnihelper/TestInner$1;->this$0:Lcom/example/jnihelper/TestInner;

invoke-direct {p0}, Ljava/lang/Object;-><init>()V

return-void
.end method


# virtual methods
.method public run()V
.registers 3

.line 17
const-string v0, "\u6d4b\u8bd5"

const-string v1, "dddd1"

invoke-static {v0, v1}, Landroid/util/Log;->i(Ljava/lang/String;Ljava/lang/String;)I

.line 18
return-void
.end method

所以匿名内部类会持有外部类的引用,因此会有内存泄漏的风险。

那么Lambda 表达式呢?

Lambda 表达式

我们先来看下含有Lambda表达式的test方法的编译产物

.method static synthetic Lambda表达式()V
.registers 2

.line 9
const-string v0, "\u6d4b\u8bd5"

const-string v1, "dddd"

invoke-static {v0, v1}, Landroid/util/Log;->i(Ljava/lang/String;Ljava/lang/String;)I

.line 10
return-void
.end method


# virtual methods
.method public test()V
.registers 3

.line 8
new-instance v0, Ljava/lang/Thread;

sget-object v1, Lcom/example/jnihelper/TestInner$$ExternalSyntheticLambda0;->INSTANCE:Lcom/example/jnihelper/TestInner$$ExternalSyntheticLambda0;

invoke-direct {v0, v1}, Ljava/lang/Thread;-><init>(Ljava/lang/Runnable;)V

.line 10
invoke-virtual {v0}, Ljava/lang/Thread;->start()V

.line 11
return-void
.end method

我们可以看到test方法里面调用了com/example/jnihelper/TestInner$$ExternalSyntheticLambda0

打开TestInner$$ExternalSyntheticLambda0,我们发现最后执行的run方法

会调用TestInner的lambda$test$0方法,而该方法是一个静态方法。

# direct methods
.method static synthetic constructor <clinit>()V
.registers 1

new-instance v0, Lcom/example/jnihelper/TestInner$$ExternalSyntheticLambda0;

invoke-direct {v0}, Lcom/example/jnihelper/TestInner$$ExternalSyntheticLambda0;-><init>()V

sput-object v0, Lcom/example/jnihelper/TestInner$$ExternalSyntheticLambda0;->INSTANCE:Lcom/example/jnihelper/TestInner$$ExternalSyntheticLambda0;

return-void
.end method

.method private synthetic constructor <init>()V
.registers 1

invoke-direct {p0}, Ljava/lang/Object;-><init>()V

return-void
.end method


# virtual methods
.method public final run()V
.registers 1

invoke-static {}, Lcom/example/jnihelper/TestInner;->lambda$test$0()V

return-void
.end method

可以看出此时Lambda表达式并未持有外部类的引用,因而没有造成内存泄漏。

然而真的是这样吗?

我们再回过头来看下TestInner的Lambda 表达式实现,可以看出,内部确实没有用到外部类。

public void test(){
new Thread(()->{
Log.i("测试","dddd");
}).start();
}

那么,我们如果手动引用呢?

public class TestInner {

private String helloInner = "helloIIIII";

public void test() {
new Thread(() -> {
Log.i("测试", "dddd");
Log.i("测试", TestInner.this.helloInner);//手动显示引用
}).start();
}

...
}

编译之后查看其编译产物

# virtual methods
.method public synthetic lambda$test$0$com-example-jnihelper-TestInner()V
.registers 3

.line 11
const-string v0, "\u6d4b\u8bd5"

const-string v1, "dddd"

invoke-static {v0, v1}, Landroid/util/Log;->i(Ljava/lang/String;Ljava/lang/String;)I

.line 12
iget-object v1, p0, Lcom/example/jnihelper/TestInner;->helloInner:Ljava/lang/String;

invoke-static {v0, v1}, Landroid/util/Log;->i(Ljava/lang/String;Ljava/lang/String;)I

.line 13
return-void
.end method

.method public test()V
.registers 3

.line 10
new-instance v0, Ljava/lang/Thread;

new-instance v1, Lcom/example/jnihelper/TestInner$$ExternalSyntheticLambda0;

invoke-direct {v1, p0}, Lcom/example/jnihelper/TestInner$$ExternalSyntheticLambda0;-><init>(Lcom/example/jnihelper/TestInner;)V

invoke-direct {v0, v1}, Ljava/lang/Thread;-><init>(Ljava/lang/Runnable;)V

.line 13
invoke-virtual {v0}, Ljava/lang/Thread;->start()V

.line 14
return-void
.end method

我们发现对应的实现方法lambda$test0 00com-example-jnihelper-TestInner已经变成非静态方法了,

而在test方法里面,调用了TestInner$$ExternalSyntheticLambda0的init方法,并将外部类当作参数传入,

我们再来看TestInner$$ExternalSyntheticLambda0的实现,可以发现TestInner会在init方法中当作参入传入,并当作其成员变量。

.class public final synthetic Lcom/example/jnihelper/TestInner$$ExternalSyntheticLambda0;
.super Ljava/lang/Object;

# interfaces
.implements Ljava/lang/Runnable;


# instance fields
.field public final synthetic f$0:Lcom/example/jnihelper/TestInner;


# direct methods
.method public synthetic constructor <init>(Lcom/example/jnihelper/TestInner;)V
.registers 2

invoke-direct {p0}, Ljava/lang/Object;-><init>()V

iput-object p1, p0, Lcom/example/jnihelper/TestInner$$ExternalSyntheticLambda0;->f$0:Lcom/example/jnihelper/TestInner;

return-void
.end method


# virtual methods
.method public final run()V
.registers 2

iget-object v0, p0, Lcom/example/jnihelper/TestInner$$ExternalSyntheticLambda0;->f$0:Lcom/example/jnihelper/TestInner;

invoke-virtual {v0}, Lcom/example/jnihelper/TestInner;->lambda$test$0$com-example-jnihelper-TestInner()V

return-void
.end method

所以,可以看出,如果在Lambda 表达式中需显示的调用外部类,才会持有外部类的引用。

3 结论

匿名内部类会持有外部类的引用,有造成内存泄漏的风险;

Lambda 表达式,若内部没有引用外部类,则不会持有外部类;若内部引用到了外部类,则会持有外部类。

文章目录
  1. 1. 1 背景
  2. 2. 2 匿名内部类 VS Lambda 表达式
    1. 2.1. 匿名内部类
    2. 2.2. Lambda 表达式
  3. 3. 3 结论