APP性能-內存優化-內存管理認知

前言

作爲一名Java程序員,我們不需要像C/C++那樣爲每一個new出來的對象手動delete/free釋放內存。因爲有GC(垃圾回收器)的自動回收機制會幫我們自動處理。正因爲我們把這些操作交給了JVM,所以如果出現內存溢出和內存泄漏的情況,如果對JVM不熟悉,往往會很難找出問題所在,進而解決問題。所以要對內存使用進行優化,必須先熟悉Java的內存機制。

1.瞭解Java的內存管理

我們都知道android的App是由Java編寫的,其實APP也是運行在一個虛擬機上的,所以這裏就不得不提Java的內存管理。

根據《Java虛擬機規範(第2版)》的規定,Java虛擬機所管理的內存將會包括以下幾個運行時數據區域,如下圖所示:
這裏寫圖片描述

  1. 程序計數器:一塊較小內存區域,指向當前所執行的字節碼。如果線程正在執行一個Java方法,這個計數器記錄正在執行的虛擬機字節碼指令的地址,如果執行的是Native方法,這個計算器值爲空。

  2. Java虛擬機棧:線程私有的,其生命週期和線程一致,每個方法執行時都會創建一個棧幀用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。

  3. 本地方法棧:與虛擬機棧功能類似,只不過虛擬機棧爲虛擬機執行Java方法服務,而本地方法棧則爲使用到的Native方法服務。

  4. Java堆:是虛擬機管理內存中最大的一塊,被所有線程共享,該區域用於存放對象實例,幾乎所有的對象都在該區域分配。Java堆是內存回收的主要區域,因此也是內存泄漏發生的區域

  5. 方法區:與Java一樣,是各個線程所共享的,用於存儲已被虛擬機加載類信息、常亮、靜態變量、即時編譯器編譯後的代碼等數據。
    運行時常量池,運行時常量池是方法區的一部分,Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池,用於存放編譯期生成的各種字面量和符號引用。

1.1通過句柄訪問對象

這裏寫圖片描述

1.2 通過指針訪問對象

這裏寫圖片描述

通過上面兩個例子就基本對Java的內存管理和訪問使用有了個大概瞭解,有興趣可以更深入瞭解內存管理

2.GC垃圾回收器

既然Java的內存釋放交給了GC,那麼我們就有必須要了解下GC的原理和影響

NOTE:Android的GC和Java的GC有一些區別,以後有機會單獨把Android的GC單獨拿出來彙總

2.1 GC的影響

  • 內存泄露
  • 程序暫停
  • 程序吞吐量顯著下降
  • 響應時間變慢

2.2 對象存活判斷

GC需要先判斷對象是否存活,才能決定是否回收。判斷對象是否存活有兩種方式

  • 引用計數:每個對象有一個引用計數屬性,新增一個引用時計數加1,引用釋放時計數減1,計數爲0時可以回收。此方法簡單,無法解決對象相互循環引用的問題。

  • 可達性分析(Reachability Analysis):從GC Roots開始向下搜索,搜索所走過的路徑稱爲引用鏈。當一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象是不可用的。不可達對象。

在Java語言中,GC Roots包括:
虛擬機棧中引用的對象。
方法區中類靜態屬性實體引用的對象。
方法區中常量引用的對象。
本地方法棧中JNI引用的對象

下圖是GC Roots
這裏寫圖片描述

2.3 垃圾收集算法

2.3.1 標記 -清除算法

這裏寫圖片描述

“標記-清除”(Mark-Sweep)算法,如它的名字一樣,算法分爲“標記”和“清除”兩個階段:首先標記出所有需要回收的對象,在標記完成後統一回收掉所有被標記的對象。之所以說它是最基礎的收集算法,是因爲後續的收集算法都是基於這種思路並對其缺點進行改進而得到的。

它的主要缺點有兩個:一個是效率問題,標記和清除過程的效率都不高;另外一個是空間問題,標記清除之後會產生大量不連續的內存碎片,空間碎片太多可能會導致,當程序在以後的運行過程中需要分配較大對象時無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。

2.3.2 複製算法

這裏寫圖片描述

“複製”(Copying)的收集算法,它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另外一塊上面,然後再把已使用過的內存空間一次清理掉。

這樣使得每次都是對其中的一塊進行內存回收,內存分配時也就不用考慮內存碎片等複雜情況,只要移動堆頂指針,按順序分配內存即可,實現簡單,運行高效。只是這種算法的代價是將內存縮小爲原來的一半,持續複製長生存期的對象則導致效率降低。

2.3.3 標記-整理算法

這裏寫圖片描述

複製收集算法在對象存活率較高時就要執行較多的複製操作,效率將會變低。更關鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行分配擔保,以應對被使用的內存中所有對象都100%存活的極端情況,所以在老年代一般不能直接選用這種算法。

根據老年代的特點,有人提出了另外一種“標記-整理”(Mark-Compact)算法,標記過程仍然與“標記-清除”算法一樣,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的內存

2.3.4 分代收集算法

GC分代的基本假設:絕大部分對象的生命週期都非常短暫,存活時間短。

分代收集”(Generational Collection)算法,把Java堆分爲新生代和老年代,這樣就可以根據各個年代的特點採用最適當的收集算法。在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就選用複製算法,只需要付出少量存活對象的複製成本就可以完成收集。而老年代中因爲對象存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記-清理”或“標記-整理”算法來進行回收

需要了解更多關於:GC算法

3.Android的內存管理

Android的ART和DVM都是使用pagingmemory-paging來管理內存。這意味着一個App能修改的任何內存(不管是用來分配對象還是用來映射頁)都只能常駐在RAM,而不能被移除。因此從app中徹底釋放內存的唯一方法是,銷燬持有的對象的引用,從而使被佔用的內存空間可以被GC收集。這就產生了潛在的異常:當系統內存吃緊時,任何被映射的卻沒有修改的文件,例如代碼,都可能被移除RAM(銷燬)。

3.1 安卓的GC

  1. 託管內存環境,如ART或Dalvik虛擬機,可以跟蹤每個內存分配。一旦確定一塊內存不再被程序使用,它就可以將其釋放回堆,而不需要程序員的任何干預。在託管內存環境中回收未使用內存的機制稱爲垃圾回收。垃圾收集有兩個目標:在將來無法訪問的程序中查找數據對象;並回收這些對象使用的資源。

  2. Android的內存堆是一代的,這意味着根據正在分配的對象的預期使用壽命和大小,​​它可以跟蹤不同的分配桶。例如,最近分配的對象屬於新生代。當一個對象保持活躍的時間足夠長時,它可以升格爲老生代,然後就是永久代了。

  3. 每個堆生成都有自己的專用上限,即可以佔用對象的內存量上限。任何時候一代人開始填滿,系統執行一個垃圾收集事件,試圖釋放內存。垃圾收集的持續時間取決於它收集的對象是哪個代際(比如新生代,老生代,永久代)以及每一代中有多少活動對象。

  4. 即使這樣GC速度相當快,仍然會影響應用的性能。你通常不會控制GC事件何時發生在你的代碼內。系統具有運行的一套標準,用於確定何時執行GC。當滿足標準時,系統停止執行進程並開始GC。如果GC發生在密集處理循環的中間,如動畫或音樂播放,會增加處理時間。這種增加可能會潛在地推動應用程序中的代碼執行超過建議的16ms閾值,以實現高效和平滑的幀渲染。

此外,代碼流可能會執行各種各樣的工作,強制GC事件更頻繁地發生或使其持續時間超過正常。例如,如果在Alpha混合動畫的每個幀期間在for循環的最內層代碼分配多個對象,則可能會有大量的污染對象會在內存堆。在這種情況下,GC會執行多個GC事件,並降低應用程序的性能

3.2 共享內存

爲了適配RAM,安卓系統通過進程來共享內存頁

  1. Android應用的進程都是從一個叫做Zygote的進程fork出來的。Zygote進程在系統啓動並且載入通用的framework的代碼與資源之後開始啓動。爲了啓動一個新的程序進程,系統會fork Zygote進程生成一個新的進程,然後在新的進程中加載並運行應用程序的代碼。這使得大多數的RAM pages被用來分配給framework的代碼,同時使得RAM資源能夠在應用的所有進程之間進行共享。

  2. 大多數static的數據被mmapped到一個進程中。這不僅僅使得同樣的數據能夠在進程間進行共享,而且使得它能夠在需要的時候被paged out。常見的static數據包括Dalvik Code,app resources,so文件等。

  3. 大多數情況下,Android通過顯式的分配共享內存區域(例如ashmem或者gralloc)來實現動態RAM區域能夠在不同進程之間進行共享的機制。例如,Window Surface在App與Screen Compositor之間使用共享的內存,Cursor Buffers在Content Provider與Clients之間共享內存。
    由於廣泛使用共享內存,確定應用程序使用多少內存需要注意。正確確定應用程序內存使用的技巧見:Investigating Your RAM Usage

3.3 分配與回收內存

  1. 每個進程的Dalvik堆棧被限制在一個虛擬內存範圍內。這就定義了邏輯堆棧的大小,它可以根據需要自增長(但只能增長到系統爲每個應用定義的空間上限)。
  2. 堆棧的邏輯大小與堆棧使用的物理內存數量是不一樣的。當檢查應用的堆棧時,安卓會計算一個稱爲Proportional Set Size(PSS)的值,它用來解釋被其他進程共享的髒和乾淨頁——但也僅僅是按照有多少應用共享RAM,並按比例分攤得來的。這個PSS值的總大小才被系統認爲是物理內存的大小。更多關於PSS的信息,Investigating Your RAM Usage
  3. Dalvik 堆棧不會整理堆棧的邏輯大小,這意味着安卓不會通過整理堆棧碎片回收空間。只有在堆棧的尾部有沒有使用的空間時,安卓纔會壓縮邏輯堆的大小。但這並不意味着堆棧使用的物理內存不能被壓縮。在GC工作過後,Dalvik 會遍歷整個堆棧,找到無用的頁,通過madvise函數把它們返回給內核。因此成對兒申請和回收大塊內存會導致所有使用過的物理內存被回收。然而,回收小塊內存可能效率更低,因爲小塊內存使用的頁可能仍被還沒釋放的對象佔用,進而導致該頁無法被回收。

3.4 限制應用的內存

爲了保持多任務處理的工作環境,安卓系統對每個應用的堆棧大小都有嚴格的限制。這個數值隨着設備變化而變化,具體要看設備可用的RAM空間有多少。一旦你的應用佔用的內存達到了堆棧上限,卻仍試圖申請更多內存,系統就會報OOM的錯誤提示。
在某些時候,你可能會想查詢系統,以便準確地知道當前設備到底有多大的可用空間。例如,確定緩存多大的數據比較安全。你可以通過調用getMemoryClass()方法得到一個以兆字節爲單位的整型數字,表示你的應用可用的堆棧大小。

3.5 應用切換

1.當用戶在App之間切換時,Android會保留在LRU緩存中的包括:不是前臺的App和正在進行的前臺服務比如音樂播放。例如,當用戶首次啓動App時,會爲其創建一個進程;但是當用戶離開App時,該進程不會退出。系統保持進程緩存。如果用戶稍後返回到App,系統重新使用該過程,從而使應用切換更快。

2.如果您的App具有緩存進程,並且保留了當前不需要的內存,那麼即使在用戶未使用的情況下,您的應用也會影響系統的整體性能。當系統內存不足時,它會殺死保存在LRU緩存中的進程。系統還會計算使用最多內存的進程,以決定終止這些進程來釋放RAM。

注意:當系統開始殺死LRU中的進程時,它主要從下往上工作。該系統還考慮哪些進程消耗更多的內存,從而在被殺掉之後爲系統提供更多的內存。總體來說,在LRU列表中消耗的內存越少,進程有更多機會保留在LRU,以能夠快速恢復。
這裏可以看下安卓的5中進程:
這裏寫圖片描述
處於low priority的進程很容易被殺掉,瞭解更多的processes-and-threads

4.調查RAM的使用情況

篇幅有限,更多查看:調查RAM使用情況

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