效能優化——記憶體洩漏(1)入門篇

GitLqr發表於2019-02-16

記憶體洩漏系列文章:
效能優化——記憶體洩漏(1)入門篇
效能優化——記憶體洩漏(2)工具分析篇
效能優化——記憶體洩漏(3)程式碼分析篇

一、簡述

本篇是作為記憶體洩漏入門,主要說的是一些關於記憶體洩漏的概念,包括什麼是記憶體洩漏,記憶體分配的幾種策略,為什麼會造成記憶體洩漏 及 如何避免記憶體洩漏等。

1、避免記憶體洩露的重要性

對於一個APP的評測,最直接的評分點就是使用者體驗,使用者體驗除介面設計外,就數APP是否執行流暢較為重要,當APP中出現越來越多記憶體洩漏時,卡頓特效就會隨之而來。類比下電腦,cpu效能低下或記憶體不足時,程式執行效率就會降低,常見的現象就是執行卡頓。或許你會說現在的安卓手機配置多牛逼,8核的驍龍cpu,4G的執行記憶體,流暢的執行一個app足夠啦,但實際情況是這樣的嗎?一個安卓APP是執行在一個dalvik虛擬機器上的,系統分配給一個dalvik虛擬機器的記憶體是固定的,如:16M,32M,64M(不同手機分配的記憶體不一樣,可能現在的國產機分配的記憶體會更大,但絕對不會分配全部記憶體給一個安卓APP),分配給一個APP的執行記憶體只有幾十M,想想是不是有點少了呢?所以在這有限的執行記憶體中,想讓一個APP一直流暢的執行,解決記憶體洩漏是十分必要的。

2、java與c/c++的對比

作為一個使用java開發的程式設計師,我們知道,java比c/c++更“高階”,這裡的“高階”不是說java比其他語言好(我不想引起聖戰哈~),而是說java在記憶體申請與回收方面不需要人為管理,而c/c++則需要自己去分配記憶體和釋放記憶體。下面對比下兩者之間的差別:

  1. 申請記憶體:
    java只要在程式碼中new一個Object,系統就會自己計算並分配好記憶體大小;而c/c++則相對麻煩,需要呼叫malloc(size_t size),手動計算並傳入要分配的記憶體值。

  2. 釋放記憶體:
    java有回收機制,即GC,不需要呼叫(也可以通過程式碼呼叫),一段時間後便會自己去回收已經不需要的記憶體;而c/c++則需要手動呼叫free(void *ptr)來釋放指標指向的記憶體空間。

所以說java比c/c++更“高階”,但是java的垃圾回收機制也沒有那麼智慧,因為它在執行垃圾回收時需要根據一個標準去判斷這塊記憶體是否是垃圾,當這塊垃圾不符合作為垃圾的標準時,GC就不會去回收它,這就產生了記憶體洩漏,下面開始進入正題。

  • 上述的標準是:某物件不再有任何的引用時才會進行回收。
  • 這裡的記憶體指的是堆記憶體,堆中存放的就是引用指向的物件實體。

二、基本概念

1、什麼是記憶體洩露

當一個物件已經不需要再使用,本該被回收時,而有另一個正在使用的物件持有它的引用從而就導致物件不能被回收。這種導致了本該被回收的物件不能被回收而停留在堆記憶體中,就產生了記憶體洩漏。簡而言之,記憶體不在GC掌控之內了。

2、java中記憶體分配的幾種策略

1)靜態的

靜態的儲存區:記憶體在程式編譯的時候就已經分配好,這塊記憶體在整個程式的執行期間都一直存在。它主要存放靜態資料、全域性的static資料和一些常量。

2)棧式的

在執行函式(方法)時,函式中的一些內部變數的儲存都可以放在棧中建立,函式執行結束時,這些儲存單元就會自動被釋放。

3)堆式的

也叫動態記憶體分配。java中需要呼叫new來申請分配一塊記憶體,依賴GC機制回收。而c/c++則可以通過呼叫malloc來申請分配一塊記憶體,並且需要自己負責釋放。c/c++是可以自己掌控記憶體的,但要求程式設計師有很高的素養來解決記憶體的問題。而java這塊對程式設計師而言並沒有很好的方法去解決垃圾記憶體,需要在程式設計時就注意自己良好的程式設計習慣。

  • 堆管理很麻煩,頻繁地new/remove會造成大量的記憶體碎片,這樣就會慢慢導致程式效率低下。
  • 對於棧,採用先進後出,完全不會產生碎片,執行效率高且穩定。

下面通過一段程式碼,來說明一個類被建立時,往堆疊都存放了些什麼:

public class Main {
    int a = 1; // a變數在堆中
    Person pa = new Person(); // pa變數在堆中,new Person()例項也在堆中

    public void hehe() {
        int b = 1; // b變數在棧中
        Person pb = new Person(); // pb變數在棧中,但new Person()例項在堆中
    }
}複製程式碼
  • 成員變數全部儲存在堆中(包括基本資料型別,引用及引用的物件實體)——因為它們屬於類,類的例項是存放在堆中的。
  • 區域性變數的基本資料型別和引用儲存於棧中,引用的物件實體儲存在堆中。——因為它們屬於方法當中的變數,生命週期會隨著方法一直結束。

3、java中一些特殊類

型別 回收時機 使用 生命週期
StrongReference 強引用 從不回收 物件的一般儲存 JVM停止是才會終止
SoftReference 軟引用 當記憶體不足時 SoftReference結合ReferenceQueue,有效期短 記憶體不足時終止
WeakReference 弱引用 在垃圾回收時 同軟體引用 GC後終止
PhatomReference 虛引用 在垃圾回收時 結合ReferenceQueue來跟蹤物件被垃圾回收期回收的活動 GC後終止

開發時,為了防止記憶體溢位,處理一些比較佔用記憶體並且生命週期長的物件時,可以儘量使用軟引用和弱引用。

三、例項

1、記憶體洩露例子

單例模式導致物件無法釋放從而造成記憶體洩露

/**
 * @建立者 CSDN_LQR
 * @描述 一個簡單的單例
 */
public class CommonUtil {
    private static CommonUtil mInstance;
    private Context mContext;
    public CommonUtil(Context context) {
        mContext = context;
    }
    public static CommonUtil getmInstance(Context context) {
        if (mInstance == null) {
            synchronized (CommonUtil.class) {
                if (mInstance == null) {
                    mInstance = new CommonUtil(context);
                }
            }
        }
        return mInstance;
    }
    ...
}複製程式碼

這種單例工具類在開發中是很常見的,它本身並沒有什麼問題。但如果使用不善,那問題就來了:

/**
 * @建立者 CSDN_LQR
 * @描述 記憶體洩漏
 */
public class MemoryLeakActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_memory_leak);
        CommonUtil.getmInstance(this);
    }
}複製程式碼

在MemoryLeakActivity中獲取CommonUtil物件時,把自己作為引數傳給了CommonUtil,這會有什麼問題呢?因為CommonUtil物件使用了static修飾,是靜態變數,在整個APP的執行期內,GC不會回收CommonUtil例項,並且它持有了傳入的Activity,當Activity呼叫onDestroy()銷燬時(例如螢幕旋轉時,Activity會重建),發現自己還被其他變數引用了,所以該Activity也不會被回收銷燬。

2、Memory Monitor的簡單使用

Android Studio提供了一套Monitors工具,可以實時檢視APP的記憶體分配、CPU佔用及網路等情況,本篇主要針對記憶體分配,所以使用Memory Monitor來驗證上面的說法。

1)找到Memory Monitor

圖中的幾個說明很詳細,請細看。

MemoryMonitors
MemoryMonitors

2)執行APP,證明記憶體洩漏

先開啟APP,看到目前分配的記憶體為3.43MB。

程式預設佔用記憶體
程式預設佔用記憶體

接著開啟MemoryLeakActivity介面(從這裡開始),檢視到APP目前分配的記憶體為3.51MB。

開啟介面後檢視記憶體佔用
開啟介面後檢視記憶體佔用

我旋轉下螢幕,可以看到APP目前分配的記憶體增加到了3.60MB。(可以認為每建立一個簡單的Activity就會佔用大約0.1MB記憶體)

旋轉螢幕後,檢視記憶體佔用
旋轉螢幕後,檢視記憶體佔用

點選Initiate GC(啟動GC),再點選Dump Java Heap(獲取當前記憶體快照)。

啟動GC並獲取當前記憶體快照
啟動GC並獲取當前記憶體快照

在Capture區找到剛剛獲取的記憶體快照,找到MemoryLeakActivity,可以發現記憶體中有2個例項。
其實上一步中點選Initiate去啟動GC,只是證明豎屏時建立的MemoryLeakActivity已經沒辦法被GC回收,也就是MemoryLeakActivity[0]不在GC的掌握之內,即記憶體洩漏了。

記憶體快照
記憶體快照

分別點選MemoryLeakActivity例項0和1,可以看到堅屏MemoryLeakActivity[0]還被CommonUtil引用,而橫屏MemoryLeakActivity[1]沒有被CommonUtil引用。

堅屏MemoryLeakActivity
堅屏MemoryLeakActivity

橫屏MemoryLeakActivity
橫屏MemoryLeakActivity

3、為什麼會記憶體洩漏

如果不在onCreate()中獲取CommonUtil物件的話,在改變螢幕方向後,豎屏的MemoryLeakActivity在呼叫onDestroy()時,會被GC回收。而這裡出現了記憶體洩漏,就是因為在程式碼中獲取CommonUtil物件搞的鬼。詳情如下圖所示:

螢幕旋轉
螢幕旋轉

4、解決方案

既然CommonUtil例項是靜態的,存在於整個APP生命週期中,而ApplicationContext在整個APP的生命週期中也一直存在,那就給它傳ApplicationContext物件即可。程式碼修改如下:

public class MemoryLeakActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_memory_leak);
        CommonUtil.getmInstance(getApplicationContext());
    }
}複製程式碼

之後重覆上述步驟,可以看到,記憶體中只有一個MemoryLeakActivity例項了。

沒有記憶體洩漏了
沒有記憶體洩漏了

相關文章