需求–內存泄漏優化,PSS有所下降, OOM率減少
主要是與某個版本作基準進行對比(一般是最新版本的前一個版本作原數據),優化后,PSS有所下降,線上OOM率減少(Bugly版本對比),泄漏點減少(從捉取一些線上上傳回來的內存堆棧信息分析,或本地測試后dump下hprof文件分析)。
內存泄漏優化的思路
- 了解什么是內存泄漏
- 了解虛擬機中的對象的創建過程
- 了解Java內存分配模型
- 了解垃圾回收分代收集理論
- 了解java的引用類型
- GC是如何判斷對象存活
- 有哪些對象可作為GC Roots
- 了解內存泄漏的工具
- 總結
什么是內存泄漏
App程序中己動態分配的堆內存
,由于某種原因,App程序未釋放或無法釋放,會造成系統(手機)內存的浪費。長生命周期對象持有短生命周期對象強引用
,從而導致短生命周期對象無法被回收。 我們注意這兩個關鍵詞堆內存、強引用
。
虛擬機中的對象的創建過程(類的生命周期)
什么都不用說,先上張自畫圖。為大家推薦一本書《深入理解JVM》
第一步,當虛擬機遇到一條new指令時,首先檢查這條指令的參數是否能在常量池中定位到一個類的符號引用,并且檢查這個符號引用代表的類是否已被加載、解析和初始化過。如果沒有,那必須先執行相應的類加載過程。
第二步,如果檢查通過后,虛擬機將為這個new出的對象進行分配內存。劃分內存是通過指針碰撞、空間列表的組合,同時也考慮并發安全問題(CAS(Compare And Swap的縮寫–樂觀鎖)失敗重試、本地線程內存緩沖)。這中是進行內存分配哦,這時候還不能確定對象所需要的內存大小。在類加載完成后才確定內存的大小。
第三步,內存分配完成后,虛擬機將分配到的內存空間進行初始化為零值(默認的初始值),但不包括對象頭信息,如果使用TLAB(Thread Local Allocation Buffer ,即線程本地分配緩沖區),這過程可以提前至TLAB分配時進行(Eden區劃分出一小塊區域作為TLAB)。這一步操作是保證了對象的實例成員(字段)在Java代碼中可以不賦初始值就直接使用,程序能訪問到這些字段的數據類型所對應的零值。
第四步,對象進行必要的設置(主要是一些對象頭信息的設置),例如這個對象的運行狀態、GC分代年齡、鎖狀態、屬于哪個類的實例、哈希碼等等信息。
第五步,從前面幾個步驟知道,只是設置了對象頭信息和所有類成員(字段)賦為默認的初始值,對象并沒有執行方法,所以最后是會接著執行方法,這樣才算創建出一個真正可用的對象。幾乎所有對象是存放在堆區。
Java內存分配模型
使用一張圖,快速了解一下內存分配模型。
方法區
編譯時就分配好,在程序整個運行期間都存在。它用于存儲已經被虛擬機加載的類信息、靜態變量、常量等數據。
堆區
幾乎所有 new 出來的對象是存放在堆區,由 Java 垃圾回收器回收。堆中的對象是垃圾回收的重點。 堆區的劃分新生代、老年代:
新生代
新生代是用來存放新new出來的對象,劃分為 Eden區、From Survivor區 、To Survivor區 。幾乎所以的new出來的對象都會存放在Eden區 (如果new出來的對象占用的內存非常大,在新生代中存放不下,直接進入老年代存放)。
當Eden區的內存空間不足時,系統會觸發Minor GC /Young GC進行回收Eden區的對象(From Survivor區 、To Survivor區不會觸發GC),經過GC后,一些對象仍然存活(對象被引用著–通過GC Root可達性來判斷),則會被移到To Survivor區存放,當對象在Survivor區熬過一次GC后,此對象的GC年齡就會+1(GC年齡是對象頭信息的一個標記參數),會被復制到From Survivor區,當From Survivor區的對象達到一定年齡時(默認年齡是15,但可以通過XX:MaxTenuringThreshold設置),被移到老年代,否則復制到To Survivor區。
老年代
老年代是新生代存放不下的大對象,或對象經過多次Minor GC /Young GC后仍然存活的對象(長期存活的對象)。
當隨著Eden區的Minor GC /Young GC持續進行,老年代的對象持續增加,會導致老年代可用的內存空間也會持續減少,最終系統會觸發Major GC。
元空間(永久代)
永久代(持久代)是存放包含應用的類/方法信息,以及JRE庫的類和方法信息。然而在Java8中,元空間取代了永久代,元空間(Metaspace)被稱為“元數據區”。
需要注意的是
:元空間并不在虛擬機中哦,而是使用本地內存(以前永久代是在jvm中的)。這樣就解決了以前永久代的OOM問題,元數據和class對象存放在永久代中,容易出現性能問題和內存溢出,畢竟是和老年代共享堆空間。
堆內存分配策略
內存分配原則
- 對象優先在Eden分配----如果說Eden內存空間不足,就會發生Minor GC /Young GC。
- 大對象直接進入老年代----大對象:需要大量連續內存空間的Java對象,比如很長的字符串和大型數組。會導致新生代內存有空間,還是需要提前進行垃圾回收獲取連續空間來放此大對象。Survivor區會進行大量的內存復制,-XX:PretenureSizeThreshold 參數 ,大于這個數量直接在老年代分配,缺省為0 ,表示絕不會直接分配在老年代。當Eden分配和Survivor區都沒有足夠空間存放此大對象時,則直接分配到老年代。
- 長期存活的對象將進入老年代----Survivor區的對象達到一定年齡時,直接移到老年代。默認15歲,可以通過XX:MaxTenuringThreshold設置。
- 動態對象年齡判定----為了能更好地適應不同程序的內存狀況,虛擬機并不是永遠地要求對象的年齡必須達到了MaxTenuringThreshold才能晉升老年代,如果在Survivor區中相同年齡所有對象大小的總和大于Survivor區的一半,年齡大于或等于該年齡的對象就可以直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡。
- 空間分配擔保:新生代中有大量的對象存活,Survivor區不夠,當出現大量對象在Minor GC后仍然存活的情況(最極端的情況就是內存回收后新生代中所有對象都存活),就需要老年代進行分配擔保,把Survivor區無法容納的對象直接進入老年代,只要老年代的連續空間大于新生代對象的總大小或者歷次晉升的平均大小,就進行Minor GC,否則Full GC。
棧中分配對象
- 逃逸分析----如果符合逃逸分析規則,則在棧中分配對象。
堆中的優化技術
- TLAB ----Thread Local Allocation Buffer ,即線程本地線程分配緩沖。
棧區
當方法執行時,會在棧區內存中創建方法體內部的局部變量,方法結束后自動釋放內存。
垃圾回收分代收集理論
我們都知道,在java中不同的對象存在不同的生命周期的,java對象在JVM中也存放在不同的區域,所以對不同生命周期不同的存放區,采取不同的回收策略,以提高效率。
當Eden區的內存空間不足時,系統觸發Minor GC/Young GC, 隨著GC持續進行,老年代的對象持續增加,導致老年代的內存空間不足,系統觸發Major GC。當堆區或方法區內存空間不足時,系統觸發Full GC。
Full GC:清理成本高,系統資源消耗高,對系統性能產生影響,很多性能什么都是針對Full GC進行的。 觸發Full GC的條件有:
- 調用System.gc()
- 方法區空間不足
- 堆區空間不足
不同階段GC的特點
- Minor GC/Young GC – 執行非常頻繁,速度特別快。
- Major GC – 速度上,一般會比Minor GC/Young GC慢十倍以上。
- Full GC – Minor GC和Major GC都會執行,會發出"Stop the World"事件,會中斷程序運行,直到GC完成。所以Full GC時,我們會感知到APP有卡頓之感。
垃圾回收分代收集對應的回收算法
- 復制算法: 實現簡單,運行高效,內存復制,內存利用率只有一半。
- 標記-清除: 利用率100%,不需要內復制,有內存碎片
- 標記-整理:利用率100%,沒有內存碎片,需要內存復制(整理存活的對象,將其拷貝到一塊連續內存中)
GC是如何判斷對象存活
-
可達性分析 (java) 通過一系列稱之為“GC Roots”的對象作為起始點,從這些節點向下搜索,搜索所有的引用鏈,當一個對象到GC Roots有引用鏈,則說明這個對象存活著;當一個對象到GC Roots沒有任何引用鏈(即GC Roots到對象不可達)時,則證明此對象是不可用的(所謂的垃圾)。
-
引用計數算法(JVM早期使用的—已經不使用) A對象引用B 對象(+1),同時C對象引用B對象(1+1=2),計數法就是引用一次累加1次,如果沒有引用就累減1次,如果歸到0時,說明沒有引用。
缺點:就是相互引用。如A對象引用B對象,同時B對象引用A對象,很難去判斷對象是否應該回收。
在Java, 可作為GC Roots的對象包括:
- 方法區: 類靜態屬性的對象;
- 方法區: 常量的對象;
- 虛擬機棧(本地變量表)中的對象。
- 本地方法棧JNI(Native方法)中的對象。
四種引用類型
- 強引用(StrongReference):JVM 寧可拋出 OOM ,也不會讓 GC 回收具有強引用的對象。
- 軟引用(SoftReference):只有在內存空間不足時,對象才會被回收。
- 弱引用(WeakReference):在 GC 時,一旦發現了只具有弱引用的對象,不管當前內存空間足夠與否,對象都會被回收。
- 虛引用(PhantomReference):任何時候都可以被GC回收,當垃圾回收器準備回收一個對象時,如果發現它還有虛引用,就會在回收對象的內存之前,把這個虛引用加入到與之關聯的引用隊列中。程序可以通過判斷引用隊列中是否存在該對象的虛引用,來了解這個對象是否將要被回收。可以用來作為GC回收Object的標志。
我們定義對象,應該考慮使用那種引用,多考慮使用軟引用
(定義一些還有用但并非必須的對象)或弱引用
(定義非必須對象)。
Android Studio的profiler工具
- 我們也可以利用Android Studio的profiler工具,方便快速查找、觀察,簡單分析一些對象生成情況。
- 也可以dump下hprof文件,結合MAT深入分析與排查,對象發生是否泄漏。
注意
:MAT打開Android Studio的profiler里dump下hprof文件時,利用AS自帶的hprof工具轉換一下格式(通過命令hprof-conv -z 原hprof文件 輸出hprof文件),不然打開是亂碼。
當然檢測內存泄漏的工具和方法有很多,就不一一列舉了,感興趣的可以網上查閱一下。
常見的內存問題場景
- 靜態成員/單例
- 作為GC ROOT,持有短生命周期引用(如Activity)導致其短生命周期對象無法釋放。
- 集合類
- 當使用集合時,只有添加元素,沒有對應的刪除元素。
- 非靜態內部類/匿名內部類
- 如Handler postDelayed一個匿名Runnable,退出Activity時消息沒處理完。
- 上下文 – Context
- 持有的上下文,需要特別注意。
- 注冊/反注冊
- 如EventBus只有注冊沒有注銷。addXXXListener函數,需要有對應的removeXXXListener等等。
- 未關閉/釋放資源
- 如FileOutputStream未close。
- 系統Bug
- WebView、InputMethodManager等
總結
- 上面的內存相關知識也是自己學習的一種總結,有錯誤的可以留言指正。
- 內存優化,需要對下面的知識有一定的了解。
- Java內存分配模型
- Java的四大引用及其使用場景
- 內存檢測工具及常用命令
- GC Root的定義
為了幫助到大家更好的全面清晰的掌握好性能優化,準備了相關的核心筆記(還該底層邏輯):https://qr18.cn/FVlo89
性能優化核心筆記:https://qr18.cn/FVlo89
啟動優化
內存優化
UI優化
網絡優化
Bitmap優化與圖片壓縮優化:https://qr18.cn/FVlo89
多線程并發優化與數據傳輸效率優化
體積包優化
《Android 性能監控框架》:https://qr18.cn/FVlo89
《Android Framework學習手冊》:https://qr18.cn/AQpN4J
- 開機Init 進程
- 開機啟動 Zygote 進程
- 開機啟動 SystemServer 進程
- Binder 驅動
- AMS 的啟動過程
- PMS 的啟動過程
- Launcher 的啟動過程
- Android 四大組件
- Android 系統服務 - Input 事件的分發過程
- Android 底層渲染 - 屏幕刷新機制源碼分析
- Android 源碼分析實戰