Android 記憶體流失,android記憶體流失
Android記憶體流失是一個經常要遇到的問題,程式在記憶體流失的時候很容易導致OOM的發生。那麼如何尋找記憶體流失和避免記憶體流失就是需要知曉的一個問題,首先我們需要知道一些基礎知識。
Java的四種引用
強引用: 強引用是Java中最普通的引用,隨意建立一個對象然後在其他的地方引用一下,就是強引用,強引用的對象Java寧願OOM也不會回收他
軟引用: 軟引用是比強引用弱的引用,在Java gc的時候,如果軟引用所引用的對象被回收,首次gc失敗的話會繼而回收軟引用的對象,軟引用適合做緩衝處理 可以和引用隊列(ReferenceQueue)一起使用,當對象被回收之後儲存他的軟引用會放入引用隊列
弱引用: 弱引用是比軟引用更加弱的引用,當Java執行gc的時候,如果弱引用所引用的對象被回收,無論他有沒有用都會回收掉弱引用的對象,不過gc是一個比較低優先順序的線程,不會那麼及時的回收掉你的對象。 可以和引用隊列一起使用,當對象被回收之後儲存他的弱引用會放入引用隊列
虛引用: 虛引用和沒有引用是一樣的,他必須和引用隊列一起使用,當Java回收一個對象的時候,如果發現他有虛引用,會在回收對象之前將他的虛引用加入到與之關聯的引用隊列中。 可以通過這個特性在一個對象被回收之前採取措施
下面是一個例子:
public class Main { public static void main(String[] args) throws InterruptedException { ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>(); String sw = "虛引用"; switch (sw) { case "軟引用": Object objSoft = new Object(); SoftReference<Object> softReference = new SoftReference<>(objSoft, referenceQueue); System.out.println("GC前擷取:" + softReference.get()); objSoft = null; System.gc(); Thread.sleep(1000); System.out.println("GC後擷取:" + softReference.get()); System.out.println("隊列中的結果:" + referenceQueue.poll()); break; /* * GC前擷取:java.lang.Object@61bbe9ba * GC後擷取:java.lang.Object@61bbe9ba * 隊列中的結果:null * */ case "弱引用": Object objWeak = new Object(); WeakReference<Object> weakReference = new WeakReference<>(objWeak, referenceQueue); System.out.println("GC前擷取:" + weakReference.get()); objWeak = null; System.gc(); Thread.sleep(1000); System.out.println("GC後擷取:" + weakReference.get()); System.out.println("隊列中的結果:" + referenceQueue.poll()); /* * GC前擷取:java.lang.Object@61bbe9ba * GC後擷取:null * 隊列中的結果:java.lang.ref.WeakReference@610455d6 * */ break; case "虛引用": Object objPhan = new Object(); PhantomReference<Object> phantomReference = new PhantomReference<>(objPhan, referenceQueue); System.out.println("GC前擷取:" + phantomReference.get()); objPhan = null; System.gc(); //此處的區別是當objPhan的記憶體被gc回收之前虛引用就會被加入到ReferenceQueue隊列中,其他的引用都為當引用被gc掉時候,引用會加入到ReferenceQueue中 Thread.sleep(1000); System.out.println("GC後擷取:" + phantomReference.get()); System.out.println("隊列中的結果:" + referenceQueue.poll()); /* * GC前擷取:java.lang.Object@61bbe9ba * GC後擷取:null * 隊列中的結果:java.lang.ref.WeakReference@610455d6 * */ break; } }}
Java GC
目前oracle jdk和open jdk的虛擬機器都為Hotspot,android 為Dalvik和Art
曾經的GC演算法:引用計數
簡短的說引用計數就是對每一個對象的引用計算數字,如果引用就+1,不引用就-1,回收掉引用計數為0的對象。來達到記憶體回收
弊端:如果兩個對象都應該被回收但是他倆卻互相依賴,那麼他兩者的引用永遠都不會為0,那麼就永遠無法回收, 無法解決循環參考的問題
這個演算法只在很少數的虛擬機器中使用過
現代的GC演算法
- 標記回收演算法(Mark and Sweep GC) :從"GC Roots"集合開始,將記憶體整個遍曆一次,保留所有可以被GC Roots直接或間接引用到的對象,而剩下的對象都當作垃圾對待並回收,這個演算法需要中斷進程內其它組件的執行並且可能產生記憶體片段。
- 複製演算法(Copying) :將現有的記憶體空間分為兩快,每次只使用其中一塊,在記憶體回收時將正在使用的記憶體中的存活對象複製到未被使用的記憶體塊中,之後,清除正在使用的記憶體塊中的所有對象,交換兩個記憶體的角色,完成記憶體回收。
- 標記-壓縮演算法(Mark-Compact) :先需要從根節點開始對所有可達對象做一次標記,但之後,它並不簡單地清理未標記的對象,而是將所有的存活對象壓縮到記憶體的一端。之後,清理邊界外所有的空間。這種方法既避免了片段的產生,又不需要兩塊相同的記憶體空間,因此,其性價比比較高。
- 分代 :將所有的建立對象都放入稱為年輕代的記憶體地區,年輕代的特點是對象會很快回收,因此,在年輕代就選擇效率較高的複製演算法。當一個對象經過幾次回收後依然存活,對象就會被放入稱為老生代的記憶體空間。對於新生代適用於複製演算法,而對於老年代則採取標記-壓縮演算法。
以上四種演算法資訊引用自QQ空間團隊分享 Android GC 那點事 &version=11000003&pass_ticket=nhSGhYD4LC9FWvUPv26Y7AdIzqEDu8FTImf2AKlyrCk%3D) ,總結的特別棒
導致記憶體流失的原因
對象在GC Root中可達,也就是他的引用不為空白,所以GC無法回收它也就會導致記憶體流失
GC Root起點
- 虛擬機器棧中引用的對象
- 方法區中類靜態屬性引用的對象
- 方法區中常量引用的對象
- JNI引用的對象
GC可以續一秒
當一個對象在引用鏈中失S#x53BB;了引用,那麼他就真的要告別世界了嗎,其實並不是,虛擬機器會給他“緩刑”,每一個對象有一個finalize() 方法,虛擬機器是否給他緩刑取決於這個對象的這個方法是否被執行,如果這個對象的這個方法沒有被覆蓋或者這個方法被執行過一次,那麼就要“行刑”了。真的是“續一秒”
如果這個對象的finalize()方法應該被執行,那麼虛擬機器會將它放在F-Queue隊列中,稍後虛擬機器會自動建立一個Finalizer線程去執行這個隊列中的對象的這個方法。如果對象在finalize()中成功自救,舉個例子,把自己和一個存在的對象強引用,那麼就不會被回收,否則就真的被回收了。
但是虛擬機器並不會保證Finalizer線程執行結束再進行回收,因為如果在某一個對象的finalize()方法中執行了死迴圈或者超級耗時的操作,虛擬機器等待這個執行結束的話就會導致整個Gc崩潰了
首先注意這個方法只能被執行一次,第二次就會標記了這個方法被執行過不會再執行了,其次,這個方法不一定會被執行到,所以不要依賴finalize()去自救。這不是好的做法。
並發GC和非並發GC
Android2.3之後支援了並發的GC。
- 非並發GC : 虛擬機器在執行GC的時候進行Stop the world,也就是掛起其他所有的線程,通常會持續上百毫秒,一次Mark,然後直接清理
- 並發GC : 跟非並發的簡單gc來比較,一般非並發GC需要耗費上百ms的時間來進行,而並發gc僅僅需要10ms左右的時間,效率大幅度提升(資料來自:技術小黑屋大大),但是並發gc由於需要進行重複的處理改動的對象,所以需要更多的CPU資源
兩者的差別:
首先非並發GC簡單粗暴,直接掛起所有的線程,此時Java堆中肯定不會有任何的添加和修改,此時去遞迴GC樹,然後標記-清理。但是這樣會造成很大的開銷,大家都等著你豈不是很沒面子= =
然而非並發的GC是一點一點來的,跟線程同步進行這樣就不會有很長時間的等待,但是你要明白一個道理,想把地掃乾淨這段時間必須沒人來踩,所以他要有掛起線程的過程。
那麼並發是怎麼實現的呢?首先有個知識點就是Jvm在分配記憶體的時候,有兩種方式
- 指標碰撞:一個指標,申請一塊記憶體就指標挪動相應的距離,不會產生記憶體片段,這要求記憶體是很規整的
- 空閑列表:每次申請一塊記憶體給需要的對象,然後有一個列表記錄了哪些位置被申請了,下次申請的時候就不申請這個位置,這樣適用於記憶體不是很規整的情況
建立對象是一個頻繁的操作,那麼我們如何保證原子性呢?兩種方案
- CAS(Compare and Swap)策略配上失敗重試來保證原子性
- 每個線程分配一個TLAB : 很簡單,每個線程自己有自己的一塊記憶體,那麼分配的時候自己鎖自己的分區就行了,提高了效率
我們用的是第二種 233
所以擷取Java堆鎖的時候,重點來了,我們逐個線程去鎖TLAB,而不是一次全鎖住,當然提高了並發GC的效率,所以更快。但是引來的問題就是並發的問題,所以下一步要重複去修改在一個個探索時候被改的對象。也就需要更多的CPU資源。
我們為什麼要關注GC
首先我們知道虛擬機器如何去GC才能瞭解到如何讓一個對象被正確的回收,這樣才不能記憶體流失
其次無論是並發GC還是非並發GC都會導致掛起其他的所有線程,那麼就會帶來程式卡頓。
ART在GC上做到了更加細粒度的控制,可以更加流暢的GC
常見的記憶體流失案例:Handler記憶體流失
首先鋪墊一句話:非靜態內部類和匿名類會隱式的持有外部類的引用
public class MainActivity extends AppCompatActivity { private Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { Log.d("smallSohoSolo", "Hello Handler"); } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mHandler.postDelayed(new Runnable() { @Override public void run() { Log.d("smallSohoSolo", "Running"); } }, 1000 * 60 * 10); //10分鐘之後執行 finish(); }}
這段代碼有很明顯的記憶體流失,首先Handler和Runnable都是匿名內部類的執行個體,他們都會持有MainActivity的引用,
有人可能會說短暫的記憶體流失又能怎樣?這是錯誤的想法,因為只要發生記憶體流失,在這段時間只要進行了大記憶體的操作(比如載入一個照片牆),就有風險因為這個記憶體流失造成OOM(佔用記憶體肯定剩下的少了)
上面這個如何修改呢?
將Runnable和Handler改成static 或者在外部定義內部使用。
其他常見的記憶體流失
- 靜態變數記憶體流失:使用靜態變數來引用一個事物,在不使用之後沒有下掉,那麼引用存在就會一直泄漏
- 單例導致的記憶體流失:使用的單例中儲存了不應該被一直持有的對象,那麼就會造成記憶體流失
- 由第三方庫使用不當導致的記憶體流失:比如EventBus,Activity銷毀的時候沒有反註冊就會導致引用一直被持有無法回收
- 還有很多。。。他們都是因為引用沒有被清理造成的
如何查看記憶體流失
簡單粗暴 —> LeakCanary: Square出品的庫,當出現記憶體流失的時候會出現
精打細算 —> Android Studio 記憶體工具: 可以Dump下來當前的記憶體路徑,然後分析出來哪些對象目前的狀態。很強