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