1. 程式人生 > >Android:Handler 二三事(二)由記憶體洩漏所想到的(垃圾回收機制)

Android:Handler 二三事(二)由記憶體洩漏所想到的(垃圾回收機制)

主要內容

解決Handler記憶體洩漏以及延伸(垃圾回收、引用等)

解決Handler記憶體洩漏及延伸

為什麼Handler會引起記憶體洩漏?

這是一段使用Handler的程式碼

public class LeakHandlerActivity extends AppCompatActivity {

    private Handler myHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    };

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        myHandler.postDelayed(new Runnable() {
            @Override
            public void run() {

            }
        },1000 * 60 * 3);
    }

}

首先lint會給出提示

展開全部,可以看到

Since this Handler is declared as an inner class, it may prevent the outer class from being garbage collected. If the Handler is using a Looper or MessageQueue for a thread other than the main thread, then there is no issue. If the Handler is using the Looper or MessageQueue of the main thread, you need to fix your Handler declaration, as follows: Declare the Handler as a static class; In the outer class, instantiate a WeakReference to the outer class and pass this object to your Handler when you instantiate the Handler; Make all references to members of the outer class using the WeakReference object.


分析一下大意:

在Java中,非靜態內部類和匿名內部類會持有外部類的引用,因此可能會阻止外部類的垃圾回收。

如果程式在主執行緒以外的執行緒使用Looper或者MessageQueue,沒有問題。但是如果在主執行緒,則需要對Handler做處理;

  1. 將Handler宣告為靜態類或者新寫一個Java檔案用一個類繼承Handler
  2. 因為是靜態類,不持有外部類的引用,如果有對外部類的引用,使用WeakReference ,也就是弱引用。

這裡涉及到幾個問題:

  1. Java的垃圾回收是如何工作的,怎麼就阻止垃圾回收了
  2. 為什麼主執行緒中需要對Handler做處理
  3. 為什麼使用弱引用

Java的垃圾回收機制

Java相對於C++,它的垃圾回收是自動的,不需要寫專門的程式碼去釋放垃圾。

為什麼需要垃圾回收

因為記憶體空間是有限的,如果一直為新物件分配記憶體空間,而不釋放的話,記憶體就會承載不了,就會造成OOM異常。

什麼是垃圾回收機制

顧名思義,就是釋放垃圾所佔的空間。那麼就存在幾個問題:

  • 怎麼判定某個物件是垃圾
  • 怎麼回收
  • 用什麼回收
怎麼判定某個物件是垃圾?

可達性分析法。超出作用域就是不可達的,在作用域內就是可達的。這個演算法是從通過一系列稱為“GC Roots”的物件作為起點,從這些節點向下搜尋,搜尋過的路徑就是引用鏈。當一個物件到GC Roots沒有任何引用鏈的話,那個這個物件就是不可達的,也就是不可用的。這裡需要注意的是,這個物件是從Root搜尋不到,並且經過第一次標記、清理後,仍然沒有復活的物件。

如何選取“GC Roots”物件呢,Java裡,一般可以作為Root物件的有這幾種:

  • 虛擬機器棧中引用的物件
  • 方法區中類靜態屬性引用的物件
  • 方法區中常量引用的物件
  • 本地方法咋棧中JNI(native方法)引用的物件

這幾個跟JVM(Java虛擬機器)執行時資料區有關係。它分為5個部分:程式計數器,Java棧、本地方法棧、方法區、堆

  • 程式計數器:雖然他不像彙編中的程式計數器一樣時物理概念上的CPU暫存器,但是功能是一樣的,都是用來指示執行哪條指令的。在JVM中,多執行緒中是輪流切換著被CPU執行的,一個CPU在某個時刻只會執行一條執行緒中的指令。為了能保證執行緒在切換後能回覆到切換前的執行位置,每個執行緒都需要有自己獨立的程式計數器,而且不能被相互干擾,所以他們是每個執行緒都有的,並且是私有的。它的大小不會隨著程式的執行而改變。
  • Java棧(又叫棧,虛擬機器棧):裡面存放的是一個個棧幀,每個棧幀對應著一個被呼叫的方法,裡面有一個區域性變量表,用來儲存方法中的區域性變數。如果是基本資料型別就儲存值,如果引用型別的變數,就儲存物件的引用。它在編譯騎就確定其大小了。每個執行緒都有自己的棧,並且會不干擾。
  • 本地方法棧:和Java棧類似,區別是Java棧是為Java方法服務的,本地方法棧是為本地方法(Native Method)服務的。
  • 方法區:和堆一樣,是執行緒共享區域。它儲存的是類資訊,靜態變數,常量,以及編譯器編譯後的程式碼等。Java規範中,沒有要求方法區必須實現垃圾回收
  • 堆:唯一一個程式設計師可以管理的區域。Java堆中是用來儲存物件本身以及陣列(陣列引用是存放在棧中)。JVM中只有一個棧,是所有執行緒共享的。Java中有垃圾回收機制來管理這塊區域。

引用記數法也是判斷物件是不是垃圾的方式之一,當物件被引用時,計數器+1,失效時,計數器-1。也即是說,當計數器為0的時候物件就是不可能再被使用了。但是Java中沒有采用這種引用方式。因為它無法處理相互引用。那麼什麼是相互引用呢?

public class TestA {
    public TestB mTestB;
}
public class TestB {
    public TestA mTestA;
}

類A和類B各自持有一個對方類的物件

public class TestGC {

    public static void mian(String[] args) {
        TestA mTestA = new TestA();
        TestB mTestB = new TestB();

        mTestA.mTestB = mTestB;
        mTestB.mTestA = mTestA;

        mTestA = null;
        mTestB = null;
    }
}

即便最後都已經置null了,但是他們還是相互引用著,所以引用計數器還是標記為1。這就是問題,所以目前主流是用可達性分析法。

怎麼回收

這裡需要了解的是Java的記憶體模型。

JVM主要管理兩種記憶體模型:堆和非堆。

  • 堆上面有講到,主要為類例項和資料分配記憶體
  • 非堆是存放類載入資訊等等,它是JVM堆之外的記憶體。
  • 還有一個Other,存放JVM自身程式碼。


堆分為三個代:年輕代,年老代,持久代。


  • 年輕代(Young Gerneration):所有新生成的物件,分為Eden和兩個Servivor區域。
  • 年老代(Old Generation):經歷多次垃圾回收還存放的區域,或者是一些生命週期比較長的物件
  • 持久代(Perm):用於存放靜態檔案,如靜態類,方法等。

一次申請記憶體的過程:

  1. JVM會為Java物件在Eden中初始化一塊記憶體區域
  2. 如果記憶體足夠,OK,申請結束;
  3. 如果不夠,JVM會試圖釋放在Eden中所有不活躍的物件(minor gc)。釋放後如果還不夠,那麼就就試圖將Eden中部分活躍的物件放到Servivor區。
  4. Servivor區用來作為Young和Old區的交換區域。如果Old區的空間足夠,那麼Servivor的物件將會移到Old區,否則留在Servivor區。
  5. 當Old區空間不夠時,將會進行完全的垃圾收集(full gc)
  6. 如果完全垃圾收集後,如果Servivor區以及Old區仍然無法存放從Eden複製過來的物件,導致JVM無法為新物件分配記憶體區域,那麼就出現OOM

從中我們可以看到垃圾回收的時機,以及OOM產生的原因。

年輕代回收使用的的時複製演算法,年老代回收使用的時標記演算法。

複製演算法:先把所有的物件分配到from區域,清理時將所有活動物件複製到to區域,然後清除from區域。然後from區域和to區域互換。每次清理重複這個過程。

標記演算法:標記出所有可以被回收的物件,並且清理空間。

  • 標記演算法的好處是容易實現,但是容易產生記憶體碎片,導致下一次回收提前;
  • 複製演算法的好處是高效而且不容易產生碎片,但是記憶體空間代價高,因為能夠使用的記憶體縮減了一半。而且存活物件很多,那麼複製演算法的效率就會降低
用什麼回收

垃圾回收器。如果回收演算法是理論基礎,那麼回收器就是具體實現。

  • Serial/Serial Old:最古老的收集器。單執行緒收集器,如果進行垃圾收集時,必須暫停所有使用者執行緒。Serial採用複製演算法,針對年輕代,Serial Old採用標記演算法,針對年老代。它的優點是簡單高效,但是會對使用者帶來停頓。
  • ParNew:Serial收集器的多執行緒版本,使用多個執行緒進行垃圾收集。
  • Parallel Scavenge 年輕代的多執行緒收集器。回收期間不需要暫停其他執行緒。採用複製演算法,目的是達到一個可控的吞吐量。
  • Parallel Old: 上一個的年老代版本,使用多執行緒和標記演算法。
  • CMS:以獲取最短回收停頓時間為目標的併發收集器,採用標記演算法。
  • G1:最前沿的成果,併發,面向服務端,能建立可預測的停頓時間模型

為什麼主執行緒中需要對Handler做處理

當一個程式啟動的時候,FrameWork自動為這個應用程式的主執行緒新建一個Looper,這個Loope關聯管著所有主要的框架時事件,比如Activity生命週期,點選事件等,這些事件都是一個個訊息物件,它一直在迴圈處理訊息物件。它存在與應用的生命週期中。

如果Handler在主執行緒中初始化,就會與Looper關聯,訊息傳送到Looper的訊息佇列中時,會有一個Handler的引用,以便處理訊息時可以呼叫handleMessage方法。

當Activity被銷燬時,因為延時訊息會在被處理之前在主執行緒的訊息佇列中有段時間,這個訊息還保留著Handler的引用,Handler因為時匿名內部類又保留著Activity的例項,這些引用會一直保持到訊息被處理,從而導致Activity暫時無法被回收,Activity持有的資源也無法回收,造成記憶體洩漏。

為什麼使用弱引用

Java的四種引用型別:強引用,軟引用,弱引用,虛引用。好處是可以管理物件的生命週期,便於垃圾回收。

強引用:例項化一個物件。強引用只要不為null,有引用變數,那麼就永遠不會被垃圾回收。

 TestA a = new TestA();
 String b = "blabla";

軟引用:如果記憶體足夠,不會回收;如果記憶體不夠,就會回收。常用於圖片快取,網路快取等。

SoftReference<TestA> s = new SoftReference<>(a);

弱引用:無論記憶體是否充足,;垃圾回收器檢測到時,都會被回收。

 WeakReference<TestA> w = new WeakReference<>(a);

虛引用:隨時可能被回收。

解決記憶體洩漏

由上總結,我們需要做的,靜態內部類,弱引用。

    /**
     * 靜態內部類
     */
    private static class MyHandler extends Handler {

        private final WeakReference<Activity> activity;

        public MyHandler(Activity activity) {
            this.activity = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);

            Activity useActivity = activity.get();
            if(useActivity != null) {
                // do something
            }
        }
    }

當然,我們也可以封裝起來,以便複用。

public class BaseHandler<T> extends Handler {

    private final WeakReference<T> weak_reference;
    
    public BaseHandler(T t) {
        weak_reference = new WeakReference<>(t);
    }

    @Override
    public void handleMessage(Message msg) {
        T activity = weak_reference.get();
        if (activity != null) {
            handleMessageWeakActivity(msg, activity);
        }
    }

    public void handleMessageWeakActivity(Message msg, T t) {

    }
}

參考

https://www.cnblogs.com/dolphin0520/p/3613043.html

http://icyfenix.iteye.com/blog/715301

http://www.cnblogs.com/dolphin0520/p/3783345.html

https://blog.csdn.net/ithomer/article/details/6252552

http://ifeve.com/jvm-yong-generation/

https://blog.csdn.net/cpcpcp123/article/details/51262940

https://www.cnblogs.com/xiaoxi/p/6486852.html

https://blog.csdn.net/lqw_student/article/details/52954837

https://blog.csdn.net/swebin/article/details/78571933

感謝@star