Android記憶體泄漏是一個經常要遇到的問題,程式在記憶體泄漏的時候很容易導致OOM的發生。那麼如何查找記憶體泄漏和避免記憶體泄漏就是需要知曉的一個問題,首先我們需要知道一些基礎知識。 Java的四種引用 強引用: 強引用是Java中最普通的引用,隨意創建一個對象然後在其他的地方引用一下,就是強引用,強引 ...
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的引用,
- Handler發送的消息到了消息隊列中
- Activity被結束掉
- 這個消息中包含了Handler的引用,Handler包含了Activity的引用,而且他還是個Runnable,也是匿名內部類,也間接包含了MainActivity引用
- 在Main Lopper中,當此消息被取出來,這未執行的10分鐘裡面,MainActivity沒法回收
- 記憶體泄漏
有人可能會說短暫的記憶體泄漏又能怎樣?這是錯誤的想法,因為只要發生記憶體泄漏,在這段時間只要進行了大記憶體的操作(比如載入一個照片牆),就有風險因為這個記憶體泄漏造成OOM(占用記憶體肯定剩下的少了)
上面這個如何修改呢?
將Runnable和Handler改成static 或者在外部定義內部使用。
其他常見的記憶體泄漏
- 靜態變數記憶體泄漏:使用靜態變數來引用一個事物,在不使用之後沒有下掉,那麼引用存在就會一直泄漏
- 單例導致的記憶體泄漏:使用的單例中保存了不應該被一直持有的對象,那麼就會造成記憶體泄漏
- 由第三方庫使用不當導致的記憶體泄漏:比如EventBus,Activity銷毀的時候沒有反註冊就會導致引用一直被持有無法回收
- 還有很多。。。他們都是因為引用沒有被清理造成的
如何查看記憶體泄漏
簡單粗暴 —> LeakCanary: Square出品的庫,當出現記憶體泄漏的時候會出現
精打細算 —> Android Studio 記憶體工具: 可以Dump下來當前的記憶體路徑,然後分析出來哪些對象目前的狀態。很強