【編者按】本文介紹了一個使用了 JAVA 的雙括號初始化語法導致內存泄漏的案例。作者分析了泄漏的原因,提出了幾種解決的方法,并給出了代碼示例。
鏈接:https://blog.p-y.wtf/avoid-java-double-brace-initialization
作者 | Pierre-Yves Ricau責編 | 明明如月
責編 | 夏萌
出品 | CSDN(ID:CSDNnews)

結論先行
避免像這樣,在 Java 中使用雙括號初始化:
newHashMap< String, String> {{ put( "key", value); }};內存泄漏追蹤
我最近正在 LeakCanary看到了以下內存泄漏追蹤信息:
┬─── │ GC Root: Global variable innativecode │ ├─ 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$ 1instance │ Leaking: UNKNOWN │ Anonymous subclass of java.util.HashMap │ ↓ MainActivity$ 1. this$ 0 │ ~~~~~~ ╰→ com.example.MainActivity instance Leaking: YES (Activity#mDestroyed istrue)當打開一個內存泄漏追蹤日志時,我首先會看底部的對象,了解它的生命周期,這將幫助我理解內存泄漏追蹤中的其他對象是否應該有相同的生命周期。
在底部,我們看到:
???????╰→ com.example.MainActivityinstance Leaking: YES( Activity#mDestroyedistrue)Activity已經被銷毀,應該已被垃圾回收器給回收掉了,但它仍駐留在內存中。
此時,我開始在內存泄漏追蹤日志中尋找已知類型,并嘗試弄清楚它們是否屬于同一個被銷毀的范圍(=> 正在泄漏)或更高的范圍(=> 沒有泄漏)。
在頂部,我們看到:
???????├─ com.bugsnag.android.Clientinstance │ Leaking: UNKNOWN我們的 BugSnag客戶端是一個用于分析崩潰報告單例,由于每個應用我們創建一個實例,所以它沒有泄漏。
???????├─ com.bugsnag.android.Clientinstance │ 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 $1instance │ 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.BreadcrumbInternalinstance │ Leaking: NO我們只需要關注從最后一個 Leaking: NO到第一個Leaking: YES的部分:
… ├─ com.bugsnag.android.BreadcrumbInternal instance │ Leaking: NO │ ↓ BreadcrumbInternal.metadata │ ~~~~~~~~ ├─ com.example.MainActivity $1instance │ Leaking: UNKNOWN │ Anonymous subclass of java.util.HashMap │ ↓ MainActivity $1.this $0 │ ~~~~~~ ╰→ com.example.MainActivity instance Leaking: YES (Activity #mDestroyed is true)- BreadcrumbInternal.metadata :內存泄漏追蹤通過面包屑實現的元數據字段。
也就是說:記錄到 BugSnag 的面包屑之一有一個元數據映射,這是一個 HashMap的匿名子類 ,它保留對外部類的引用,這個外部類就是被銷毀的 Activity 。
讓我們看看我們在 MainActivity中記錄面包屑的地方:
voidlogSavingTicket( StringticketId) { Map< String, Object> metadata = newHashMap< String, Object> {{ put( "ticketId", ticketId); }}; bugsnagClient.leaveBreadcrumb( "Saving Ticket", metadata, LOG); }這段代碼利用了一個被稱為“雙括號初始化” 的有趣的 Java 代碼塊 。它允許你創建一個 HashMap,并通過添加代碼到HashMap的匿名子類的構造函數中同時初始化它。
newHashMap< String, Object> {{ put( "ticketId", ticketId); }};Java 的匿名類總是隱式地引用其外部類。
因此,這段代碼:
voidlogSavingTicket( StringticketId) { Map< String, Object> metadata = newHashMap< String, Object> {{ put( "ticketId", ticketId); }}; bugsnagClient.leaveBreadcrumb( "Saving Ticket", metadata, LOG); }實際上被編譯為:
???????classMainActivity$1 extendsHashMap< String, Object> { private final MainActivity this$ 1;MainActivity$ 1(MainActivity this$ 1, StringticketId) { this.this$ 1= this$ 1; put( "ticketId", ticketId); }}
voidlogSavingTicket( StringticketId) { Map< String, Object> metadata = newMainActivity$ 1( this, ticketId); bugsnagClient.leaveBreadcrumb( "Saving Ticket", metadata, LOG); }
結果,這個 breadcrumb 就一直持有對已銷毀的 activity 實例的引用。
總結
盡管使用 Java 的雙括號初始化看起來很"炫酷",但它會無故地額外創建類,可能會導致內存泄漏。因此避免在 Java 中使用雙括號初始化。
你可以用下面這種更安全的方式來解決這個問題:
???????Map< String, Object> metadata = newHashMap<>; 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 時遇到過內存泄漏的問題?






