前言
在日常開(kāi)發(fā)中,可能經(jīng)常會(huì)遇到一些莫名奇妙的崩潰問(wèn)題,但是仔細(xì)查看代碼邏輯卻似乎也找不出代碼中有哪些不對(duì)的邏輯。這時(shí)候就需要仔細(xì)分析,你的代碼中是否存在內(nèi)存泄漏的問(wèn)題。LeakCanary是Square公司開(kāi)源的一款性能優(yōu)化工具,它能夠幫你方便的分析你的app中是否存在內(nèi)存泄漏的問(wèn)題。在使用LeakCanary之前,讓我們先來(lái)了解幾個(gè)概念。
一些概念
Java虛擬機(jī)
相信學(xué)過(guò)Android的你對(duì)Java虛擬機(jī)一定不會(huì)陌生,但還是簡(jiǎn)單的介紹一下Java虛擬機(jī)。如果你學(xué)過(guò)C/C++,一些C/C++書(shū)中都會(huì)強(qiáng)調(diào)你需要手動(dòng)分配內(nèi)存,并在使用之后手動(dòng)回收內(nèi)存。但是好像Java中從來(lái)沒(méi)有人讓你釋放內(nèi)存啊,這一切都需要?dú)w功于Java虛擬機(jī),Java虛擬機(jī)能夠幫我們自動(dòng)釋放可回收內(nèi)存。當(dāng)然,java虛擬機(jī)除了能夠幫助我們自動(dòng)回收內(nèi)存之外,還有很多別的功能,但本文的重點(diǎn)在于內(nèi)存泄漏,而安卓使用的虛擬機(jī)與Java虛擬機(jī)相似。所以下面我們聊一聊Java虛擬機(jī)的垃圾回收吧。-
GC
GC全稱(chēng)(Garbage Collection),也就是垃圾回收,Java虛擬機(jī)主要通過(guò)兩種途徑自動(dòng)幫我們回收內(nèi)存。-
引用計(jì)數(shù)
Java虛擬機(jī)會(huì)給對(duì)象增加一個(gè)引用計(jì)數(shù)器,每當(dāng)程序引用一次對(duì)象,計(jì)數(shù)器就會(huì)加一;反之,每當(dāng)一個(gè)引用計(jì)數(shù)器失效時(shí),計(jì)數(shù)器就會(huì)減一。當(dāng)計(jì)數(shù)器的值為0,則說(shuō)明此對(duì)象沒(méi)有被引用,可以被回收。
舉個(gè)例子Object obj = new Object(); // 計(jì)數(shù)器 + 1 = 1 obj = null; // 計(jì)數(shù)器 - 1 = 0,GC回收 // 但是如果對(duì)象相互調(diào)用,引用計(jì)數(shù)器就無(wú)法使得GC回收 Object a = new Object(); // a的引用計(jì)數(shù)為1 Object b = new Object(); // b的引用計(jì)數(shù)為1 a.next = b; // a的引用計(jì)數(shù)為2 b.next = a; // b的引用計(jì)數(shù)為2 a = null; // a的引用計(jì)數(shù)為1,盡管已經(jīng)顯示地將a賦值為null,但是由于引用計(jì)數(shù)為1,GC無(wú)法回收a b = null; // b的引用計(jì)數(shù)為1,同理,GC也不回收b
為了解決對(duì)象之間相互引用導(dǎo)致的無(wú)法GC的問(wèn)題,Java虛擬機(jī)還有另一種GC策略。
-
可達(dá)性分析
設(shè)立若干根對(duì)象(GC Root) ,每個(gè)對(duì)象都是一個(gè)子節(jié)點(diǎn),當(dāng)一個(gè)對(duì)象找不到根節(jié)點(diǎn),也就是無(wú)人引用時(shí),標(biāo)志其不可達(dá)。
可以作為GC Root的對(duì)象包括:
1.jvm棧中引用的對(duì)象
2.方法區(qū)中靜態(tài)變量引用的對(duì)象
3.方法區(qū)中常量引用的對(duì)象
4.本地方法棧中引用的對(duì)象
5.新生代,活不了多久就死的對(duì)象,比如局部變量,用復(fù)制算法[1]
6.老年代,生命周期長(zhǎng)的對(duì)象,活的久不過(guò)也是會(huì)死的,用標(biāo)記清除算法[2]
7.永久代[3] -----基本上GC不回收
但即使Java虛擬機(jī)已經(jīng)如此優(yōu)秀,它也不能保證所有的可回收內(nèi)存都能正常回收,
-
引用計(jì)數(shù)
-
內(nèi)存泄漏
內(nèi)存泄漏是指在app運(yùn)行的過(guò)程中,由于內(nèi)存并沒(méi)有合理的回收,如:生命周期長(zhǎng)的對(duì)象持有了生命周期短的對(duì)象的引用,導(dǎo)致生命周期短的對(duì)象一直無(wú)法回收。當(dāng)這種情況累積到一定程度,可分配的棧內(nèi)存不足的時(shí)候,就會(huì)導(dǎo)致OOM,我們就看到了程序崩潰。而且這種崩潰不像一般的程序崩潰那樣能夠復(fù)現(xiàn),所以直接由程序崩潰,導(dǎo)致了程序員崩潰。
例如- 在onDestroy()調(diào)用Android活動(dòng)實(shí)例的方法后,不再需要該活動(dòng)實(shí)例,并且在靜態(tài)字段中存儲(chǔ)對(duì)該活動(dòng)的引用將防止其被垃圾回收。
- 添加一個(gè)Fragment到backstack而沒(méi)有在Fragment.onDestroyView()中清除它的view的成員。
- 在一個(gè)對(duì)象中以成員的方式保存了一個(gè)Activity的context,而Activity在配置更改時(shí)依然存在。
- 注冊(cè)的綁定生命周期對(duì)象的監(jiān)聽(tīng)、廣播接收器、RxJava訂閱等,在生命周期結(jié)束的時(shí)候忘記取消注冊(cè)。
OOM
OOM是(Out Of Memory)的簡(jiǎn)稱(chēng),就是內(nèi)存不足的意思,類(lèi)似的問(wèn)題也有StackOverflow,寫(xiě)一個(gè)簡(jiǎn)單的沒(méi)有出口的遞歸函數(shù)就能看到。
使用LeakCanary
內(nèi)存泄漏在安卓app中十分常見(jiàn),小內(nèi)存泄漏的積累會(huì)導(dǎo)致應(yīng)用內(nèi)存不足,并導(dǎo)致OOM崩潰。LeakCanary將幫助我們?cè)陂_(kāi)發(fā)期間找到這些內(nèi)存泄漏。
使用LeakCanary十分簡(jiǎn)單,只需要找到·build.gradle·,并在·dependencies·中加入引用即可。
dependencies {
// debugImplementation because LeakCanary should only run in debug builds.
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.1'
}
我們可以通過(guò)過(guò)濾LeakCanary標(biāo)簽在Logcat中查看。
分析內(nèi)存泄漏
當(dāng)發(fā)生內(nèi)存泄漏的時(shí)候,LeakCanary會(huì)自動(dòng)幫你保存內(nèi)存泄漏信息,并將內(nèi)存泄漏相關(guān)的代碼以另一個(gè)app的形式展示給你,你可以根據(jù)提示,修改代碼,從而解決內(nèi)存泄漏的問(wèn)題。
如何解決內(nèi)存泄漏呢
內(nèi)存泄漏常常存在的原因是因?yàn)閮蓚€(gè)或多個(gè)對(duì)象生命周期不同,同時(shí)存在相互引用。導(dǎo)致生命周期短的對(duì)象被生命周期長(zhǎng)的對(duì)象引用后無(wú)法正?;厥?,從而造成內(nèi)存泄漏。
下面是一個(gè)可能經(jīng)常遇見(jiàn)的內(nèi)存泄漏的小例子:
-
Activity內(nèi)存泄漏
安卓開(kāi)發(fā)中,我們時(shí)常會(huì)把Activity當(dāng)做Context傳入某些單例如UserInfo等類(lèi)中,而我們知道,單例的生命周期可能是整個(gè)Application的生命周期,遠(yuǎn)遠(yuǎn)要比Activity的生命周期要長(zhǎng)。如果使用在單例中某些成員變量保存了Activity的引用,當(dāng)Activity被關(guān)閉的時(shí)候,就會(huì)導(dǎo)致內(nèi)存泄漏了。所以,當(dāng)我們寫(xiě)代碼的時(shí)候,要格外的慎重,如果Context不是必須傳入Activity,我們可以將Context傳入Application的Context。如果實(shí)在非要傳入Activity,你可以在使用完Activity只有,將相關(guān)的成員變量置空,這個(gè)時(shí)候,如果發(fā)成GC,Activity的引用計(jì)數(shù)為0,自然就能正常GC了。
這應(yīng)該是年前寫(xiě)的最后一篇了,希望這篇文章能夠幫到你。
-
最初是將內(nèi)存分為相等的兩塊,只用其中的一塊,當(dāng)這塊內(nèi)存滿(mǎn)的時(shí)候,將其中存活的對(duì)象復(fù)制到另一塊內(nèi)存中,然后GC 回收釋放之前這塊內(nèi)存。 ?
-
就是遍歷GC Root,標(biāo)記可達(dá)不可達(dá)對(duì)象,然后回收不可達(dá)對(duì)象,這種算法缺點(diǎn)是效率低,無(wú)法回收連續(xù)物理內(nèi)存,后來(lái)升級(jí)為標(biāo)記 - 整理算法,將可達(dá)對(duì)象移動(dòng)到內(nèi)存的一端,然后GC回收剩下部分連續(xù)的物理內(nèi)存。 ?
-
分代算法,在Java中,將內(nèi)存中的對(duì)象按照生命周期長(zhǎng)短分成新生代,老年代,永久代 ?