性能優化——內存泄漏(1)入門篇

內存泄漏系列文章:
性能優化——內存泄漏(1)入門篇
性能優化——內存泄漏(2)工具分析篇
性能優化——內存泄漏(3)代碼分析篇java

1、簡述

本篇是做爲內存泄漏入門,主要說的是一些關於內存泄漏的概念,包括什麼是內存泄漏,內存分配的幾種策略,爲何會形成內存泄漏 及 如何避免內存泄漏等。c++

一、避免內存泄露的重要性

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

二、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就不會去回收它,這就產生了內存泄漏,下面開始進入正題。app

  • 上述的標準是:某對象再也不有任何的引用時纔會進行回收。
  • 這裏的內存指的是堆內存,堆中存放的就是引用指向的對象實體。

2、基本概念

一、什麼是內存泄露

當一個對象已經不須要再使用,本該被回收時,而有另外一個正在使用的對象持有它的引用從而就致使對象不能被回收。這種致使了本該被回收的對象不能被回收而停留在堆內存中,就產生了內存泄漏。簡而言之,內存不在GC掌控以內了。ide

二、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()實例在堆中
    }
}複製代碼
  • 成員變量所有存儲在堆中(包括基本數據類型,引用及引用的對象實體)——由於它們屬於類,類的實例是存放在堆中的。
  • 局部變量的基本數據類型和引用存儲於棧中,引用的對象實體存儲在堆中。——由於它們屬於方法當中的變量,生命週期會隨着方法一直結束。

三、java中一些特殊類

類型 回收時機 使用 生命週期
StrongReference 強引用 從不回收 對象的通常保存 JVM中止是纔會終止
SoftReference 軟引用 當內存不足時 SoftReference 結合ReferenceQueue,有效期短 內存不足時終止
WeakReference 弱引用 在垃圾回收時 同軟件引用 GC後終止
PhatomReference 虛引用 在垃圾回收時 結合ReferenceQueue來跟蹤對象被垃圾回收期回收的活動 GC後終止

開發時,爲了防止內存溢出,處理一些比較佔用內存而且生命週期長的對象時,能夠儘可能使用軟引用和弱引用。

3、實例

一、內存泄露例子

單例模式致使對象沒法釋放從而形成內存泄露

/**
 * @建立者 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也不會被回收銷燬。

二、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

三、爲何會內存泄漏

若是不在onCreate()中獲取CommonUtil對象的話,在改變屏幕方向後,豎屏的MemoryLeakActivity在調用onDestroy()時,會被GC回收。而這裏出現了內存泄漏,就是由於在代碼中獲取CommonUtil對象搞的鬼。詳情以下圖所示:

屏幕旋轉
屏幕旋轉

四、解決方案

既然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實例了。

沒有內存泄漏了
沒有內存泄漏了
相關文章
相關標籤/搜索