Android內(nèi)存泄漏思考
Android內(nèi)存泄漏是一個(gè)經(jīng)常要遇到的問(wèn)題,程序在內(nèi)存泄漏的時(shí)候很容易導(dǎo)致OOM的發(fā)生。那么如何查找內(nèi)存泄漏和避免內(nèi)存泄漏就是需要知曉的一個(gè)問(wèn)題,首先我們需要知道一些基礎(chǔ)知識(shí)。
Java的四種引用強(qiáng)引用: 強(qiáng)引用是Java中最普通的引用,隨意創(chuàng)建一個(gè)對(duì)象然后在其他的地方引用一下,就是強(qiáng)引用,強(qiáng)引用的對(duì)象Java寧愿OOM也不會(huì)回收他
軟引用: 軟引用是比強(qiáng)引用弱的引用,在Java gc的時(shí)候,如果軟引用所引用的對(duì)象被回收,首次gc失敗的話會(huì)繼而回收軟引用的對(duì)象,軟引用適合做緩存處理 可以和引用隊(duì)列(ReferenceQueue)一起使用,當(dāng)對(duì)象被回收之后保存他的軟引用會(huì)放入引用隊(duì)列
弱引用: 弱引用是比軟引用更加弱的引用,當(dāng)Java執(zhí)行g(shù)c的時(shí)候,如果弱引用所引用的對(duì)象被回收,無(wú)論他有沒(méi)有用都會(huì)回收掉弱引用的對(duì)象,不過(guò)gc是一個(gè)比較低優(yōu)先級(jí)的線程,不會(huì)那么及時(shí)的回收掉你的對(duì)象。 可以和引用隊(duì)列一起使用,當(dāng)對(duì)象被回收之后保存他的弱引用會(huì)放入引用隊(duì)列
虛引用: 虛引用和沒(méi)有引用是一樣的,他必須和引用隊(duì)列一起使用,當(dāng)Java回收一個(gè)對(duì)象的時(shí)候,如果發(fā)現(xiàn)他有虛引用,會(huì)在回收對(duì)象之前將他的虛引用加入到與之關(guān)聯(lián)的引用隊(duì)列中。 可以通過(guò)這個(gè)特性在一個(gè)對(duì)象被回收之前采取措施
下面是一個(gè)例子:
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('隊(duì)列中的結(jié)果:' + referenceQueue.poll());break;/* * GC前獲取:java.lang.Object@61bbe9ba * GC后獲取:java.lang.Object@61bbe9ba * 隊(duì)列中的結(jié)果: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('隊(duì)列中的結(jié)果:' + referenceQueue.poll());/** GC前獲取:java.lang.Object@61bbe9ba* GC后獲取:null* 隊(duì)列中的結(jié)果: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();//此處的區(qū)別是當(dāng)objPhan的內(nèi)存被gc回收之前虛引用就會(huì)被加入到ReferenceQueue隊(duì)列中,其他的引用都為當(dāng)引用被gc掉時(shí)候,引用會(huì)加入到ReferenceQueue中Thread.sleep(1000);System.out.println('GC后獲取:' + phantomReference.get());System.out.println('隊(duì)列中的結(jié)果:' + referenceQueue.poll());/** GC前獲取:java.lang.Object@61bbe9ba* GC后獲取:null* 隊(duì)列中的結(jié)果:java.lang.ref.WeakReference@610455d6* */break;} }} Java GC
目前oracle jdk和open jdk的虛擬機(jī)都為Hotspot,android 為Dalvik和Art
曾經(jīng)的GC算法:引用計(jì)數(shù)
簡(jiǎn)短的說(shuō)引用計(jì)數(shù)就是對(duì)每一個(gè)對(duì)象的引用計(jì)算數(shù)字,如果引用就+1,不引用就-1,回收掉引用計(jì)數(shù)為0的對(duì)象。來(lái)達(dá)到垃圾回收
弊端:如果兩個(gè)對(duì)象都應(yīng)該被回收但是他倆卻互相依賴,那么他兩者的引用永遠(yuǎn)都不會(huì)為0,那么就永遠(yuǎn)無(wú)法回收, 無(wú)法解決循環(huán)引用的問(wèn)題
這個(gè)算法只在很少數(shù)的虛擬機(jī)中使用過(guò)
現(xiàn)代的GC算法
標(biāo)記回收算法(Mark and Sweep GC) :從'GC Roots'集合開(kāi)始,將內(nèi)存整個(gè)遍歷一次,保留所有可以被GC Roots直接或間接引用到的對(duì)象,而剩下的對(duì)象都當(dāng)作垃圾對(duì)待并回收,這個(gè)算法需要中斷進(jìn)程內(nèi)其它組件的執(zhí)行并且可能產(chǎn)生內(nèi)存碎片。 復(fù)制算法(Copying) :將現(xiàn)有的內(nèi)存空間分為兩快,每次只使用其中一塊,在垃圾回收時(shí)將正在使用的內(nèi)存中的存活對(duì)象復(fù)制到未被使用的內(nèi)存塊中,之后,清除正在使用的內(nèi)存塊中的所有對(duì)象,交換兩個(gè)內(nèi)存的角色,完成垃圾回收。 標(biāo)記-壓縮算法(Mark-Compact) :先需要從根節(jié)點(diǎn)開(kāi)始對(duì)所有可達(dá)對(duì)象做一次標(biāo)記,但之后,它并不簡(jiǎn)單地清理未標(biāo)記的對(duì)象,而是將所有的存活對(duì)象壓縮到內(nèi)存的一端。之后,清理邊界外所有的空間。這種方法既避免了碎片的產(chǎn)生,又不需要兩塊相同的內(nèi)存空間,因此,其性價(jià)比比較高。 分代 :將所有的新建對(duì)象都放入稱為年輕代的內(nèi)存區(qū)域,年輕代的特點(diǎn)是對(duì)象會(huì)很快回收,因此,在年輕代就選擇效率較高的復(fù)制算法。當(dāng)一個(gè)對(duì)象經(jīng)過(guò)幾次回收后依然存活,對(duì)象就會(huì)被放入稱為老生代的內(nèi)存空間。對(duì)于新生代適用于復(fù)制算法,而對(duì)于老年代則采取標(biāo)記-壓縮算法。以上四種算法信息引用自QQ空間團(tuán)隊(duì)分享 Android GC 那點(diǎn)事 &version=11000003&pass_ticket=nhSGhYD4LC9FWvUPv26Y7AdIzqEDu8FTImf2AKlyrCk%3D) ,總結(jié)的特別棒
導(dǎo)致內(nèi)存泄漏的原因對(duì)象在GC Root中可達(dá),也就是他的引用不為空,所以GC無(wú)法回收它也就會(huì)導(dǎo)致內(nèi)存泄漏
GC Root起點(diǎn)
虛擬機(jī)棧中引用的對(duì)象 方法區(qū)中類靜態(tài)屬性引用的對(duì)象 方法區(qū)中常量引用的對(duì)象 JNI引用的對(duì)象 GC可以續(xù)一秒當(dāng)一個(gè)對(duì)象在引用鏈中失S#x53BB;了引用,那么他就真的要告別世界了嗎,其實(shí)并不是,虛擬機(jī)會(huì)給他“緩刑”,每一個(gè)對(duì)象有一個(gè)finalize() 方法,虛擬機(jī)是否給他緩刑取決于這個(gè)對(duì)象的這個(gè)方法是否被執(zhí)行,如果這個(gè)對(duì)象的這個(gè)方法沒(méi)有被覆蓋或者這個(gè)方法被執(zhí)行過(guò)一次,那么就要“行刑”了。真的是“續(xù)一秒”
如果這個(gè)對(duì)象的finalize()方法應(yīng)該被執(zhí)行,那么虛擬機(jī)會(huì)將它放在F-Queue隊(duì)列中,稍后虛擬機(jī)會(huì)自動(dòng)創(chuàng)建一個(gè)Finalizer線程去執(zhí)行這個(gè)隊(duì)列中的對(duì)象的這個(gè)方法。如果對(duì)象在finalize()中成功自救,舉個(gè)例子,把自己和一個(gè)存在的對(duì)象強(qiáng)引用,那么就不會(huì)被回收,否則就真的被回收了。
但是虛擬機(jī)并不會(huì)保證Finalizer線程執(zhí)行結(jié)束再進(jìn)行回收,因?yàn)槿绻谀骋粋€(gè)對(duì)象的finalize()方法中執(zhí)行了死循環(huán)或者超級(jí)耗時(shí)的操作,虛擬機(jī)等待這個(gè)執(zhí)行結(jié)束的話就會(huì)導(dǎo)致整個(gè)Gc崩潰了
首先注意這個(gè)方法只能被執(zhí)行一次,第二次就會(huì)標(biāo)記了這個(gè)方法被執(zhí)行過(guò)不會(huì)再執(zhí)行了,其次,這個(gè)方法不一定會(huì)被執(zhí)行到,所以不要依賴finalize()去自救。這不是好的做法。
并發(fā)GC和非并發(fā)GCAndroid2.3之后支持了并發(fā)的GC。
非并發(fā)GC : 虛擬機(jī)在執(zhí)行GC的時(shí)候進(jìn)行Stop the world,也就是掛起其他所有的線程,通常會(huì)持續(xù)上百毫秒,一次Mark,然后直接清理兩者的差別:
首先非并發(fā)GC簡(jiǎn)單粗暴,直接掛起所有的線程,此時(shí)Java堆中肯定不會(huì)有任何的添加和修改,此時(shí)去遞歸GC樹(shù),然后標(biāo)記-清理。但是這樣會(huì)造成很大的開(kāi)銷(xiāo),大家都等著你豈不是很沒(méi)面子= =
然而非并發(fā)的GC是一點(diǎn)一點(diǎn)來(lái)的,跟線程同步進(jìn)行這樣就不會(huì)有很長(zhǎng)時(shí)間的等待,但是你要明白一個(gè)道理,想把地掃干凈這段時(shí)間必須沒(méi)人來(lái)踩,所以他要有掛起線程的過(guò)程。
那么并發(fā)是怎么實(shí)現(xiàn)的呢?首先有個(gè)知識(shí)點(diǎn)就是Jvm在分配內(nèi)存的時(shí)候,有兩種方式
指針碰撞:一個(gè)指針,申請(qǐng)一塊內(nèi)存就指針挪動(dòng)相應(yīng)的距離,不會(huì)產(chǎn)生內(nèi)存碎片,這要求內(nèi)存是很規(guī)整的 空閑列表:每次申請(qǐng)一塊內(nèi)存給需要的對(duì)象,然后有一個(gè)列表記錄了哪些位置被申請(qǐng)了,下次申請(qǐng)的時(shí)候就不申請(qǐng)這個(gè)位置,這樣適用于內(nèi)存不是很規(guī)整的情況創(chuàng)建對(duì)象是一個(gè)頻繁的操作,那么我們?nèi)绾伪WC原子性呢??jī)煞N方案
CAS(Compare and Swap)策略配上失敗重試來(lái)保證原子性 每個(gè)線程分配一個(gè)TLAB : 很簡(jiǎn)單,每個(gè)線程自己有自己的一塊內(nèi)存,那么分配的時(shí)候自己鎖自己的分區(qū)就行了,提高了效率我們用的是第二種 233
所以獲取Java堆鎖的時(shí)候,重點(diǎn)來(lái)了,我們逐個(gè)線程去鎖TLAB,而不是一次全鎖住,當(dāng)然提高了并發(fā)GC的效率,所以更快。但是引來(lái)的問(wèn)題就是并發(fā)的問(wèn)題,所以下一步要重復(fù)去修改在一個(gè)個(gè)探索時(shí)候被改的對(duì)象。也就需要更多的CPU資源。
我們?yōu)槭裁匆P(guān)注GC首先我們知道虛擬機(jī)如何去GC才能了解到如何讓一個(gè)對(duì)象被正確的回收,這樣才不能內(nèi)存泄漏
其次無(wú)論是并發(fā)GC還是非并發(fā)GC都會(huì)導(dǎo)致掛起其他的所有線程,那么就會(huì)帶來(lái)程序卡頓。
ART在GC上做到了更加細(xì)粒度的控制,可以更加流暢的GC
常見(jiàn)的內(nèi)存泄漏案例:Handler內(nèi)存泄漏首先鋪墊一句話:非靜態(tài)的內(nèi)部類和匿名類會(huì)隱式的持有外部類的引用
public class MainActivity extends AppCompatActivity { private Handler mHandler = new Handler() {@Overridepublic 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分鐘之后執(zhí)行finish(); }}
這段代碼有很明顯的內(nèi)存泄漏,首先Handler和Runnable都是匿名內(nèi)部類的實(shí)例,他們都會(huì)持有MainActivity的引用,
Handler發(fā)送的消息到了消息隊(duì)列中 Activity被結(jié)束掉 這個(gè)消息中包含了Handler的引用,Handler包含了Activity的引用,而且他還是個(gè)Runnable,也是匿名內(nèi)部類,也間接包含了MainActivity引用 在Main Lopper中,當(dāng)此消息被取出來(lái),這未執(zhí)行的10分鐘里面,MainActivity沒(méi)法回收 內(nèi)存泄漏有人可能會(huì)說(shuō)短暫的內(nèi)存泄漏又能怎樣?這是錯(cuò)誤的想法,因?yàn)橹灰l(fā)生內(nèi)存泄漏,在這段時(shí)間只要進(jìn)行了大內(nèi)存的操作(比如加載一個(gè)照片墻),就有風(fēng)險(xiǎn)因?yàn)檫@個(gè)內(nèi)存泄漏造成OOM(占用內(nèi)存肯定剩下的少了)
上面這個(gè)如何修改呢?
將Runnable和Handler改成static 或者在外部定義內(nèi)部使用。
其他常見(jiàn)的內(nèi)存泄漏 靜態(tài)變量?jī)?nèi)存泄漏:使用靜態(tài)變量來(lái)引用一個(gè)事物,在不使用之后沒(méi)有下掉,那么引用存在就會(huì)一直泄漏 單例導(dǎo)致的內(nèi)存泄漏:使用的單例中保存了不應(yīng)該被一直持有的對(duì)象,那么就會(huì)造成內(nèi)存泄漏 由第三方庫(kù)使用不當(dāng)導(dǎo)致的內(nèi)存泄漏:比如EventBus,Activity銷(xiāo)毀的時(shí)候沒(méi)有反注冊(cè)就會(huì)導(dǎo)致引用一直被持有無(wú)法回收 還有很多。。。他們都是因?yàn)橐脹](méi)有被清理造成的 如何查看內(nèi)存泄漏簡(jiǎn)單粗暴 —> LeakCanary: Square出品的庫(kù),當(dāng)出現(xiàn)內(nèi)存泄漏的時(shí)候會(huì)出現(xiàn)
精打細(xì)算 —> Android Studio 內(nèi)存工具: 可以Dump下來(lái)當(dāng)前的內(nèi)存路徑,然后分析出來(lái)哪些對(duì)象目前的狀態(tài)。很強(qiáng)
參考文獻(xiàn) 深入理解Java虛擬機(jī) Form: 周志明 Android GC那點(diǎn)事 &version=11000003&pass_ticket=nhSGhYD4LC9FWvUPv26Y7AdIzqEDu8FTImf2AKlyrCk%3D) Form: QQ空間終端團(tuán)隊(duì) 細(xì)話Java:'失效'的private修飾符 Form: 技術(shù)小黑屋 避免Android中Context引起的內(nèi)存泄露 Form: 技術(shù)小黑屋 Handler引起的內(nèi)存泄漏 Form: 技術(shù)小黑屋 Android 內(nèi)存泄漏案例和解析 Form: drakeet來(lái)自:https://techblog.toutiao.com/2017/08/16/untitled-4/
相關(guān)文章:
1. IntelliJ IDEA安裝插件的方法步驟2. 《CSS3實(shí)戰(zhàn)》筆記--漸變?cè)O(shè)計(jì)(一)3. python如何寫(xiě)個(gè)俄羅斯方塊4. JavaScript設(shè)計(jì)模式之策略模式實(shí)現(xiàn)原理詳解5. JAVA抽象類及接口使用方法解析6. IntelliJ IDEA配置Tomcat服務(wù)器的方法7. python b站視頻下載的五種版本8. 如何通過(guò)vscode運(yùn)行調(diào)試javascript代碼9. JS數(shù)據(jù)類型判斷的幾種常用方法10. 本站用的rss輸出
