內存泄漏分析和開發注意點

概要

Android的程序由Java語言編寫,所以Android的內存管理與Java的內存管理相似。程序員通過new爲對象分配內存,所有對象在java堆內分配空間;然而對象的釋放是由垃圾回收器來完成的。C/C++中的內存機制是“誰污染,誰治理”,java的就比較人性化了,給我們請了一個專門的清潔工(GC)。

深入理解 Java 垃圾回收機制

Java GC回收算法

介紹

Java中的內存回收全交由GC回收器,程序員無法手動釋放內存,可用幾個函數訪問GC,如`System.gc()`

通知GC回收器開始執行。不同額JVM虛擬機實現不同,所以效果有限。通常Java中GC線程優先級較弱。

​ CG回收爲使用對象佔用的內存資源時,會想判斷此對象是否處於存活狀態,有沒有被其他對象引用或關聯,如果沒有,則回收。內存溢出就是因爲對象已經使用完畢,缺沒有斷掉對此對象的引用,GC回收器則沒有回收此對象佔用的內存資源。

對象存活分析

​ 不同的Java虛擬機會採用不同的判斷機制,Java採取的是可達性分析算法。

引用計數法

~
在堆中存儲對象A時,在對象A頭處維護一個counter計數器,如果一個對象引用了對象A,則將A的counter++,引用失效則counter--。若 counter == 0 則對象已被廢棄,可回收。
~

此邏輯無法解決對象A、B相互持有,類似死鎖循環的情況。

可達性分析

Java使用可達性分析來判斷對象是否存活。

示例圖
~
通過一組 GC Roots 對象作爲引用關係的起點,往下搜索經過的路徑稱爲引用鏈(Reference Chain),當一個對象到GC Roots不與任何引用鏈相連時,則判定此對象到GC Roots不可達,是可回收對象。
~

可做GC Roots的對象:

  • 虛擬機棧的的引用對象
  • 方法區中的類靜態屬性的引用對象
  • 方法區中的常量的引用對象
  • 本地方法棧中JNI的引用對象

關於的虛擬機棧方法區本地方法棧的理解可看 拓展知識 - Java虛擬機內存分區

內存泄漏原因

簡單瞭解Java GC回收算法後,便知道內存泄漏的根本原因:

GC回收工作時,應當回收的未使用對象通過了對象存活判斷,確認爲存活狀態。於是對此未使用對象不做回收處理。此種情況不斷髮生,積累未回收對象到一定量,就會拋出OOM內存溢出異常。

Android內存泄漏常見場景

關鍵在於對象之間的生命週期長短。

  • 資源對象未關閉
    • BroadcastReceiver,ContentObserver、File、Cursor、Socket、Bitmap等
  • 靜態對象
    • 集合類HashMap、Vector等,不及時setnull會一直持有對象
    • 靜態的View、Activity。View默認持有當前Activity的引用
    • 鍵盤焦點,InputMethodManager
    • 當Activity有View獲取鍵盤焦點
    • 在Activity銷燬後View會被InputMethodManagerhold
    • View --hold--> Activity對象,造成泄漏
  • 監聽器對象及時移除
    • addxxxListener 等,一般情況:靜態對象 --hold--> listener實例 --hold--> Activity對象
  • 內部類
    • 匿名內部類持有外部類引用
    • 匿名對象進行異步任務時,可能產生泄漏
    • 當Activity回收時停止異步任務
    • 非靜態內部類:持有外部類引用
    • Handler是串行處理任務的,當Activity回收時,Looper仍然有消息未處理完畢時會發生泄漏。
    • 因爲Looper使用ThreadLocal實例保存,此實例對象是靜態的。
    • Looper --hold--> MessageQueue --hold--> Handler (msg.target) --hold--> Activity
    • 建議使用WeakReference 弱引用
  • WebView在主線程中使用
    • 爲Webview新開線程,通過AIDL與主線程通信

檢測工具

  • MemoryMonitor:隨時間變化,內存佔用的變化情況

    • LeakCanary:實時監測內存泄漏的庫
  • MAT:輸入HRPOF文件,輸出分析結果

    • Histogram:查看不同類型對象及其大小
    • DominateTree:對象佔用內存及其引用關係
    • MAT使用教程

拓展知識

Java虛擬機內存分區

程序計數器

  • 線程私有
  • 沒規定OOM情況

在虛擬機的概念模型中,字節碼解釋器工作就是通過改變計數器的值,來選取下一條需要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴這個計數器來完成。

Java虛擬機內多線程是通過線程輪流切換並分配處理器時間的方式實現的,一個處理器(多核處理器一個內核)在一個確定的時刻只能執行一個線程的任務。線程反覆切換並且恢復到正確的執行位置,每條線程都需要一個獨立的程序計數器。

如果線程執行的是Java代碼,計數器記錄的是正在執行的虛擬機字節碼指令的地址;如果執行的是Native方法,計數器爲空(Undefined)。

虛擬機棧

  • 線程私有
  • StackOverflowError
  • OutOfMemoryError

Java內存分區經常被程序員劃分爲棧內存堆內存,這種劃分很粗躁,表明了程序員最關心的部分。

這常說的棧內存相當於虛擬機棧中的局部變量表部分。

局部變量表

存放了編譯期可知的各種基本類型對象引用returnAddress類型

  • 基本類型:boolean、byte、char、int等
  • 對象引用:對象起始地址的引用指針、代表對象的句柄、其他與此對象相關的位置
  • returnAddress:字節碼指令地址

線程請求棧深度大於虛擬機的允許深度,會拋出StackOverflowError。目前大多數虛擬機棧可以動態擴展,當擴展時無法申請到足夠內存,會拋出OutOfMemoryError。

操作數棧

操作數棧用於存放JVM從局部變量表複製的常量或變量,提供讀取、結果入棧,也用於存放調用方法所需要的參數以及接受方法返回的結果。

  • 後進先出LIFO
  • 最大深度由編譯期確定
  • 可以存放JVM定義的任意數據類型的值
  • 基本類型long、double佔用兩個深度,其他佔用一個深度
動態連接
  • 棧幀都持有運行時常量池中該棧幀所屬方法的引用
  • 持有該引用是爲了支持方法調用過程中的動態連接

Class文件的常量池存在大量符號引用,字節碼(Java)中的方法調用指令

~
常量池中 指向方法的符號引用 作爲參數
~

這些符號引用分爲兩個部分:

  • 靜態解析:在類加載階段或首次使用時 轉化爲直接引用如staitc/final
  • 動態連接:在運行期間轉化爲直接引用

本地方法棧

  • 線程私有

和虛擬機棧相似。虛擬機棧爲虛擬機執行 Java方法(字節碼) 服務,本地方法棧爲虛擬機執行Native方法服務,也會拋出StackOverflowError、OutOfMemoryError異常。

虛擬機規範中強制規定本地方法棧使用的語言使用方法數據結構,也有虛擬機 Sun HotSpot直接將本地方法棧和虛擬機棧合併。

任何本地方法接口都會使用本地方法棧。

調用Java方法時,虛擬機會創建一個新的棧幀並壓入虛擬機棧中。

此Java方法--調用-->本地方法時,虛擬機棧保持不變,只是簡單的動態連接並直接調用指定的本地方法。

示例圖

圖中流程:

  1. 調用了兩個Java方法
  2. 第二個Java方法調用了本地方法
    1. 假設本地方法棧是個C語言棧
  3. 第一個C函數調用第二個C函數
  4. 第二個C函數調用第三個Java方法
  5. 第三個Java方法又調用一個Java方法

JVM堆

  • 線程共有
  • 主要用於存儲對象

在虛擬機啓動時創建的內存區域,線程共享。唯一的目的就是存放對象實例。

Java堆是垃圾回收器管理的主要區域,很多時候被稱爲”GC堆”。

Java堆可以處於物理上不連續的內存空間。

Java堆內存可拓展主流實現方式,當堆中沒有內存完成實例分配,且堆無法拓展時,拋出OOM

方法區

  • 線程共有
  • 存儲類信息:全限定名、類型接口\類、訪問修飾符
  • 存儲常量、靜態變量
  • 即時編譯後的代碼等數據

Java虛擬機規範中描述方法區爲堆的邏輯部分,但它有個實際的名字非堆,用來區分Java堆。

方法區實現也肯能在堆區。

Java支持方法重載(符號毀滅)和方法重寫(多肽、動態派發)

~
爲了處理好動態派發,一種可能的實現就是專門開闢一個區,單獨管理所有方法。
按照穩定性給對象方法進行排序,聚集類似方法。
調用方法時在方法區搜索一次定位到相同方法起始位置。
~

減少GC開銷的措施

  • 不要顯式調用System.gc()
    • 此函數建議JVM進行主GC,雖然只是建議而非一定,但很多情況下它會觸發主GC,從而增加主GC的頻率,也即增加了間歇性停頓的次數。
  • 儘量減少臨時對象的使用
    • 臨時對象在跳出函數調用後,會成爲垃圾,少用臨時變量就相當於減少了垃圾的產生,從而延長了出現上述第二個觸發條件出現的時間,減少了主GC的機會。
  • 對象不用時最好顯式置爲Null
    • 一般而言,爲Null的對象都會被作爲垃圾處理,所以將不用的對象顯式地設爲Null,有利於GC收集器判定垃圾,從而提高了GC的效率。
  • 儘量使用StringBuffer,而不用String來累加字符串
    • 使用String進行大量繁瑣字符串拼加時,會產生大量臨時對象,使剩餘內存空間碎片話。此時剩餘內存總量不少,但可能無法分配出滿足指定大小的剩餘內存區間,產生內存抖動現象,頻繁GC佔用過多硬件資源,造成卡頓,甚至出現OOM。
  • 謹慎使用靜態變量
    • 靜態變量屬全局變量,不進行GC回收,它持有的對象也不會回收。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章