如何避免在 Java 中使用双括号初始化

文章讲述了Java中使用双括号初始化HashMap可能导致内存泄漏的问题,通过一个BugSnag客户端的内存泄漏追踪例子,解释了如何分析内存泄漏,并提出更安全的初始化方式,如使用`Collections.singletonMap()`或转用Kotlin来避免此类问题。

结论先行

避免像这样,在 Java 中使用双括号初始化:​​​​​​​

new HashMap<String, String>() {{     put("key", value);}};

内存泄漏追踪

我最近正在  LeakCanary  看到了以下内存泄漏追踪信息:​​​​​​​

┬───│ GC Root: Global variable in native code├─ com.bugsnag.android.AnrPlugin instance│   Leaking: UNKNOWN│   ↓ AnrPlugin.client│               ~~~~~~├─ com.bugsnag.android.Client instance│   Leaking: UNKNOWN│   ↓ Client.breadcrumbState│               ~~~~~~~~~~~~~~~├─ com.bugsnag.android.BreadcrumbState instance│   Leaking: UNKNOWN│   ↓ BreadcrumbState.store│                       ~~~~~├─ com.bugsnag.android.Breadcrumb[] array│   Leaking: UNKNOWN│   ↓ Breadcrumb[494]│               ~~~~~├─ com.bugsnag.android.Breadcrumb instance│   Leaking: UNKNOWN│   ↓ Breadcrumb.impl│                   ~~~~├─ com.bugsnag.android.BreadcrumbInternal instance│   Leaking: UNKNOWN│   ↓ BreadcrumbInternal.metadata│                           ~~~~~~~~├─ com.example.MainActivity$1 instance│   Leaking: UNKNOWN│   Anonymous subclass of java.util.HashMap│   ↓ MainActivity$1.this$0│                       ~~~~~~╰→ com.example.MainActivity instance    Leaking: YES (Activity#mDestroyed is true)
当打开一个内存泄漏追踪日志时,我首先会看底部的对象,了解它的生命周期,这将帮助我理解内存泄漏追踪中的其他对象是否应该有相同的生命周期。

在底部,我们看到:​​​​​​​

╰→ com.example.MainActivity instance    Leaking: YES (Activity#mDestroyed is true)

Activity 已经被销毁,应该已被垃圾回收器给回收掉了,但它仍驻留在内存中。

此时,我开始在内存泄漏追踪日志中寻找已知类型,并尝试弄清楚它们是否属于同一个被销毁的范围(=> 正在泄漏)或更高的范围(=> 没有泄漏)。

在顶部,我们看到:​​​​​​​

├─ com.bugsnag.android.Client instance│   Leaking: UNKNOWN

我们的 BugSnag 客户端是一个用于分析崩溃报告单例,由于每个应用我们创建一个实例,所以它没有泄漏。​​​​​​​

├─ com.bugsnag.android.Client instance│   Leaking: NO

所以我们现在需要转变焦点,特别关注从最后一个 Leaking: NO  到第一个 Leaking: YES  的部分:​​​​​​​

├─ com.bugsnag.android.Client instance│   Leaking: NO│   ↓ Client.breadcrumbState│               ~~~~~~~~~~~~~~~├─ com.bugsnag.android.BreadcrumbState instance│   Leaking: UNKNOWN│   ↓ BreadcrumbState.store│                       ~~~~~├─ com.bugsnag.android.Breadcrumb[] array│   Leaking: UNKNOWN│   ↓ Breadcrumb[494]│               ~~~~~├─ com.bugsnag.android.Breadcrumb instance│   Leaking: UNKNOWN│   ↓ Breadcrumb.impl│                   ~~~~├─ com.bugsnag.android.BreadcrumbInternal instance│   Leaking: UNKNOWN│   ↓ BreadcrumbInternal.metadata│                           ~~~~~~~~├─ com.example.MainActivity$1 instance│   Leaking: UNKNOWN│   Anonymous subclass of java.util.HashMap│   ↓ MainActivity$1.this$0│                       ~~~~~~╰→ com.example.MainActivity instance    Leaking: YES (Activity#mDestroyed is true)

BugSnag 客户端保持了一个面包屑的环形缓冲区。这些应该保留在内存中,它们也没有泄漏。

所以让我们跳过上述内容,从下面这里继续分析:​​​​​​​

├─ com.bugsnag.android.BreadcrumbInternal instance│   Leaking: NO

我们只需要关注从最后一个 Leaking: NO 到第一个Leaking: YES 的部分:​​​​​​​

├─ com.bugsnag.android.BreadcrumbInternal instance│   Leaking: NO│   ↓ BreadcrumbInternal.metadata│                           ~~~~~~~~├─ com.example.MainActivity$1 instance│   Leaking: UNKNOWN│   Anonymous subclass of java.util.HashMap│   ↓ MainActivity$1.this$0│                       ~~~~~~╰→ com.example.MainActivity instance    Leaking: YES (Activity#mDestroyed is true)
  • BreadcrumbInternal.metadata :内存泄漏追踪通过面包屑实现的元数据字段。

  • MainActivity$1 实例是 java.util.HashMap 的匿名子类:MainActivity$1  是在MainActivity 中定义的 HashMap  的匿名子类。它是从 MainActivity.java  中定义的第一个匿名类(因为是 $1 )。

  • this$0:每个匿名类都有一个隐式字段引用到定义它的外部类,这个字段被命名为 this$0 。

也就是说:记录到 BugSnag 的面包屑之一有一个元数据映射,这是一个 HashMap 的匿名子类 ,它保留对外部类的引用,这个外部类就是被销毁的 Activity 。

让我们看看我们在 MainActivity 中记录面包屑的地方:​​​​​​​

void logSavingTicket(String ticketId) {    Map<String, Object> metadata = new HashMap<String, Object>() {{      put("ticketId", ticketId);    }};    bugsnagClient.leaveBreadcrumb("Saving Ticket", metadata, LOG);}

这段代码利用了一个被称为“双括号初始化” 的有趣的 Java 代码块 。它允许你创建一个 HashMap,并通过添加代码到 HashMap 的匿名子类的构造函数中同时初始化它。​​​​​​​

new HashMap<String, Object>() {{    put("ticketId", ticketId);}};
Java 的匿名类总是隐式地引用其外部类。

因此,这段代码:​​​​​​​

void logSavingTicket(String ticketId) {    Map<String, Object> metadata = new HashMap<String, Object>() {{      put("ticketId", ticketId);    }};    bugsnagClient.leaveBreadcrumb("Saving Ticket", metadata, LOG);}

实际上被编译为:

class MainActivity$1 extends HashMap<String, Object> {
  private final MainActivity this$1;

  MainActivity$1(MainActivity this$1, String ticketId) {
     this.this$1 = this$1;
     put("ticketId", ticketId);
  }
}

void logSavingTicket(String ticketId) {
  Map<String, Object> metadata = new MainActivity$1(this, ticketId);
  bugsnagClient.leaveBreadcrumb("Saving Ticket", metadata, LOG);
}

结果,这个 breadcrumb 就一直持有对已销毁的 activity 实例的引用。

总结

尽管使用 Java 的双括号初始化看起来很"炫酷",但它会无故地额外创建类,可能会导致内存泄漏。因此避免在 Java 中使用双括号初始化。

你可以用下面这种更安全的方式来解决这个问题:​​​​​​​

Map<String, Object> metadata = new HashMap<>();metadata.put("ticketId", ticketId);bugsnagClient.leaveBreadcrumb("Saving Ticket", metadata, LOG);

或者利用 Collections.singletonMap() 进一步简化代码:​​​​​​​

Map<String, Object> metadata = singletonMap("ticketId", ticketId);bugsnagClient.leaveBreadcrumb("Saving Ticket", metadata, LOG);

或者,直接将文件转换为 Kotlin。

你是否在使用 Java 时遇到过内存泄漏的问题?

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值