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