Memory Leak 那一兩件事情

Jast Lai
Jastzeonic
Published in
11 min readMay 15, 2022

在我寫 Android 的職涯經歷中我經常碰到一件事情:

「你這東西不能傳進去裏頭當作 Local variable」

「這樣會造成 memory leak」

小時候我聽到一臉矇逼,前輩講講聽話就算了,但長大後,越想越不對勁,大家總說 memory leak ,那具體是甚麼意思?又是怎麼發生的?為什麼要避免呢?

Memory Leak 的定義

最近常常碰到一件事情,我常常講一個大家都知道的字眼、大家都懂的概念,但實際要我解釋這個字眼或者是這個概念的時候,就愣在那邊說不出半句話。

Memory leak 就是一個常見的例子,大家都知道是甚麼東西但大家都說不出來是甚麼東西,說大家可能有點誇張,但是說真的,知道這詞彙的意思但解釋不出來的人還真不少。

Memory leak 至少存在電腦科學當中一段時間了,有個叫做牛津語言(Oxford Languages)的地方給 Google Search 做了個定義

A failure in a program to release discarded memory, causing impaired performance or failure.

程式沒能成功釋放棄用的記憶體,造成效能的損失或是執行的失敗。

我自己是比較喜歡維基百科上的定義

In computer science, a memory leak is a type of resource leak that occurs when a computer program incorrectly manages memory allocations in a way that memory which is no longer needed is not released.

在電腦科學之中,記憶體洩漏是記憶體不再需要時未被釋放的不正確記憶體分配管理,是一種資源的流失。

每次看維基要翻譯成中文都覺得很困難。但這邊的詞彙提供了很重要的資訊,leak 是一個行為動作,我們可以想像,這個行為是一個容器有個破洞會漏東西出來,那 memory leak 漏的東西就是記憶體的空間,那容器是甚麼呢?這裡可以想像是 memory manager ,也就是說 memory manger 有流漏導致 memory 的洩漏浪費,故稱為 memory leak。

那必然會有某種程度的後果,這才會被稱為問題和錯誤嘛,那具體會發生甚麼事情呢?維基百科上是這樣說的:

A memory leak reduces the performance of the computer by reducing the amount of available memory. Eventually, in the worst case, too much of the available memory may become allocated and all or part of the system or device stops working correctly, the application fails, or the system slows down vastly due to thrashing.

Memory leak 會讓可用的記憶體變少導致電腦的效能變差。最終,最壞的狀況是,所有可用的記憶體都被占滿,導致系統或者裝置上所有的作業都沒法正常的運作,如應用程式失效、系統變慢或者是閃退等情況。

恩,這大家都知道,大家都覺得要避免的情況。

那 Memory leak 怎麼發生的?

要說這個,可以先從各種程式語言的老前輩 C 語言開始說起。

如果我們有寫過 C 應該就知道 C 的 standard library (stdlib.h)有個叫做 malloc 的 method ,字面意思上就可以看得出來他是用來分配記憶體的。

那如果在使用了 malloc ,程式執行完了,沒做啥其他的事情,就會 leak。那就是寫 C 時要有的習慣,寫完記得要 free()。

但問題是,人非聖賢,事無萬全。配置完記憶體用完後忘記釋放這種事情屢見不鮮 …

那麼是不是有這麼一套規則,可以 malloc 完,卻不用 free 便可以自行釋放記憶體的呢?

沒錯,這便是 Garbage Collection (GC)。

但是有 GC 具體做了甚麼事情,可以 malloc 後不需要 free 樣呢?

以 JVM 來說,一個 Application 會把需要配置 memory 的 object 放進一個 heap 當中進行管理,那麼當這個 object 不再被 Reference 到,那麼該物件就會被視為不被用到而被回收。

那具體是誰 Reference 到這個 object 呢?

這個情況大致上可以分成兩種,我認為概念上最好理解的應該就是 Reference count 了。

Reference count 的概念就是當一個 object 被 Reference 到,則 count 就會 +1,當 object 被 release ,則 count -1 ,當 count 等於 0 時,則物件立刻被釋放,Reference count 是顯而易見的,簡單,而且動作相當快,但缺點是碰到 Circular reference 則需要手動回收否則就會 memory leak 。當 A object Reference 到 B object , B object Reference 到 A object 時,A B 兩個 count 都是 1,誰也不讓誰,結果就是若沒有手動回收,則會 memory leak。

那像是 Objective-C 、PHP 是以 Reference count 作為 GC 機制。

但回過頭來看, JVM ,或者說我最熟悉的 Android 的 Dalvikvm 這個方式嗎?

很顯然不是,前面有提到一點, Reference Count 應該不會太耗時間,反正碰到 object 的 Reference Count 為 0 時,就回收給你看,其他的我不管了。這樣所需的時間複雜度和空間複雜度應該都不多,這跟我們對 Java 或者是 Android 下達 GC 時,會很耗資源的印象不一樣。

咦?等等,這是在說,在 Android 上 circular reference 不會 memory leak 嗎?

恩,還真不會。

那肯定有個條件,有個條件是可以判斷該 object 是能被回收亦或是不能被回收的。

答案是 GC Roots 。

也就是說我們可以把 Object reference 關係想像成一棵樹(tree),或者是一張圖(Graph),是樹的話則會有一個根(root),是圖的話則會有一個起始點 start point 。那麼只要該物件的關聯直接或間接連到那個根,則不能被回收。

所以要怎麼知道了,就是遍歷做標記啦。

自 GC Roots 開始開始遍歷,標記所有有直接或間接被 GC Roots 參照到的 object
遍歷完後把沒有被 GC Roots 參照到的 object 給釋放掉

簡單的概念。這也是最常見的 GC 方式,乃至於提到 GC 原則上都是泛指這種方法。

當然這個問題是顯而易見的。因為需要遍歷,這意味著,每次遍歷都至少需要 O(n) 的時間複雜度,那若是使用的 memory 越多, n 就越大,每次遍歷就會越久,則 GC 的代價越大。

那問題來了,那具體的 GC Roots 是甚麼?

可以參考 Eclipse 的文件

只是仔細看會發現,很多個好像都不常碰到。

要注意到,滿足被 GC 回收的條件是不被 GC Roots Reference 到,而不是沒 Reference 到 GC Roots。

細看在 GC Roots 的種類,真的要達到 memory leak 還真的不簡單,如果不是沒事把東西丟去給系統層 Reference ,那大多數情況都可以被 GC。

那我們最常看到的 Memory leak 例子是 Running Thread 也就是異步時,worker thread 與 main thread 的生命週期錯開。

上面那個程式碼,跑了會碰到 NullPointerException

這我看到其實就很納悶了,異步執行導致狀態不如程式執行的預期,這問題不是叫做 Race condition 嗎?

所以我認為這比較偏向是 Race condition 的問題,比較不偏向是 memory leak。當然要說 thread 應該在 sample 被 assign 為 null 時就應該被釋放,但實際上卻沒被釋放,所以視為 memory leak 也是一個解釋。但如果這麼做是必要的話,解決問題的點應該是聚焦在 object 的狀態管理,而不是記憶體的釋放,因為這情況在多執行序的程式執行的程序當中每天都在發生。(當然不是說這樣寫就不用管記憶體,那還是得取決於你的記憶體怎麼用)。

我認為更符合 memory leak 條件的狀況應該是這樣:

這個執行的情況比較尷尬的是,執行中的 Thread 毫無疑問是被 GC Roots Reference 的 (不然跑一半給回收這更尷尬了),Thread 裏頭的 while 若是沒給任何中斷,則會無限 Run 下去,那麼尷尬的是因為這個 Thread Reference 到了 main 裏頭的 sample,導致 sample 的 instance 沒辦法被 GC ,明明他都已經 Run 完了,卻還是回收不掉,更過分的是這還會導致你的 Process 沒法被執行完成,積少成多,一塊銀用一粒汗來換,系統變慢了,最後終於把系統配給能用的記憶體給灌爆了,人人都恐懼的 Out of Memory 發生了。

這就很符合我一開始定義的條件了,這才是 memory leak 的起承轉合。這個尤其在跨層 IO 時很常發生,所以對外 IO 時要確定是否關閉,否則把自己的物件傳進去可能會被對方永遠留著。

結語

每次提到 memory leak 就好像提到佛地魔似的,大家都唯恐避之不及,一句話這樣會 memory leak ,所以就不這樣寫了。

我覺得最有意思的地方是,我在 Android 碰到大多被認定為 memory leak 的問題大多都跟異步有關,且到最後異步的執行序若是有執行完是都可以被回收的,而且產生敘述的問題也大多比較偏向是 Race condition。當然這可能跟 LeakCanary 或者是 AS 的工具判定有關,以 LeakCanary 來說,他的判定條件是:

以 Activity 的來說,若 Activity 被 finish ,那他應該是要可以被被回收,但是因為他被 worker thread reference 到,間接給 GC Roots Reference 到,那便不能回收,那對 LeakCanary 來說,這東西便是 leak 了,畢竟這些工具不知道這個 Thread 何時會結束,也許等個五秒,但超過了,他就當作不合理應該被回收,於是判定是 leak,於是便有了這個訊息。

如果有任何問題,或是看到寫錯字,請不要吝嗇對我發問或對我糾正,您的回覆和迴響會是我寫文章最大的動力。

參考資料

--

--

Jast Lai
Jastzeonic

A senior who happened to be an Android engineer.