Android性能優化(1):常見內存泄漏與優化(一)


 衆所周知,Java因其擁有獨特的虛擬機(JVM)設計,使其成爲一門跨平臺、內存自動管理的高級開發語言。所謂跨平臺,即"一次編譯,多次運行",從而解決了不同平臺由於編譯器不同導致無法運行問題;所謂內存自動管理,即Java不像C/C++那樣需要開發者來分配、釋放內存,它擁有一套垃圾回收機制來管理內存,這套機制減輕了很多潛在的內存回收不當問題。然而,雖然Java的垃圾回收機制非常優秀,但當我們在寫程序過程中有一些不好的習慣可能會導致部分內存無法被垃圾回收器回收,而我們自己又無法進行回收,從而導致這部分內存長期被佔用無法釋放,並且隨着這部分內存的增大,極大的影響了程序的性能,這種情況被稱之爲“內存泄漏”。

1. Java虛擬機(JVM)

虛擬機是一種虛構出來的抽象化計算機,是通過在實際的計算機上仿真模擬各種計算機功能來實現的,它擁有自己完善的虛擬硬件架構,如處理器、堆棧、寄存器等,而且還具有相應的指令系統。Java虛擬機就是這麼一種虛擬機。Java虛擬機,即Java Virtual Machine(JVM),是運行所有Java程序的抽象計算機,是Java語言的運行環境,它屏蔽了與具體操作系統平臺相關的信息,使得Java程序只需生成在Java虛擬機上運行的目標代碼(字節碼)。任何平臺只要裝有針對於該平臺的Java虛擬機,字節碼文件(.class)就可以在該平臺上運行,即"一次編譯,多次運行",正是因爲如此,從而使得Java語言具有跨平臺移植的特性。

 Java虛擬機本質上就是一個程序,Java程序的運行依靠具體的Java虛擬機實例,每個Java應用程序都對應着一個Java虛擬機實例,且Java程序與其對應的Java虛擬機實例生命週期一致。在Java虛擬機規範中,JVM主要包括五大模塊,即類裝載器子系統運行時數據區、執行引擎、本地方法接口和垃圾收集模塊。其中,類加載器子系統,用於加載字節碼文件到內存,就是JVM中的runtime data area(運行時數據區)的method area方法區,整個過程中裝載器只負責文件結構格式能夠被裝入,並不負責能不能運行;運行時存儲區,即JVM內存區域,JVM運行程序的時候使用;執行引擎,在不同的虛擬機實現裏面,執行執行引擎可能會有解釋器解釋執行字節碼文件或即時編譯器編譯產生本地代碼執行字節碼文件,可能兩種都有;本地方法接口,即Java Native Interface(JNI),用於與本地庫(native library)交互,是Java與其他編程語言(C/C++)交互的"橋樑";垃圾收集,用於對已分配的內存資源進行回收,主要是Java堆和方法區的內存。JVM架構如下圖所示:

1.1 JVM內存管理

 Java虛擬機在執行Java程序時會把它所管理的內存劃分若干個不同的數據區域,這些區域的用途各不相同,創建、銷燬的時間也各有區別,比如有的隨着Java虛擬機進程的啓動而存在、有的區域則依賴於用戶線程的啓動和結束而創建、銷燬,但它們有一個共同的“名字”,即運行時數據區域。Java虛擬機管理的內存主要有:程序計數器Java虛擬機棧本地方法棧Java堆方法區以及直接內存等,其中,程序計數器、虛擬機棧和本地方法棧爲線程私有,方法區和Java堆爲線程間共享

  • 程序計數器

 程序計數器(Program Counter Register)是內存中的一塊較小的區域,它可以看作成是當前線程所執行的字節碼行號指示器,依賴於用戶線程的啓動和結束而創建、銷燬,是線程私有內存數據區域。由於Java虛擬機的多線程是通過線程輪流切換並分配處理器執行時間的方式來實現的,在任何一確定的時刻,一個處理器都只會執行一條線程中的指令,因此,爲了線程切換後能恢復到正確的執行位置,每條線程都需要有一個獨立的程序計數器,各個線程之間計數器互不影響、獨立存儲。需要注意的是,如果線程正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;如果線程正在執行的是Native方法,那麼這個計數器的值爲空。

在Java虛擬機規範中,程序計數器是唯一一個沒有規定任何OutOfMemoryError情況的區域。

  • Java虛擬機棧

 類似於程序計數器,Java虛擬機棧(Java Virtual Machine Stacks)也是線程私有,生命週期與用戶線程週期相同,它描述的是Java方法執行的內存模型,即每個Java方法在執行時JVM會爲其在這部分內存中創建一個棧幀(Stack Frame)用於存儲局部變量表操作數棧動態鏈接以及方法出口信息等,每一個方法從調用到執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧和出棧過程。局部變量表是我們在開發過程中接觸較多的部分,它存放了編譯器可知的各種基本數據類型(byte/boolean/char/int/short/long/float/double)對象引用(reference類型)returnAddress類型,其中,64位長度的long和double類型的數據佔用2個局部變量空間,其他的類型佔1個(4個字節)。局部變量表所需的內存空間在編譯期間完成分配,當進入一個方法時,這個方法需要在幀中分配多大的局部變量空間是完成確定的,在方法運行期間不會改變局部變量表的大小。下圖是虛擬機棧存儲示意圖

在Java虛擬機規範中,虛擬機棧可能會出現兩種異常情況,即StackOverflowErrorOufOfMemoryError,其中,StackOverflowError出現在如果線程請求的棧深度大於虛擬機所允許的深度OufOfMemoryError出現在如果虛擬機棧可以動態擴展,但是擴展後仍然無法申請到足夠的內存

  • 本地方法棧

 本地方法棧(Native Method Stack)與虛擬機棧所發揮的作用非常相似,它們之間的區別不過是虛擬機棧爲虛擬機執行Java方法(即字節碼),而本地方法棧則爲虛擬機使用到的Native方法服務。下圖演示了一個線程調用Java方法和本地方法時的棧,以及虛擬機棧和本地方法棧之間毫無障礙的跳轉。示意圖如下:

在Java虛擬機規範中,本地方法棧也會出現StackOverflowErrorOufOfMemoryError異常情況。

  • Java堆

  Java堆(Java Heap)是Java虛擬機所管理的內存中最大的一塊,它在JVM啓動時被創建,生命週期與JVM相同,是被所有線程所共享的一塊區域,此區域唯一的目的是存放對象實例數組,幾乎所有的對象實例都在這裏分配內存。Java堆是垃圾收集器管理的主要區域,也是"內存泄漏"集中出現的地方。由於JVM中的垃圾收集器大部分採用分代收集算法,因此,Java堆又被細分爲:新生代和老年代,其中,新生代區域存放創建不久的對象,老年代存放經歷過多次GC後仍然存活的對象。實際上,根據JVM規範,Java堆還可被繼續細分爲Eden空間、From Survivor空間以及To Survivor空間等,這個我們在垃圾回收機制模塊詳細闡述。下圖是Java堆內存劃分示意圖:

在JVM規範中,如果堆可以動態擴展,但是擴展後仍然無法申請到足夠的內存,就會拋出OutOfMemoryError異常。當然,我們可以通過-Xmx-Xms來控制堆內存的大小,其中,-Xmx用於設置Java堆起始的大小,-Xms用於設置Java堆可擴展到最大值。

  • 方法區

 像Java堆一樣,方法區(Method Area)是各個線程共享的內存區域,它的生命週期與虛擬機相同,即隨着虛擬機的啓動和結束而創建、銷燬。方法主要用於存放已被虛擬機加載的類信息、常量、靜態變量以及即時編譯器(JIT)編譯後的代碼等數據,它的大小決定了系統能夠加載多少個類,如果定義的類太多,導致方法區拋出OutOfMemoryError異常。需要注意的是,對於JDK1.7來說,在HotSpot虛擬機中方法區可被理解爲"永久區",但是JDK1.8以後,方法區已被取消,替代的是元數據區。元數據區是一塊堆外的直接內存,與永久區不同,如果不指定大小,默認情況下在類加載時虛擬機會盡可能加載更多的類,直至系統內存被消耗殆盡。當然,我們可以使用參數-XX:MaxMetaspaceSzie來指定元數據區的大小。

運行時常量池用於存放編譯期生成的各種字面量和符號引用。

  • 直接內存

 直接內存(Direct Memory)不是虛擬機運行時數據區的一部分,也不是Java虛擬機規範中定義的內存區域,它是在JDK1.4中新加入的NIO(New Input/Output)類,通過引入了一種基於通道與緩衝區的I/O方式,使用Native函數庫直接分配得到的堆外內存。對於這部分內存區域,主要通過存儲在Java堆中的DirectByteBuffer對象作爲這塊內存的引用進行操作,直接內存的存在避免了在Java堆和Native堆中來回複製數據,從而在某些場景能夠顯著地提高性能。

本機直接內存的分配不會受到Java堆大小的限制,但是仍然會受到本機總內存(包括RAM以及SWAP或者分頁文件)大小以及處理器尋址空間的限制。如果申請分配的內存總和(包括直接內存)超過了物理內存的限制,就會導致動態擴展時出現OutOfMemoryError異常。

1.2 垃圾回收器與內存分配策略

 在上一節中我們詳細分析了JVM的運行時內存區域,瞭解到程序計數器、虛擬機棧、本地方法棧是線程的私有區域,當線程結束時這部分所佔內存資源將會自動釋放,而線程的共享區域Java堆是存放所有對象實體的地方,因此是垃圾回收器回收(GC,Garbage Collection)的主要區域。(方法區也會有GC,但是一般我們討論的是Java堆)

1.2.1 如何判斷對象"已死"?

 垃圾收集器在回收一個對象,第一件事就是確定這些對象之中有哪些是“存活”的,哪些已經“死亡”,而垃圾回收器回收的就是那些已經“死亡”的對象。如何確定對象是否已經死亡呢?通常,我們可能會說當一個對象沒被任何地方引用時,就認爲該對象已死。但是,這種表述貌似不夠準確,因此,JVM規範中給出了兩種判斷對象是否死亡的方法,即引用計數法可達性分析

  • 引用計數法

 引用計數法實現比較簡單,它的實現原理:給對象一個引用計數器,每當一個地方引用它時,計數器就加1;當引用失效時,計數器值就減1;任何時刻計數器爲0的對象就會被認爲已經死亡。客觀地說,引用計數器法效率確實比較高,也容易實現,但是它也有不足之處,就是無用對象之間相互引用的問題,這種情況的出現會導致相互引用的對象均無法被垃圾回收器回收。
在這裏插入圖片描述

  • 可達性分析

 爲了解決引用計數法的無用對象循環引用導致無法被回收情況,JVM中又引入了可達性分析算法來判斷對象是否存活,這種算法也是普遍被應用的方式。可達性分析基本思想:通過一系列被稱爲"GC Roots"的對象作爲起始點,從這些節點開始向下搜索,搜索走過的路徑稱爲引用鏈。當一個對象到GC Roots沒有任何引用鏈相連接時,則證明此對象不可用,即被判斷可回收對象。可達性分析算法示意圖如下圖所示:

 那麼,哪些對象可以作爲"GC Roots"呢?

  • 虛擬機棧中(局部變量表)引用的對象;
  • 方法區中類靜態屬性引用的對象;
  • 方法區中常量引用的對象;
  • 本地方法棧中Native方法引用的對象;
1.2.2 垃圾收集算法

 前面我們通過引用計數法可達性分析找到了哪些對象是可以被回收的,本節將重點闡述JVM中的垃圾回收器是如何將這些不可用對象進行回收,即垃圾收集算法,主要包括標記-清除算法複製算法標記-整理以及分代收集等。相關介紹如下:

  • 標記-清除算法

 標記-清理算法是最基礎的垃圾收集算法,它的實現分爲兩個階段,即“標記”“清除”,其中,標記的作用爲通過引用計數法或可達性分析算法標記出所有需要回收的對象;清除的作用爲在標記完成後統一回收所有被標記的對象。這種算法比較簡單,但是缺陷也比較明顯,主要表現爲兩個方面:一是標記和清理的效率比較低;二是標記清理之後會產生大量不連續的內存碎片,空間碎片太大可能會導致以後在程序運行過程中需要分配較大對象時,無法找到足夠的連續內存而不得不觸發另一次GC。標記-清除算法執行過程如下圖所示:

  • 複製算法

 爲了解決標記-清理算法效率不高問題,人們提出了一種複製算法,它的基本原理:將可用內存容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活的對象複製到另一塊上,然後再把已使用的內存空間一次性清理掉。這種方式實現簡單,運行高效,且緩解了內存碎片的問題,但是由於其只對整個半區進行內存分配、回收,從而導致可使用的內存縮小爲整個內存的一半。複製算法執行過程如下圖所示:

在HotSpot虛擬機中,整個內存空間被分爲一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden空間和其中的一塊Survivor空間。當回收時,將EdenSurvivor中還存活着的對象一次性複製到另一塊Survivor空間上,最後清理掉Eden和剛纔用過的Survivor空間。HotSpot虛擬機默認EdenSurivor的大小比例爲8:1:1,也就是每次新生代中可用內存空間爲整個內存空間的90%,這就意味着有剩餘的10%不可用。當Survivor空間不夠用時,就需要依賴其他內存(老年代)進行分擔擔保。

新生代是指剛創建不久的對象;老年代指被多次GC仍然存活的對象。

  • 標記-整理算法

 雖說複製算法有效地提高了標記-清除算法效率不高問題,但是在對象存活率較高的情況下,就需要進行較多的複製操作(複製對象),尤其是所有對象都100%存活的極端情況,這種復r制算法效率將會大大降低,因此,老年代區域通常不會直接選用這種算法。根據老年代的特點,有人提出了標記-整理算法,該算法基於標記-清除算法發展而來,其中,標記同標記-清除算法一致,整理將所有存活的對象都向一端移動,然後直接清理掉端邊界以外的內存。標記-整理算法執行過程如下圖所示:

  • 分代收集算法

 分代收集算法是目前大部分虛擬機的垃圾收集器採用的算法,這種算法的思想是根據對象的存活週期的不同將Java堆內存劃分爲幾塊,即新生代區域和老年代區域,然後對不同的區域採用合適的算法。由於新生代每次GC時都會有大批對象死去,只有少量的對象存活,因此通常選用複製算法;而老年代中因爲存活率高、沒有額外空間對它進行分配擔保,就必須使用"標記-清理“或”標記-整理"算法進行回收。分代收集算法模型如下圖所示:

哪些對象能夠進入老年代?

  • 大對象;
  • 每次Eden進行GC後對象年齡加1進入Survivor,對象年齡達到15時進入老年代;
  • 如果Survivor空間中相同年齡所有對象大小的總和大於survivor空間的一半,年齡大於等於該年齡的對象就直接進入老年代。
  • 如果survivor空間不能容納Eden中存活的對象,由於擔保機制會進入老年代。如果survivor中的對象存活很多,擔保失敗,那麼會進行一次Full GC。

什麼是Minor GC、Major GC和Full GC?

  • Minor GC從新生代空間(Eden和Survivor區域)回收內存;
  • Major GC是清理永久代;
  • Full GC是清理整個堆內存空間,包括新生代和永久代。
1.2.3 內存分配與回收策略

 Java的自動內存管理歸結於兩方面,即爲對象分配內存回收分配給對象的內存,其中,在上一小節中我們詳細闡述了回收內存的具體細節,這裏不再討論。對於對象的內存分配,實際上就是在Java堆中爲對象分配內存,準確來說是在新生代的Eden區上,如果啓動了本地線程分配緩衝,將按線程優先在TLAB上分配,並且,少數情況下也可能會直接分配在老年代中。總之,JVM中對對象內存的分配不是固定的模式,其細節取決於使用哪種垃圾收集器,和虛擬機中與內存相關的參數設置。常見的內存分配策略:

  • 對象優先在Eden分配

 大多數情況下,對象在Java堆的新生代Eden區中分配,當Eden區沒有足夠空間進行分配時,虛擬機將發起一次Minor GC

  • 大對象直接進入老年代

 所謂的大對象是指需要大量連續內存空間的Java對象,最典型的大對象就是那種很長的字符串以及數組。對於內存分配來說,大對象也是一個很棘手的東西,尤其是“短命大對象”,經常出現在內存空間還較多的情況下,大對象直接導致提前出發垃圾收集器以獲取足夠的連續空間來“安置”它們。

虛擬機提供了一個-XX:PretenureSizeThreshold參數,使得大於這個設置值得對象直接在老年代內存區域分配,這樣做的目的在於避免在Eden區及兩個Survivor區之間發生大量的內存複製。

  • 長期存活的對象將進入老年代

 虛擬機給每個對象定義了一個對象年齡(Age)計數器,如果對象在Eden出生並經過第一次Minor GC後仍然存活,並且能被Survivor容納的話,將被移動到Survivor空間中,並且對象年齡設爲1.對象在Survivor區中每“熬過”一次Minor GC,年齡就增加1歲,當它的年齡增加到一定程度(默認15歲),就將會被晉升到老年代中。虛擬機提供了一個-XX:MaxTenuringThreshold參數設置老年代年齡閾值。

虛擬機並不是永遠地要求對象的年齡必須達到了-XX:MaxTenuringThreshold才能晉升到老年代,如果在Survivor空間中相同年齡所有對象的大小的總和大於Survivor空間的一半,年齡大於或者等於該年齡的對象就可以直接進入老年代,無須等到-XX:MaxTenuringThreshold中要求的年齡。

  • 空間分配擔保

 在發生Minor GC之前,虛擬機會先檢查老年代最大可用的連續空間是否大於新生代所有對象總空間,如果這個條件成立,那麼Minor GC可以確保是安全的;如果不成立,則虛擬機會查看HandlePromotionFailure設置值是否允許擔保失敗。如果允許,那麼會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小,如果大於,將嘗試着進行一次Minor GC,儘管這次Minor GC是有風險的;如果小於,或者HandlePromotionFailure設置不允許冒險,那麼這時就需要進行一次Full GC。

1.3 JVM的類加載機制

 類從被加載到虛擬機內存中開始,到卸載出內存爲止,它的整個生命週期包括:加載(Loading)驗證(Verification)準備(Preparation)解析(Resolution)初始化(Initialization)使用(Using)卸載(Unloading)7個階段,其中,驗證、準備、解析3個部分統稱爲連接(Linking)。類的生命週期如下圖所示:
在這裏插入圖片描述
 在虛擬機中,我們常說的類的加載過程是指加載(Loading)驗證(Verification)準備(Preparation)解析(Resolution)初始化(Initialization)這五個階段,它們的具體作用爲:

  • 加載

 加載過程是將二進制字節流(Class字節碼文件)通過類加載器加載到內存並實例化Class對象的過程(加載到方法區內)。這個過程獨立於虛擬機之外,並且二進制流可以從不同的環境內獲取或者由其他文件生成。

  • 驗證

 驗證Class文件的字節流是否符合虛擬機的要求,以免造成虛擬機出現異常。包括:文件格式驗證元數據驗證字節碼驗證符號引用驗證

  • 準備

 爲靜態變量(被final關鍵字修飾)分配內存空間、賦值和設置類變量初始化(自動初始化)。

  • 解析

 將常量池內的符號引用替換爲直接引用的過程。解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄以及調用點限定符7類符號引用進行。

  • 初始化

 執行類構造器<clinit>方法的過程,變量的聲明初始化就在這個階段進行。

虛擬機類加載的時機?

1)遇到new、getstatic、putstatic或者invokestatic 這四條字節碼指令的時候,且該類沒有進行初始化則進行該類的初始化;
2)使用反射機制的時候;
3)初始化類的父類;
4)初始化虛擬機要執行主類;
5)使用動態語言特性的時候;

總之,當對一個類進行主動引用的時候就會進行初始化操作,而進行被動引用的時候便不會觸發類初始化操作,比如通過子類引用父類靜態字段時子類不會被初始化。

2. 常見內存泄漏與優化

2.1 內存泄漏

 當一個對象已經不需要再使用本該被回收時,另外一個正在使用的對象持有它的引用從而導致它不能被垃圾收集器回收,結果它們就一直存在於內存中(通常指Java堆內存),佔用有效空間,永遠無法被刪除。隨着內存不斷泄漏,堆中的可用空間就不斷變小,這意味着爲了執行常用的程序,垃圾清理需要啓動的次數越來越多,非常嚴重的話會直接造成應用程序報OOM異常。

優化/避免內存泄漏原則:

  • 涉及到使用Context時,儘量使用Application的Context;
  • 對於非靜態內部類、匿名內部類,需將其獨立出來或者改爲靜態類;
  • 在靜態內部類中持有外部類(非靜態)的對象引用,使用弱引用來處理;
  • 不再使用的資源(對象),需顯示釋放資源(對象置爲null),如果是集合需要清空;
  • 保持對對象生命週期的敏感,尤其注意單例、靜態對象、全局性集合等的生命週期;
2.2 常見內存泄漏與優化

(1) 單例造成的內存泄漏

  • 案例
/** 工具類,單例模式
 * @Auther: Jiangdg
 * @Date: 2019/10/8 17:23
 * @Description:
 */
public class CommonUtils {
    private static CommonUtils instance;
    private Context mCtx;

    private CommonUtils(Context context){
        this.mCtx = context;
    }

    public static CommonUtils getInstance(Context context) {
        if(instance == null) {
            instance = new CommonUtils(context);
        }
        return instance;
    }
}

/**使用單例模式時造成內存泄漏
 *
 * @Auther: Jiangdg
 * @Date: 2019/10/8 17:24
 * @Description:
 */
public class SingleActivity extends AppCompatActivity {
    private CommonUtils mUtils;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mUtils = CommonUtils.getInstance(this);
    }
}
  • 分析與優化

 在上述示例中,當SingleActivity實例化Commontils對象完畢後,Commontils將持有SingleActivity對象的引用,而由於單例模式的靜態特性,Commontils對象的生命週期將於應用進程的一致,這就會導致在應用未退出的情況下,如果SingleActivity對象已經不再需要了,而Commontils對象該持有該對象的引用就會使得GC無法對其進行正常回收,從而導致了內存泄漏。優化:對於需要傳入Context參數的情況,儘量使用Application的Context,因爲它會伴隨着應用進程的存在而存在。

public class SingleActivity extends AppCompatActivity {
    private CommonUtils mUtils;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 造成內存泄漏
        //mUtils = CommonUtils.getInstance(this);
        
        mUtils = CommonUtils.getInstance(this.getApplicationContext());
    }
}

(2) Handler造成的內存泄漏

  • 案例
/** 使用Handler造成內存泄漏
 * @Auther: Jiangdg
 * @Date: 2019/10/8 17:55
 * @Description:
 */
public class HandlerActivity extends AppCompatActivity {
	
    // 匿名內部類
    private Handler mUIHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    };

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        new Thread(new Runnable() {
            @Override
            public void run() {
                // 處理耗時任務
                // ...
                
                mUIHandler.sendEmptyMessage(0x00);
            }
        });
    }
}
  • 分析與優化

 在剖析Handler消息機制原理一文中我們知道,在Android應用啓動時,應用程序的主線程會爲其自動創建一個Looper對象和與之關聯的MessageQueue,當主線程實例化一個Handler對象後,它就自動與主線程的MessageQueue關聯起來,所有發送到MessageQueueMessage(消息)都會持有Handler的引用。由於主線程的Looper對象會隨着應用進程一直存在的且Java類中的非靜態內部類和匿名內部類默認持有外部類的引用,假如HandlerActivity提前出棧不使用了,但MessageQueue中仍然還有未處理的MessageLooper就會不斷地從MessageQueue取出消息交給Handler來處理,就會導致Handler對象一直持有HandlerActivity對象的引用,從而出現HandlerActivity對象無法被GC正常回收,進而造成內存泄漏。優化:將Handler類獨立出來,或者使用靜態內部類,因爲靜態內部類不持有外部類的引用。

public class HandlerActivity extends AppCompatActivity {

// 匿名內部類默認持有HandlerActivity的引用
// 造成內存泄漏
//    private Handler mUIHandler = new Handler() {
//        @Override
//        public void handleMessage(Message msg) {
//            super.handleMessage(msg);
//        }
//    };

    // 優化,使用靜態內部類
    // 假如要持有HandlerActivity,以便在UIHandler中訪問其成員變量或成員方法
    // 需要使用弱引用處理
    private UIHandler mUIHandler;
    static class UIHandler extends Handler {
        private WeakReference<HandlerActivity> mWfActivity;
        
        public UIHandler(HandlerActivity activity) {
            mWfActivity = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    }

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mUIHandler = new UIHandler(this);
    }
}

Java中四種引用關係:

  • 強引用

    用來描述永遠不會被垃圾收集器回收掉的對象,類似"Object obj = new Object"

  • 軟引用

    用來描述一些還有用但並非必須的對象,由SoftReference類實現。被軟引用關聯着的對象會在系統將要發生OOM之前,垃圾收集器纔會回收掉這些對象。

  • 弱引用

    用來描述非必須的對象,比軟引用更弱一些,由WeakReference類實現。被弱引用的對象只能生產到下一次垃圾收集發生之前,無論當前內存是否足夠。

  • 虛引用

    最弱的引種引用關係,由PhantomReference類實現。一個對象是否有虛引用的存在,完全不會對其生存時間產生影響,也無法通過虛引用來獲取該對象實例。爲一個對象設置虛引用關聯的唯一目的是能在這個對象被垃圾收集器回收時收到一個系統通知

(3) 線程(非靜態內部類或匿名內部類)造成的內存泄漏

  • 案例
/** 使用線程造成的內存泄漏
 * @Auther: Jiangdg
 * @Date: 2019/10/9 10:04
 * @Description:
 */
public class ThreadActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 開啓一個子線程
        new Thread(new MyRunnable()).start();
        // 開啓一個異步任務
        new MyAsyncTask(this).execute();
    }

    class MyRunnable implements Runnable {

        @Override
        public void run() {

        }
    }

    class MyAsyncTask extends AsyncTask {
        private Context mCtx;

        public MyAsyncTask(Context context) {
            this.mCtx = context;
        }

        @Override
        protected Object doInBackground(Object[] objects) {
            return null;
        }
    }
}
  • 分析與優化

 在之前的分析中可知,Java類中的非靜態內部類和匿名內部類默認持有外部類的引用。對於上述示例中的MyRunnableMyAsyncTask來說,它們是一個非靜態內部類,將默認持有ThreadActivity對象的引用。假如子線程的任務在ThreadActivity銷燬之前還未完成,就會導致ThreadActivity無法被GC正常回收,造成內存泄漏。優化:將MyRunnable和MyAsyncTask獨立出來,或使用靜態內部類,因爲靜態內部類不持有外部類的引用。

public class ThreadActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 開啓一個子線程
        new Thread(new MyRunnable()).start();
        // 開啓一個異步任務
        // 優化:使用Application的Context
        new MyAsyncTask(this.getApplicationContext()).execute();
    }

    // 優化:使用靜態內部類
    static class MyRunnable implements Runnable {

        @Override
        public void run() {

        }
    }

    // 優化:使用靜態內部類
    // 如果需要傳入Context,使用Application的Context
    static class MyAsyncTask extends AsyncTask {
        private Context mCtx;

        public MyAsyncTask(Context context) {
            this.mCtx = context;
        }

        @Override
        protected Object doInBackground(Object[] objects) {
            return null;
        }
    }
}

(4) 靜態實例造成的內存泄漏

  • 案例
/**非靜態內部類創建靜態實例造成的內存泄漏
 * @Auther: Jiangdg
 * @Date: 2019/10/9 10:43
 * @Description:
 */
public class StaticInstanceActivity extends AppCompatActivity {
    private static SomeResources mSomeResources;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        if(mSomeResources == null) {
            mSomeResources = new SomeResources(this);
        }
    }

    class SomeResources {
        private Context mCtx;
        
        public SomeResources(Context context) {
            this.mCtx = context;
        }
    }
}
  • 分析與優化

 在上述案例中,演示了防止StaticInstanceActivity重建,比如橫豎屏切換,導致反覆創建SomeResources實例的問題,這裏使用了static修飾關鍵字將SomeResources實例聲明瞭靜態實例,以確保該實例始終存在的是同一個,且它的生命週期與應用相同。然而,由於SomeResources是一個非靜態內部類,其對象默認持有外部類StaticInstanceActivity的引用,就會導SomeResources的對象一直持有該引用,造成內存泄漏。優化:使用單例模式實現SomeResources,或者將其改成靜態內部類。如果需要傳入Context參數,必須使用Application的Context。

public class StaticInstanceActivity extends AppCompatActivity {
    private static SomeResources mSomeResources;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        if(mSomeResources == null) {
            // 優化,使用Application的Context
            mSomeResources = new SomeResources(this.getApplicationContext());
        }
    }

    // 優化:使用靜態內部類
    static class SomeResources {
        private Context mCtx;

        public SomeResources(Context context) {
            this.mCtx = context;
        }
    }
}

(5) 資源未關閉或監聽器未移除(註銷)引起的內存泄露情況

 在開發中,如果使用了BraodcastReceiverContentObserverFileCursorStreamBitmap自定義屬性attributeattr、傳感器等資源,應該在Activity銷燬時及時關閉或者註銷,否則這些資源將不會被回收,從而造成內存泄漏。比如:

// 使用傳感器等資源,需要註銷
SensorManager sensorManager = getSystemService(SENSOR_SERVICE);
Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
sensorManager.registerListener(this,sensor,SensorManager.SENSOR_DELAY_FASTEST);
sensorManager.unregisterListener(listener);
// 使用BraodcastReceiver,需要註銷
Myreceiver recevier = new Myreceiver();
intentFilter = new IntentFilter();
intentFilter.addAction("android.net.conn.CONNECTIVITY_CHANGE");
registerReceiver(recevier,intentFilter);
unRegisterReceiver(recevier);
// 自定義屬性,需要recycle
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.AttrDeclareView);
int color = a.getColor(R.styleable.AttrDeclareView_background_color, 0);
a.recycle();

 除了上述常見的5種內存泄漏外,還有包括無限循環動畫使用ListView使用集合容器以及使用WebView也會造成內存泄漏,其中,無限循環動畫造成泄漏的原因是沒有再Activity的onDestory中停止動畫;使用ListView造成泄漏的原因是構造Adapter時沒有使用緩存的convertView;使用集合容器造成泄漏的原因是在不使用相關對象時,沒有清理掉集合中存儲的對象引用。在優化時,在退出程序之前將集合中的元素(引用)全部清理掉,再置爲null;使用WebView造成泄漏的原因是在不使用WebView時沒有調用其destory方法來銷燬它,導致其長期佔用內存且不能被回收。在優化時,可以爲WebView開啓另外一個進程,通過AIDL與主線程進行通信,便於WebVIew所在的進程可以根據業務需要選擇合適的時機進行銷燬。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章