Android內存泄漏問題

Java內存垃圾回收由專門的垃圾回收(Garbage Collector,GC)後臺線程維護,自動回收機制減輕了開發者的負擔,讓開發者能夠更加專注於業務功能的處理。GC回收已經相對比較智能,能夠辨別出簡單的垃圾對象和正常使用的對象,但是對於功能邏輯上的垃圾對象還是無能爲力的,要想理解內存泄漏產生的原因需要對Java內存回收機制有一定的瞭解。

在早期的系統中想要判斷對象是不是垃圾對象通常使用數字引用的方式,有別的對象引用它,引用計數就加一,如果該對象的數字引用值大於0表示有對象還在使用它,如果引用計數爲零那麼系統中就沒有任何對象再引用它,這樣的對象就被認爲是垃圾對象,可以被回收。引用計數存在的問題是當對象之間相互引用,如果相互引用的對象同時不再被需要,但它們的引用計數都還是大於零的。Java垃圾回收機制就沒有再採用引用計數而是基於GCRoot來判定對象是否被引用,Java裏的對象往往需要依賴別的對象提供的方法來完成任務,這種依賴關係就構成了一個有向依賴圖,處於有向依賴圖中的對象都是運行程序必不可少的對象,在有向依賴圖外面的對象就屬於垃圾對象可以被回收。
在這裏插入圖片描述
通常作爲GCRoot的對象包括如下的這些對象:Java運行時棧局部變量,Native方法棧引用的對象,靜態變量,活動線程,等待中的鎖對象等。如果在運行期間某些對象已經不再被應用需要,但是GCRoot裏的對象卻依然保持着它們的引用,GC線程在做垃圾回收的時候就無法回收它們。邏輯垃圾對象由於開發者錯誤的引用導致它們存活時間超出了真正需要存活的時間,也就是說長生命週期的對象持有了短生命週期的對象,這就是內存泄漏問題的本質。
在這裏插入圖片描述
上圖中繪製出了JVM虛擬機在做垃圾對象識別時使用的GCRoot引用查找原理,從圖上的GCRoot無法查找到objC和objD的引用,因此它們即使引用計數都不爲零,但通過GCRoot判定時不可達對象,在後面的垃圾回收就會被直接釋放掉佔用的內存。還需要注意的時前面一直在說活動線程,線程必須處在啓動之後結束之前,這段時間內線程引用的對象纔是可達的,如果線程對象還未啓動或者已經退出運行狀態,它內部引用的各種對象都不是可達的。

活動線程

在探討活動線程導致的內存泄漏之前,需要先了解Java中靜態內部類和普通內部類的區別,首先定義一個Hello的外部類,內部包含static類型的World類,還有一個普通的InnerWorld類,它的定義代碼如下,使用javac將源代碼編譯成.class文件。

// 普通內部類和靜態內部類
public class Hello {
	static class World { // 靜態內部類
		void world() {
			System.out.println("Static World");
		}
}

class InnerWorld { // 普通內部類
	void inner() {
		System.out.println("Inner World");
	}
}

InnerWorld world = new InnerWorld(); // 普通成員屬性沒有問題,有this可以綁定
// static InnerWorld world2 = new InnerWorld(); 靜態成員變量不行,編譯器直接報錯

public static void main(String[] args) {
	Hello hello = new Hello();
	Hello.InnerWorld world = hello.new InnerWorld(); // 需要依賴外部類的對象才能創建
	Hello.World world2 = new Hello.World(); // 不需要外部類對象,和普通類創建一樣
}

代碼中外部類Hello內部定義兩個內部類,World類時靜態內部類,InnerWorld是普通內部類,在main()方法測試時創建World類型的變量直接new即可,而創建InnerWorld類型的變量必須依附在外部Hello類型的變量上,否則編譯器報錯。編譯後查看生成的.class文件有三個,其中Hello.class就是最外部的Hello類,Hello$World代表的是普通的內部類,Hello$InnerWorld是靜態內部類InnerWorld生成的類。JDK中提供了javap工具可以查看.class文件的內部結構,將生成的兩個內部類文件反編譯。

// 普通內部類和靜態內部類反編譯代碼
javap Hello$World
Compiled from "Hello.java"
class Hello$World { // 靜態內部類和普通類沒什麼區別
	Hello$World();
	void world();
}

javap Hello$InnerWorld
Compiled from "Hello.java"
class Hello$InnerWorld { // 普通內部類需要綁定外部對象
	final Hello this$0; // 外部類對象
	Hello$InnerWorld(Hello); // 構造方法的參數就是外部對象
	void inner();
}

可以看到靜態內部類和普通的類沒有多大區別,唯一不同的是類名前面多了外包類Hello$,在看普通內部類它包含了一個Hello類型的成員屬性,該屬性在構造函數中被傳遞進來。這也就是爲什麼創建普通內部類的對象一定要用外部類對象.new操作符或者只能在外部類裏面創建普通內部類對象。在Android應用開發中通常會使用new回調接口生成回調對象,其實編譯器會生成匿名內部類,再在new的位置創建匿名內部類的實例,該回調對象內部就會包含外部對象的強引用。

現在定義簡單的網絡請求工具類HTTPNetUtils,工具類中有回調接口Callback,如果網絡請求成功就回調onSuccess()方法,失敗就會掉onFailure()方法,因爲Android禁止在主線程發起網絡請求,工具類中會使用子線程加載網絡數據並且回調,在回調方法中將展示Toast投遞到主線程隊列中。

// 網絡請求內存泄漏示例
public class HttpNetUtils {
	public interface Callback {
		void onSuccess(String text);
		void onFailure();
	}

	public static void get(final String url, final Callback callback) {
		new Thread(new Runnable() {
			@Override
			public void run() {
				ThreadUtils.sleep(20000); // 模擬網絡比較慢
				HttpURLConnection http = 
(HttpURLConnection) new URL(url).openConnection();
				// 省略HttpURLConnection請求配置
				int statusCode = http.getResponseCode();
				if (statusCode == 200) {
					callback.onSuccess(http.getResponseMessage());
				} else {
					callback.onFailure();
				}
				http.disconnect(); 
			}
		}).start();
	}
}

public class NetworkActivity extends AppCompatActivity {
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_network);
		// 匿名內部類創建的Callback對象會包含NetworkActivity.this對象
		HttpNetUtils.get("http://www.baidu.com", new HttpNetUtils.Callback() {
			@Override
			public void onSuccess(final String text) {
				showSuccess();
			}
			@Override
			public void onFailure() { }
		});
		 // 能夠解決內存泄漏的回調
		// HttpNetUtils.get("http://www.baidu.com", new MyCallback(this)); 
	}
}

代碼邏輯非常簡單現在直接進入NetworkActivity界面之後立即退出並返回MainActivity界面,在Android Studio的Android Profiler工具窗口的Memory內存界面,不斷點擊最前面垃圾箱圖標的GC按鈕,點擊後很有可能會出發JVM的垃圾回收操作,爲了確保一定執行GC操作可以多點幾次,然後點擊帶向下箭頭圖標的Dump Heap按鈕導出當前JVM的堆信息。
在這裏插入圖片描述
Android Profiler Memory內存分析
在老版本的Android Studio上可能還無法查看詳細的對象引用信息,還需要把前面導出來的Heap文件轉換格式放到MAT(Memory Analysis Tools)工具中查看,使用的3.1.2版本集成的Heap分析工具已經很強大,直接查看分析出來的GCRoot引用。
在這裏插入圖片描述
從上圖可以看到啓動網絡請求的線程本身處於啓動之後結束之前是個活動線程,雖然Java方法棧並沒有引用它,JVM內部會有ThreadGroup專門管理它的引用,活動線程由於引用了Callback回調匿名對象,而Callback匿名對象又引用了NetworkActivity最終導致NetworkActivity雖然已經退出但依然保持在內存中,在Thread線程沒有結束之前始終不會被垃圾回收。在Android開發中通常都會使用線程池對象來執行網絡請求,線程池很大可能會使用核心工作線程執行用戶的請求,而核心線程一直存在也就會導致Activity一直不會被回收。

對這種匿名內部類回調對象導致的內存泄漏應該如何修改呢,Java中的對象引用分成了四種:強引用、軟引用(SoftReference)、弱引用(WeakReference)和幻象引用(PhatomReference),普通的對象賦值操作就是強引用,對象外面包裹在SoftReference和WeakReference中就屬於軟引用、弱引用,幻象引用比較少用;強引用通常不會被GC回收,在對象沒有強引用的情況下軟引用會在內存不足時會被回收,弱引用在下一次GC時就會被回收。現在只要用靜態內部類實現定義回調對象類,在靜態回調類中包含外部對象的WeakReference弱引用,回調判斷弱引用內部如果沒有對象表明外部對象已經被垃圾回收就不必在做回調操作。

private static class MyCallback implements HttpNetUtils.Callback {
	private WeakReference<Activity> mActivity;
	public MyCallback(Activity activity) {
		this.mActivity = new WeakReference<>(activity);
	}
	@Override
	public void onSuccess(final String text) {
		final Activity activity = mActivity.get();
		if (activity != null && !activity.isFinishing()) {
			showSuccess() ;
		}
	}
	@Override
	public void onFailure() {
	}
}

代碼1-41中實現Callback接口創建了靜態內部類,由於靜態內部類不保存外部對象的this引用,需要將外部對象從構造函數傳遞進來,不過靜態內部類使用了WeakReference引用,只要Activity被銷燬外部就不會在存在Activity的強引用,此時JVM內部執行GC操作就可以回收Activity對象,當緩慢的網絡請求返回時檢查外部引用已經爲空就不再執行回調。

Java方法棧

Java在調用方法時會爲每個方法創建棧幀並將它們加入到方法棧中,很顯然當前方法棧中引用的對象都是程序運行過程中必定會引用到的對象。不過有些在方法棧中對象的生命週期比較長,比如Android應用的消息隊列,它在Java 的main()方法的棧幀中,只要主線程消息循環不退出,它就始終存在,開發中經常會需要將某些更新UI的任務投遞主線程隊列中執行,如果在Activity退出那些任務還沒有完成就會依然保持在消息隊列中,而UI更新任務通常都會創建匿名的Runnable普通內部對象,普通內部對象都會包含外部對象的this引用。

在下面的HandlerActivity.onCreate()裏用mHandler投遞mRunnable對象到主線程的消息隊列中,5秒鐘之後纔會執行,但在回調之前用戶退出了HandlerActivity。
Handler導致內存泄漏示例

public class HandlerActivity extends AppCompatActivity {
	private Handler mHandler;
	private Runnable mRunnable;
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_handler);
		mHandler = new Handler(Looper.getMainLooper());
		mRunnable = new Runnable() { // 普通匿名內部類引用Activity
				@Override
				public void run() {
					Toast.makeText(getApplicationContext(), "Hello world!",
            					Toast.LENGTH_SHORT).show();
				}
		};
		mHandler.postDelayed(mRunnable, 5000);
	}

	@Override
	protected void onDestroy() {
		super.onDestroy();
		// mHandler.removeCallbacks(mRunnable); 需要在onDestroy的時候移除主線程回調
	}
}

下面的圖展示了ActivityThread $H類內部引用了MessageQueue消息隊列,消息隊列又引用了Message對象,Message.mCallback正是mRunnable對象,mRunnable是匿名內部類對象會包含HandlerActivity引用導致該Activity的內存泄漏。對這種情況可以用前面的靜態內部類加WeakReference來處理內存泄漏,也可以在onDestroy()週期回調中及時移除主線程回調對象。
在這裏插入圖片描述
Java方法棧上MessageQueue導致的內存泄漏

靜態變量

靜態變量和Class對象的生命週期一直,也是一種長生命週期的對象,在GC操作可達性分析中靜態變量引用的對象也被認作活動對象,不會被GC回收。單例對象通常被保存在靜態成員變量裏,它引用的對象也可能會出現內存泄漏,特別是有些單例對象會包含回調接口,註冊之後的對象就會被單例對象引用。下面代碼中用戶賬號管理類能夠實現登錄接口,有些界面在用戶登錄的情況下需要作出改變,可以使用觀察者模式實現。

public class UserManager {
	public interface UserLoginListener {
		void onLogin();
	}
	private List<UserLoginListener> mUserListeners = new ArrayList<>();
	private static class UserManagerHolder {
		private static final UserManager INSTANCE = new UserManager();
	}

	public static UserManager getInstance() {
		return UserManagerHolder.INSTANCE;
	}

	public void registerListener(UserLoginListener listener) {
		mUserListeners.add(listener);
	}

	public void unregisterListener(UserLoginListener listener) {
		mUserListeners.remove(listener);
	}
}

public class SingletonActivity extends AppCompatActivity 
	implements UserManager.UserLoginListener {

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_singleton);
		// Activity作爲觀察者註冊到單例對象中
		UserManager.getInstance().registerListener(this);
	}

	@Override
	public void onLogin() {
		// do something
	}

	@Override
	protected void onDestroy() {
		super.onDestroy();
       // 需要註銷監聽,否則會導致內存泄漏
		// UserManager.getInstance().unregisterListener(this);
	}
}

SingletonActivity就需要觀察用戶的登錄狀態,它在onCreate()方法中將自己註冊爲觀察者,當用戶登錄的時候就會通知到SingletonActivity。假如用戶直接退出SingletonActivity由於UserManager單例對象依然引用了它的實例就會導致內存泄漏,需要在onDestroy()時註銷監聽。下圖展示了直接退出沒有註銷用戶登錄觀察者導致的內存泄漏引用鏈。
在這裏插入圖片描述

集合內存泄漏

接着要討論本人在Effective Java上看到的一個自定義棧的示例 ,棧結構可以用鏈表也可以用數組來實現,通常還要支持push和pop操作,現在給出簡單的數組實現代碼。

public class MyStack {
	private int mTop;
	private Object[] mData;
	public void push(Object obj) {
         // 索引檢查
		mData[mTop++] = obj;
		mSize++;
	}
	public Object pop() { // 需要先執行top指針檢查
		Object tmp = mData[--mTop];
		// mData[mTop] = null; 需要注意置空否則會引起內存泄漏
		mSize--;
		return tmp;
	}
}

上面的代碼中沒有將退棧的對象置空,假如用戶有下面的調用序列,先push三個對象之後再pop兩個對象,在棧數組mData中引用了三個對象,而用戶真正需要的對象只有最底下的那個對象,上面的兩個對象在邏輯上已經是垃圾對象,由於棧數組依然保持着它的引用導致垃圾對象無法被回收。在退棧操作之後如果及時把棧頂元素的引用置空就不會出現內存泄漏問題了,這也就是爲什麼Java中對象確定不再使用後要把它置空,讓GC線程能及時將對象回收。

myStack.push(new Object()); // 放入對象
myStack.push(new Object());
myStack.push(new Object());

在這裏插入圖片描述

myStack.pop();// 彈出對象
myStack.pop();

在這裏插入圖片描述
自定義棧代碼展示了開發者手動維護對象內存出現的問題,Java中存在大量高效可靠的集合類型,它們極大地提高了程序員的開發效率,不過在使用集合類型時也要注意它們的特性,防止出現內存泄漏問題。下面的代碼展示了簡單的自定義對象Node,它在加入某個集合之後修改了內容,開發者再次使用它的引用將它從集合對象中刪除。

static class Node implements Comparable<Node>{
    private int data;
    @Override
    public boolean equals(Object o) {
       if (this == o) return true;
       if (o == null || getClass() != o.getClass()) return false;
        Node node = (Node) o;
        return data == node.data;
    }
    @Override
    public int hashCode() {
        return Objects.hash(data);
    }
    @Override
	public int compareTo(Node o) {
    	return data - o.data; // 不考慮o爲null的情況
    }
}

// 測試ArrayList
Node node1 = new Node(10);
List<Node> list = new ArrayList<>();
list.add(node1); node1.setData(15); list.remove(node1);
System.out.println("list = " + list.toString());
// 測試HashMap
Node node2 = new Node(20);
Map<Node, String> map = new HashMap<>();
map.put(node2, "Hello"); node2.setData(25); map.remove(node2);
System.out.println(map.toString());
// 測試TreeSet和HashSet代碼與上面類似

//~ 執行結果
list = []
map = {Node{data=25}=Hello} // 對象未刪除
treeSet = []
hashSet = [Node{data=45}] // 對象未刪除

上面代碼中定義的Node對象它的hashCode()和equals()方法都已經被重寫,確保了在equals()返回true時hashCode()的返回值一定時相同的,因爲要測試具有排序功能的TreeSet,Node類也實現了Comparable接口。從運行代碼的結果可以看出ArrayList和TreeSet內部的對象類型即使修改了數據值依然能夠成功移除,但HashMap一旦加入的鍵值對象內容一旦發生變化就無法通過鍵值對象引用來刪除,HashSet內部的對象內容修改後無法通過對象引用從集合中刪除。

在ArrayList內部判定對象是相同的使用的是equals()方法,雖然加入的對象內容發生了改變,ArrayList引用的是同一個對象,它們equals()時一定時相同的,再使用同一個對象的引用刪除時不會出現問題。TreeSet內部比較相同對象也使用的時equals()方法,它和ArrayList刪除是一樣的結果,不過TreeSet帶有排序的功能,修改了值的對象位置依然還在舊值所在的位置,並沒有發生任何位置調整,後續的操作會導致TreeSet的排序功能失效。

HashMap的工作需要依賴於hashCode(),在刪除對應的鍵值時首先根據hashCode()確定對象被映射到的數組槽索引位置,接着從數組槽指向的鏈表或紅黑樹中刪除對應的對象引用。修改鍵對象內容後的鍵值由於hashCode()改變,HashMap根本無法定位到它實際的位置,也就無法刪除該對象。HashSet同樣依賴於hashCode(),對象內容的修改導致hashCode()值改變自然也就無法刪除加入的對象。

由此可見在Java集合對象中調用remove(Object)並不是一定能夠將對象從集合中移除,有些對象內容發生了變化就無法被移除,在工作中如果發現有些集合內容不再需要需要即使調用clear()清空它內部的所有對象引用。

Native層資源

除了上面提到的活動線程、靜態成員變量和集合框架外,還有很多系統資源比如文件、Cursor遊標對象等用完之後要及時地調用close()關閉操作避免內存泄漏。文件其實時操作系統的資源,開發者在程序中打開文件,操作系統會在系統中記錄文件的打開次數,開闢內存緩存資源,沒有及時關閉文件就會導致系統資源的浪費。Cursor遊標通常在數據庫請求時會用到,Android系統中爲了能夠方便跨進程訪問會在底層開闢一塊被CursorWindow對象管理的共享內存塊,所有數據查詢都會通過fillWindow()方法填充到Cursor Window管理的內存塊中。

// SQLiteCursor.java 
public class SQLiteCursor extends AbstractWindowedCursor { 
// SQLiteCursor就是Cursor接口的實現
    // 省略不重要代碼
    private void fillWindow(int requiredPos) {
        clearOrCreateWindow(getDatabase().getPath()); // 創建新的CursorWindow對象
		// 省略填充代碼
    }

    @Override
    public void close() {
        super.close();
        synchronized (this) {
            mQuery.close();
            mDriver.cursorClosed(); // 關閉CursorWindow對象
        }
    }
}
// Native層CursorWindow實現android_database_CursorWindow.cpp
static jlong nativeCreate(JNIEnv* env, jclass clazz, jstring nameObj, jint cursorWindowSize) {
    CursorWindow* window; // C++中使用new創建了CursowWindow
    status_t status = CursorWindow::create(name, cursorWindowSize, &window);
    return reinterpret_cast<jlong>(window);
}
tatus_t CursorWindow::create(const String8& name, size_t size, 
CursorWindow** outCursorWindow) {
	// 省略其他代碼,使用new創建了CursorWindow對象
    CursorWindow* window = new CursorWindow(name, ashmemFd,
                            data, size, false /*readOnly*/);
    result = window->clear();       
    *outCursorWindow = window;
    return OK;              
}
// close()後纔會刪除Window對象調用析構函數
static void nativeDispose(JNIEnv* env, jclass clazz, jlong windowPtr) {
    CursorWindow* window = reinterpret_cast<CursorWindow*>(windowPtr);
    if (window) {
        delete window;
    }
}

上述代碼中展示了Native層通過new操作符創建了CursorWindow對象,C++語言並不具備自動GC操作,需要程序員手動釋放請求的內存,如果Java層的開發者沒有調用Cursor.close()方法就不會刪除CursorWindow對象在Native層分配的資源,導致整個系統中出現內存泄漏。

內存泄漏測試Demo

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章