Android面試被問(wèn)到內(nèi)存泄漏了雜整?
內(nèi)存泄漏即該被釋放的內(nèi)存沒(méi)有被及時(shí)的釋放,一直被某個(gè)或某些實(shí)例所持有卻不再使用導(dǎo)致GC不能回收。
Java內(nèi)存分配策略
Java程序運(yùn)行時(shí)的內(nèi)存分配策略有三種,分別是靜態(tài)分配,棧式分配,和堆式分配。對(duì)應(yīng)的三種策略使用的內(nèi)存空間是要分別是靜態(tài)存儲(chǔ)區(qū)(也稱(chēng)方法區(qū)),棧區(qū),和堆區(qū)。
- 靜態(tài)存儲(chǔ)區(qū)(方法區(qū)):主要存放靜態(tài)數(shù)據(jù),全局static數(shù)據(jù)和常量。這塊內(nèi)存在程序編譯時(shí)就已經(jīng)分配好,并且在程序整個(gè)運(yùn)行期間都存在。
- 棧區(qū):當(dāng)方法執(zhí)行時(shí),方法內(nèi)部的局部變量都建立在棧內(nèi)存中,并在方法結(jié)束后自動(dòng)釋放分配的內(nèi)存。因?yàn)闂?nèi)存分配是在處理器的指令集當(dāng)中所以效率很高,但是分配的內(nèi)存容量有限。
- 堆區(qū):又稱(chēng)動(dòng)態(tài)內(nèi)存分配,通常就是指在程序運(yùn)行時(shí)直接new出來(lái)的內(nèi)存。這部分內(nèi)存在不適用時(shí)將會(huì)由Java垃圾回收器來(lái)負(fù)責(zé)回收。
棧與堆的區(qū)別:
在方法體內(nèi)定義的(局部變量)一些基本類(lèi)型的變量和對(duì)象的引用變量都在方法的棧內(nèi)存中分配。當(dāng)在一段方法塊中定義一個(gè)變量時(shí),Java就會(huì)在棧中為其分配內(nèi)存,當(dāng)超出變量作用域時(shí),該變量也就無(wú)效了,此時(shí)占用的內(nèi)存就會(huì)釋放,然后會(huì)被重新利用。
堆內(nèi)存用來(lái)存放所有new出來(lái)的對(duì)象(包括該對(duì)象內(nèi)的所有成員變量)和數(shù)組。在堆中分配的內(nèi)存,由Java垃圾回收管理器來(lái)自動(dòng)管理。在堆中創(chuàng)建一個(gè)對(duì)象或者數(shù)組,可以在棧中定義一個(gè)特殊的變量,這個(gè)變量的取值等于數(shù)組或?qū)ο笤诙褍?nèi)存中的首地址,這個(gè)特殊的變量就是我們上面提到的引用變量。我們可以通過(guò)引用變量來(lái)訪問(wèn)堆內(nèi)存中的對(duì)象或者數(shù)組。
舉個(gè)例子:
- public class Sample {
- int s1 = 0;
- Sample mSample1 = new Sample();
- public void method() {
- int s2 = 0;
- Sample mSample2 = new Sample();
- }
- }
- Sample mSample3 = new Sample();
如上局部變量s2和mSample2存放在棧內(nèi)存中,mSample3所指向的對(duì)象存放在堆內(nèi)存中,包括該對(duì)象的成員變量s1和mSample1也存放在堆中,而它自己則存放在棧中。
結(jié)論:
局部變量的基本類(lèi)型和引用存儲(chǔ)在棧內(nèi)存中,引用的實(shí)體存儲(chǔ)在堆中。——因它們存在于方法中,隨方法的生命周期而結(jié)束。
成員變量全部存儲(chǔ)于堆中(包括基本數(shù)據(jù)類(lèi)型,引用和引用的對(duì)象實(shí)體)。——因?yàn)樗鼈儗儆陬?lèi),類(lèi)對(duì)象終究要被new出來(lái)使用。
了解了Java的內(nèi)存分配之后,我們?cè)賮?lái)看看Java是怎么管理內(nèi)存。
Java是如何管理內(nèi)存
由程序分配內(nèi)存,GC來(lái)釋放內(nèi)存。內(nèi)存釋放的原理為該對(duì)象或者數(shù)組不再被引用,則JVM會(huì)在適當(dāng)?shù)臅r(shí)候回收內(nèi)存。
內(nèi)存管理算法:
1. 引用計(jì)數(shù)法:對(duì)象內(nèi)部定義引用變量,當(dāng)該對(duì)象被某個(gè)引用變量引用時(shí)則計(jì)數(shù)加1,當(dāng)對(duì)象的某個(gè)引用變量超出生命周期或者引用了新的變量時(shí),計(jì)數(shù)減1。任何引用計(jì)數(shù)為0的對(duì)象實(shí)例都可以被GC。這種算法的優(yōu)點(diǎn)是:引用計(jì)數(shù)收集器可以很快的執(zhí)行,交織在程序運(yùn)行中。對(duì)程序需要不被長(zhǎng)時(shí)間打斷的實(shí)時(shí)環(huán)境比較有利。缺點(diǎn):無(wú)法檢測(cè)出循環(huán)引用。
引用計(jì)數(shù)無(wú)法解決的循環(huán)引用問(wèn)題如下:
- public void method() {
- //Sample count=1
- Sample ob1 = new Sample();
- //Sample count=2
- Sample ob2 = new Sample();
- //Sample count=3
- ob1.mSample = ob2;
- //Sample count=4
- ob2.mSample = ob1;
- //Sample count=3
- ob1=null;
- //Sample count=2
- ob2=null;
- //計(jì)數(shù)為2,不能被GC
- }
Java可以作為GC ROOT的對(duì)象有:虛擬機(jī)棧中引用的對(duì)象(本地變量表),方法區(qū)中靜態(tài)屬性引用的對(duì)象,方法區(qū)中常量引用的對(duì)象,本地方法棧中引用的對(duì)象(Native對(duì)象)
2. 標(biāo)記清除法:從根節(jié)點(diǎn)集合進(jìn)行掃描,標(biāo)記存活的對(duì)象,然后再掃描整個(gè)空間,對(duì)未標(biāo)記的對(duì)象進(jìn)行回收。在存活對(duì)象較多的情況下,效率很高,但是會(huì)造成內(nèi)存碎片。
3. 標(biāo)記整理算法:同標(biāo)記清除法,只不過(guò)在回收對(duì)象時(shí),對(duì)存活的對(duì)象進(jìn)行移動(dòng)。雖然解決了內(nèi)存碎片的問(wèn)題但是增加了內(nèi)存的開(kāi)銷(xiāo)。
4. 復(fù)制算法:此方法為克服句柄的開(kāi)銷(xiāo)和解決堆碎片。把堆分為一個(gè)對(duì)象面和多個(gè)空閑面。把存活的對(duì)象copy到空閑面,主要空閑面就變成了對(duì)象面,原來(lái)的對(duì)象面就變成了空閑面。這樣增加了內(nèi)存的開(kāi)銷(xiāo),且在交換過(guò)程中程序會(huì)暫停執(zhí)行。
5. 分代算法:分代垃圾回收策略,是基于:不同的對(duì)象的生命周期是不一樣的。因此,不同生命周期的對(duì)象可以采取不同的回收算法,以便提高回收效率。
年輕代:
1. 所有新生成的對(duì)象首先都是存放在年輕代。年輕代的目標(biāo)就是盡可能快速的收集掉那些生命周期短的對(duì)象。
2. 新生代內(nèi)存按照8:1:1的比例分為一個(gè)eden區(qū)和兩個(gè)survivor(survivor0,survivor1)區(qū)。一個(gè)Eden區(qū),兩個(gè) Survivor區(qū)(一般而言)。大部分對(duì)象在Eden區(qū)中生成。回收時(shí)先將eden區(qū)存活對(duì)象復(fù)制到一個(gè)survivor0區(qū),然后清空eden區(qū),當(dāng)這個(gè)survivor0區(qū)也存放滿(mǎn)了時(shí),則將eden區(qū)和survivor0區(qū)存活對(duì)象復(fù)制到另一個(gè)survivor1區(qū),然后清空eden和這個(gè)survivor0區(qū),此時(shí)survivor0區(qū)是空的,然后將survivor0區(qū)和survivor1區(qū)交換,即保持survivor1區(qū)為空, 如此往復(fù)。
3. 當(dāng)survivor1區(qū)不足以存放 eden和survivor0的存活對(duì)象時(shí),就將存活對(duì)象直接存放到老年代。若是老年代也滿(mǎn)了就會(huì)觸發(fā)一次Full GC,也就是新生代、老年代都進(jìn)行回收
4. 新生代發(fā)生的GC也叫做Minor GC,MinorGC發(fā)生頻率比較高(不一定等Eden區(qū)滿(mǎn)了才觸發(fā))
年老代:
1. 在年輕代中經(jīng)歷了N次垃圾回收后仍然存活的對(duì)象,就會(huì)被放到年老代中。因此,可以認(rèn)為年老代中存放的都是一些生命周期較長(zhǎng)的對(duì)象。
2. 內(nèi)存比新生代也大很多(大概比例是1:2),當(dāng)老年代內(nèi)存滿(mǎn)時(shí)觸發(fā)Major GC即Full GC,F(xiàn)ull GC發(fā)生頻率比較低,老年代對(duì)象存活時(shí)間比較長(zhǎng),存活率標(biāo)記高。
持久代:
用于存放靜態(tài)文件,如Java類(lèi)、方法等。持久代對(duì)垃圾回收沒(méi)有顯著影響,但是有些應(yīng)用可能動(dòng)態(tài)生成或者調(diào)用一些class,例如Hibernate 等,在這種時(shí)候需要設(shè)置一個(gè)比較大的持久代空間來(lái)存放這些運(yùn)行過(guò)程中新增的類(lèi)。
Android常見(jiàn)的內(nèi)存泄漏匯總
集合類(lèi)泄漏
先看一段代碼
- List<Object> objectList = new ArrayList<>();
- for (int i = 0; i < 10; i++) {
- Object o = new Object();
- objectList.add(o);
- o = null;
- }
上面的實(shí)例,雖然在循環(huán)中把引用o釋放了,但是它被添加到了objectList中,所以objectList也持有對(duì)象的引用,此時(shí)該對(duì)象是無(wú)法被GC的。因此對(duì)象如果添加到集合中,還必須從中刪除,最簡(jiǎn)單的方法
- //釋放objectList
- objectList.clear();
- objectList=null;
單例造成的內(nèi)存泄漏
由于單例的靜態(tài)特性使得其生命周期跟應(yīng)用的生命周期一樣長(zhǎng),所以如果使用不恰當(dāng)?shù)脑?huà),很容易造成內(nèi)存泄漏。比如下面一個(gè)典型的例子。
- public class SingleInstanceClass {
- private static SingleInstanceClass instance;
- private Context mContext;
- private SingleInstanceClass(Context context) {
- this.mContext = context;
- }
- public SingleInstanceClass getInstance(Context context) {
- if (instance == null) {
- instance = new SingleInstanceClass(context);
- }
- return instance;
- }
- }
正如前面所說(shuō),靜態(tài)變量的生命周期等同于應(yīng)用的生命周期,此處傳入的Context參數(shù)便是禍端。如果傳遞進(jìn)去的是Activity或者Fragment,由于單例一直持有它們的引用,即便Activity或者Fragment銷(xiāo)毀了,也不會(huì)回收其內(nèi)存。特別是一些龐大的Activity非常容易導(dǎo)致OOM。
正確的寫(xiě)法應(yīng)該是傳遞Application的Context,因?yàn)锳pplication的生命周期就是整個(gè)應(yīng)用的生命周期,所以沒(méi)有任何的問(wèn)題。
- public class SingleInstanceClass {
- private static SingleInstanceClass instance;
- private Context mContext;
- private SingleInstanceClass(Context context) {
- this.mContext = context.getApplicationContext();// 使用Application 的context
- }
- public SingleInstanceClass getInstance(Context context) {
- if (instance == null) {
- instance = new SingleInstanceClass(context);
- }
- return instance;
- }
- }
- or
- //在Application中定義獲取全局的context的方法
- /**
- * 獲取全局的context
- * @return 返回全局context對(duì)象
- */
- public static Context getContext(){
- return context;
- }
- public class SingleInstanceClass {
- private static SingleInstanceClass instance;
- private Context mContext;
- private SingleInstanceClass() {
- mContext=MyApplication.getContext;
- }
- public SingleInstanceClass getInstance() {
- if (instance == null) {
- instance = new SingleInstanceClass();
- }
- return instance;
- }
- }
匿名內(nèi)部類(lèi)/非靜態(tài)內(nèi)部類(lèi)和異步線(xiàn)程
- 非靜態(tài)內(nèi)部類(lèi)創(chuàng)建靜態(tài)實(shí)例造成的內(nèi)存泄漏
我們都知道非靜態(tài)內(nèi)部類(lèi)是默認(rèn)持有外部類(lèi)的引用的,如果在內(nèi)部類(lèi)中定義單例實(shí)例,會(huì)導(dǎo)致外部類(lèi)無(wú)法釋放。如下面代碼:
- public class TestActivity extends AppCompatActivity {
- public static InnerClass innerClass = null;
- @Override
- protected void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- if (innerClass == null)
- innerClass = new InnerClass();
- }
- private class InnerClass {
- //...
- }
- }
當(dāng)TestActivity銷(xiāo)毀時(shí),因?yàn)閕nnerClass生命周期等同于應(yīng)用生命周期,但是它又持有TestActivity的引用,因此導(dǎo)致內(nèi)存泄漏。
正確做法應(yīng)將該內(nèi)部類(lèi)設(shè)為靜態(tài)內(nèi)部類(lèi)或?qū)⒃搩?nèi)部類(lèi)抽取出來(lái)封裝成一個(gè)單例,如果需要使用Context,請(qǐng)按照上面推薦的使用Application 的 Context。當(dāng)然,Application 的 context 不是***的,所以也不能隨便亂用,對(duì)于有些地方則必須使用 Activity 的 Context,對(duì)于Application,Service,Activity三者的Context的應(yīng)用場(chǎng)景如下:
- 匿名內(nèi)部類(lèi)
android開(kāi)發(fā)經(jīng)常會(huì)繼承實(shí)現(xiàn)Activity/Fragment/View,此時(shí)如果你使用了匿名類(lèi),并被異步線(xiàn)程持有了,那要小心了,如果沒(méi)有任何措施這樣一定會(huì)導(dǎo)致泄露。如下代碼:
- public class TestActivity extends AppCompatActivity {
- //....
- private Runnable runnable=new Runnable() {
- @Override
- public void run() {
- }
- };
- @Override
- protected void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- //......
- }
- }
上面的runnable所引用的匿名內(nèi)部類(lèi)持有TestActivity的引用,當(dāng)將其傳入異步線(xiàn)程中,線(xiàn)程與Activity生命周期不一致就會(huì)導(dǎo)致內(nèi)存泄漏。
- Handler造成的內(nèi)存泄漏
Handler造成內(nèi)存泄漏的根本原因是因?yàn)椋琀andler的生命周期與Activity或者View的生命周期不一致。Handler屬于TLS(Thread Local Storage)生命周期同應(yīng)用周期一樣??聪旅娴拇a:
- public class TestActivity extends AppCompatActivity {
- private Handler mHandler = new Handler() {
- @Override
- public void dispatchMessage(Message msg) {
- super.dispatchMessage(msg);
- }
- };
- @Override
- protected void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- mHandler.postDelayed(new Runnable() {
- @Override
- public void run() {
- //do your things
- }
- }, 60 * 1000 * 10);
- finish();
- }
- }
在該TestActivity中聲明了一個(gè)延遲10分鐘執(zhí)行的消息 Message,mHandler將其 push 進(jìn)了消息隊(duì)列 MessageQueue 里。當(dāng)該 Activity 被finish()掉時(shí),延遲執(zhí)行任務(wù)的Message 還會(huì)繼續(xù)存在于主線(xiàn)程中,它持有該 Activity 的Handler引用,所以此時(shí) finish()掉的 Activity 就不會(huì)被回收了從而造成內(nèi)存泄漏(因 Handler 為非靜態(tài)內(nèi)部類(lèi),它會(huì)持有外部類(lèi)的引用,在這里就是指TestActivity)。
修復(fù)方法:采用內(nèi)部靜態(tài)類(lèi)以及弱引用方案。代碼如下:
- public class TestActivity extends AppCompatActivity {
- private MyHandler mHandler;
- private static class MyHandler extends Handler {
- private final WeakReference<TestActivity> mActivity;
- public MyHandler(TestActivity activity) {
- mActivity = new WeakReference<>(activity);
- }
- @Override
- public void dispatchMessage(Message msg) {
- super.dispatchMessage(msg);
- TestActivity activity = mActivity.get();
- //do your things
- }
- }
- private static final Runnable mRunnable = new Runnable() {
- @Override
- public void run() {
- //do your things
- }
- };
- @Override
- protected void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- mHandler = new MyHandler(this);
- mHandler.postAtTime(mRunnable, 1000 * 60 * 10);
- finish();
- }
- }
需要注意的是:使用靜態(tài)內(nèi)部類(lèi) + WeakReference 這種方式,每次使用前注意判空。
前面提到了 WeakReference,所以這里就簡(jiǎn)單的說(shuō)一下 Java 對(duì)象的幾種引用類(lèi)型。
Java對(duì)引用的分類(lèi)有 Strong reference, SoftReference, WeakReference, PhatomReference 四種。
ok,繼續(xù)回到主題。前面所說(shuō)的,創(chuàng)建一個(gè)靜態(tài)Handler內(nèi)部類(lèi),然后對(duì) Handler 持有的對(duì)象使用弱引用,這樣在回收時(shí)也可以回收 Handler 持有的對(duì)象,但是這樣做雖然避免了Activity泄漏,不過(guò)Looper 線(xiàn)程的消息隊(duì)列中還是可能會(huì)有待處理的消息,所以我們?cè)贏ctivity的 Destroy 時(shí)或者 Stop 時(shí)應(yīng)該移除消息隊(duì)列 MessageQueue 中的消息。
下面幾個(gè)方法都可以移除 Message:
- public final void removeCallbacks(Runnable r);
- public final void removeCallbacks(Runnable r, Object token);
- public final void removeCallbacksAndMessages(Object token);
- public final void removeMessages(int what);
- public final void removeMessages(int what, Object object);
盡量避免使用 staic 成員變量
如果成員變量被聲明為 static,那我們都知道其生命周期將與整個(gè)app進(jìn)程生命周期一樣。
這會(huì)導(dǎo)致一系列問(wèn)題,如果你的app進(jìn)程設(shè)計(jì)上是長(zhǎng)駐內(nèi)存的,那即使app切到后臺(tái),這部分內(nèi)存也不會(huì)被釋放。按照現(xiàn)在手機(jī)app內(nèi)存管理機(jī)制,占內(nèi)存較大的后臺(tái)進(jìn)程將優(yōu)先回收,意味著如果此app做過(guò)進(jìn)程互保保活,那會(huì)造成app在后臺(tái)頻繁重啟。就會(huì)出現(xiàn)一夜時(shí)間手機(jī)被消耗空了電量、流量,這樣只會(huì)被用戶(hù)棄用。
這里修復(fù)的方法是:
不要在類(lèi)初始時(shí)初始化靜態(tài)成員??梢钥紤]lazy初始化。
架構(gòu)設(shè)計(jì)上要思考是否真的有必要這樣做,盡量避免。如果架構(gòu)需要這么設(shè)計(jì),那么此對(duì)象的生命周期你有責(zé)任管理起來(lái)。
- 避免 override finalize():
- finalize 方法被執(zhí)行的時(shí)間不確定,不能依賴(lài)與它來(lái)釋放緊缺的資源。時(shí)間不確定的原因是: 虛擬機(jī)調(diào)用GC的時(shí)間不確定以及Finalize daemon線(xiàn)程被調(diào)度到的時(shí)間不確定。
- finalize 方法只會(huì)被執(zhí)行一次,即使對(duì)象被復(fù)活,如果已經(jīng)執(zhí)行過(guò)了 finalize 方法,再次被 GC 時(shí)也不會(huì)再執(zhí)行了,原因是:含有 finalize 方法的 object 是在 new 的時(shí)候由虛擬機(jī)生成了一個(gè) finalize reference 在來(lái)引用到該Object的,而在 finalize 方法執(zhí)行的時(shí)候,該 object 所對(duì)應(yīng)的 finalize Reference 會(huì)被釋放掉,即使在這個(gè)時(shí)候把該 object 復(fù)活(即用強(qiáng)引用引用住該 object ),再第二次被 GC 的時(shí)候由于沒(méi)有了 finalize reference 與之對(duì)應(yīng),所以 finalize 方法不會(huì)再執(zhí)行。
- 含有Finalize方法的object需要至少經(jīng)過(guò)兩輪GC才有可能被釋放。
其它
內(nèi)存泄漏檢測(cè)工具強(qiáng)烈推薦 squareup 的 LeakCannary,但需要注意Android版本是4.4+的,否則會(huì)Crash。