深入理解Java中的記憶體洩漏

answer_05發表於2017-12-16

理解Java中的記憶體洩漏,我們首先要清楚Java中的記憶體區域分配問題和記憶體回收的問題本文將分為三大部分介紹這些內容。

Java中的記憶體分配

Java中的記憶體區域主要分為執行緒共享的和執行緒私有的兩大區域:

深入理解Java中的記憶體洩漏

  • Java堆:在虛擬機器啟動時建立,是所有執行緒共享的一塊記憶體區域。存放了所有的new出來的物件的例項和陣列,物件的reference則在虛擬機器棧上。
  • 方法區:與Java堆一樣,是各個執行緒共享的記憶體區域,用於儲存被虛擬機器載入的類資訊、常量、靜態變數、即時編譯後的程式碼等資料
  • 虛擬機器棧:每個方法在執行過程中都會建立一個棧幀,用來儲存區域性變數表、運算元棧、動態連結、方法出口等資訊。每個方法從開始執行到執行結束都對應著一個棧幀在虛擬機器中從入棧到出棧的過程。我們通常說的棧記憶體就是指的是虛擬機器棧的本地變數表部分,主要儲存了基本資料型別和物件的引用。
  • 程式計數器:是執行緒執行位元組碼檔案位置的指示器,用於執行緒切換後,再次切換回來能準確執行上次執行到的位元組碼檔案的位置。
  • 本地方法棧:和虛擬機器棧類似,只是記錄的是Native方法。

介紹了Java的記憶體分配問題,通過一段程式碼來進行一下總結

public static void main(String[] args) {
         Animal animal = new Animal();
    }
publci class Animal{
    public static String address = "the earth"
    public Animal(){
        
    }
}
複製程式碼

在main方法中,我們new Animal(),首先,檢查記憶體中是否載入了Animal.class,如果沒有載入,則會先將Animal.class載入到方法區中,同時在方法區開闢一塊記憶體區域,用於儲存static 的"the earth"(這裡更能清晰的理解static這個關鍵字,就是static修飾的變數是屬於類的,而不是屬於類的某一個物件的),隨後在Java堆中開闢一塊記憶體區域,用於儲存new Animal()這個物件,在虛擬機器棧中的animal會指向這個物件的地址。

深入理解Java中的記憶體洩漏

Java中的記憶體回收機制

Java記憶體回收主要是指的是Java堆上的已經分配給物件的記憶體回收,判斷Java堆上的記憶體是否被回收,主要是通過可達性分析演算法:從一系列可以作為 GC Roots 的物件開始向下搜尋,搜尋走過的路徑稱為引用鏈,當GC回收時,一個物件沒有通過引用鏈與任何GC Roots物件連線,則這個物件就可以被回收了。可作為GC Roots物件的有以下幾種:

  • 虛擬機器棧的本地變數表中引用的物件。
  • 方法區中靜態屬性和常量引用的物件。
  • 本地Native方法引用的物件。

需要注意的是GC Roots並不是一直可以作為GC Roots的,eg:

public void testGc(){
    Person person = new Person();
}
複製程式碼

根據GC Roots的定義,new Person()這個物件被person所引用,person在虛擬機器棧中,所以new Person()這個物件是作為了GC Roots的,但是當這個testGc()方法執行完成,person釋放記憶體,new Person()這個物件就沒有其他的引用,就不再是GC Roots。

Java中的記憶體洩漏

記憶體洩漏需要和記憶體溢位區分開來,記憶體洩漏是指:部分記憶體我們不再需要了,但是虛擬機器不能回收,洩漏過多就會造成記憶體溢位。就是部分物件我們已經用不上了,但是這些物件和GC Roots存在一定程度上的引用關係,導致不能被垃圾回收機制回收。

幾種常見的記憶體洩漏

  • 非靜態內部類的靜態例項
public class InnerStactivity extends AppCompatActivity {

    private static Object ininerClass;
    @Override
    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_inner_stactivity);

    }

    class InnerClass{

    }

    public void startInnerClass(){

        ininerClass = new InnerClass();

    }

    /**
     * 建立了InnerClass的靜態例項引用
     * @param view
     */
    public void createInner(View view) {

        startInnerClass();

    }
}
複製程式碼

當static Object ininerClass指向了newInnerClass()這個物件時,這個物件就可以作為了GC Roots,同時非靜態的內部類會持有外部類的引用,InnerStactivity就會在GC Roots的引用鏈上,當我們需要離開這個InnerStactivity,並且不再需要這個InnerStactivity時,這個InnerStactivity並不能被回收。

  • 匿名內部類的靜態例項

  • Handler記憶體洩漏

public class HandlerActivity extends AppCompatActivity {

    Handler mHandler;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_handler);
        mHandler= new Handler(){
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
                show();
            }
        };
    }

    public void sendMessage(View view) {
        mHandler.sendEmptyMessageDelayed(1,1000*30);
        finish();
    }

    public void show(){

    }

}
複製程式碼

深入理解Java中的記憶體洩漏

可以看到我們這麼寫Android studio已經提示了警告,提示我們應該用static修飾handler物件,否則會造成記憶體的洩漏,這是不容易犯的錯誤。

Handler這麼寫之所以會出現記憶體洩漏是因為:Message會持有一個對Handler的引用,當這個Handler是非靜態內部類的時候,又會持有一個對外部類的引用(比如Activity)。如果傳送一條延時的Message,由於這個Message會長期存在於佇列中,就會導致Handler長期持有對Activity的引用,從而引起檢視和資源洩漏。當你傳送一條延時的Mesaage,並且把這個Message儲存在某些地方(例如靜態結構中)備用,記憶體洩漏會變得更加嚴重。

我們現在都通過static修飾 Handler類,並通過弱引用來和當前介面的Activity互動,並在onDestory()中去除Handler的所有的訊息來規避可能出現的記憶體洩漏。

public class HandlerImproveActivity extends AppCompatActivity {

    Handler mHandler;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_handler);
        mHandler= new ImproveHandler(HandlerImproveActivity.this);
    }

    public void sendMessage(View view) {
        mHandler.sendEmptyMessageDelayed(1,1000*30);
        finish();
    }

    private static class ImproveHandler extends Handler{

        private WeakReference<HandlerImproveActivity> mActivity;

        public ImproveHandler(HandlerImproveActivity improveActivity) {
            this.mActivity = new WeakReference<HandlerImproveActivity>(improveActivity);
        }


        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if (mActivity != null && mActivity.get() != null) {
                mActivity.get().show();
            }
        }
    }

    public void show(){

    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if(mHandler != null){
            mHandler.removeCallbacksAndMessages(null);
        }

    }
}
複製程式碼
  • 未正確使用Context 在開發中會有大量的使用到Context的地方,如果不是必須使用到Activity的Context(例如Dialog就必須使用到activity的Context),則都可以使用getApplicationContext()來替代。
 AlertDialog alertDialog= new AlertDialog.Builder(this).create();
 Toast.makeText(getApplicationContext(), "", Toast.LENGTH_SHORT).show();
複製程式碼
  • 註冊未取消: 在使用觀察者模式的時候,在register後,完成時要即時unRegister監聽器。在某個Activity介面使用Rxjava進行網路請求,在離開這個頁面的時候一定要取消註冊。
  • 資源物件未關閉: 資源物件比如Cursor、File等,不使用的時候應該關閉它們。把他們的引用置為null,而不關閉它們,往往會造成記憶體洩漏。因此,在資源物件不使用時,一定要確保它已經關閉,通常在finally語句中關閉,防止出現異常時,資源未被釋放的問題。

Android Developer中關於如何管理記憶體的連結 developer.android.com/topic/perfo…

相關文章