Android 內存分析指北
android 內存泄漏分析指北
簡單來說內存泄漏就是當對象不再被應用程序使用,但是垃圾回收器卻不能移除它們,因為它們正在被引用
java 垃圾回收介紹:
Java 虛擬機運行所管理的內存包括以下幾個運行時的數據區域
如下圖:
程序計數器: 一塊比較小的內存區域,可以看作是當前線程所執行的字節碼的行號指示器。且每個線程都有一個獨立的程序計數器。
java 虛擬機棧: 線程私有的,描述的是java 方法執行的內存模型,每個線程執行的時候都會創建一個棧幀用於儲存 局部變量、操作數棧、動態鏈接、方法出口、等信息。一個方法的調用到執行結束的過程就對應一個棧幀在虛擬機棧中的入棧到處棧的過程。 虛擬機局部變量表中存放了編譯可知的各種局部數據類型(boolean、byte、char、short、int 、float 、long、double)
本地方法棧:和虛擬機棧類似,其中虛擬機棧為執行java 方法服務,而本地方法棧為虛擬機使用的native 的方法服務java 堆:java 虛擬機所管理的內存中最大的一塊,且其是被所有的線程共享的一塊內存區域,在虛擬機啟動的時候創建。該區域的唯一目的就是來存放對象實例的。 java 堆是垃圾啊回收管理的主要區域,因此在很多的時候被叫做"GC堆"
方法區:和java 堆一樣是各個線程共享的內存區域,用來存儲已經被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼和數據。
運行中常量池:為方法區的一部分。class 文件中除了有類的版本、字段、方法、接口等描述信息外、還有一項信息是常量池、用來存放編譯期生產的各種字面量和符號引用。
GC 時那些內存需要釋放:
首先對於程序計數器、虛擬機棧、本地方法棧 這3個區域都是隨線程而生、所線程而亡。 棧中的棧幀隨著方法的進入和退出執行著如棧和出棧的操作。每一個棧幀中分配的內存基本上在類結構確定下來的時候已經是可知的了。所以在這幾個區域就不需要考慮內存回收的問題,因為在方法結束,或者線程結束的時候內存自然就會回收了。
但是java 堆和方法區確不一樣,如一個接口中的多個實現類需要的內存可能不一樣,一個方法中的的多個分支需要的內存也可能不一樣,我們只有在程序處於運行的情況下才可以知道會創建那些對象,這部分的內存的分配和回收也是動態的,垃圾回收所關註的也這一部分的內容。
確定哪些對象是存活的,那些已經“死去”(即不可能被任何途徑使用的對象)
方式:
A、引用計數法:
JAVA
虛擬器並沒有使用引用計數法來管理內存,其中主要的原因是它很難解決對象的相互循環引用的問題) 。
B、可達性分析法: 通過一系列的稱為GC Roots
的對象作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈,當一個對象到GC Rotts
沒有任何引用鏈 相連時(即GC Roots
到這個對象不可達),則證明這個對象是不可用的。
如下圖論:
如上圖所示,object1~object4對GC Root都是可達的,說明不可被回收,object5和object6對GC Root節點不可達,說明其可以被回收
可作為GC roots 的對象包括:虛擬機棧(棧幀中的本地變量表)中引用的對象(就是指正執行的方法中的局部變量,參數,臨時值所引用的對象)、方法區中的靜態屬性引用的對象、方法區中常量引用的對象、本地方法中JNI
引用的對象。
各中引用:
強引用:只要強引用還存在,垃圾收集器將永遠不會回收掉被引用的對象。
軟引用:用來描述一些還有用但並非必須的對象、在系統將要發生內存溢出異常之前,將會把這些對象列進對象回收範圍中進行第二次回收。如果這次回收還沒有足夠的內存,才會拋出內存溢出的異常。
弱引用:也是用來描述非必須對象,其強度比軟引用更弱些,被弱引用關聯的對象只能生存到下一次垃圾收集發生之前,當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。
虛引用:一種最弱的引用關系、一個對象是否有引用存在完全不會影響其生存時間、且無法通過虛引用活得一個對象的實例。一個虛引用唯一的目的就是在這個對象在被收集器回收的時候能夠收到一個系統通知。
參考:深入理解java 虛擬機第二版
內存泄漏檢測:
1、Dump App
的meminfo
:在一個App 中存在著很多個Activity ,其實我們只需要一個一個界面 去檢查其是否存在Activity泄漏的情況 ,
利用adb shell dumpsys meminfo <package name>
來dump
該app
的內存信息,該信息中有包含當前 app
中所未釋放的 activity
的數量,以及view
的個數,如下圖所示, 在進入一個界面之前檢查下activity
數量和view
的數量 ,在退出該界面後檢查下在查看一邊activity
和view
的數量,對比進入和退出後activity 和view 的數量是否有差異。
如下圖:目前存在著1 個activity 184 個 view 。 因此我們如果要檢查某個activity 是否存在泄漏,我們只需要在進入該activity 之前dump 下該信息, 然後在進入該activity ,進行一系列操作,然後點擊back鍵,退出 。
ps:
在dump meminfo
信息的時候,需要等內存穩定下然後進行dump
, 或者通過手動gc
(利用 android
的monitor
工具手動gc
按鈕)後進行dump
操作。
2、跑自動化測試腳本,或者跑Monkey 查看內存的增長曲線(測試檢測的方式)。
3、LeakCanary Square 公司開源作品,使用方便,可以直接定位到泄漏的對象,並且給出調用鏈。
內存泄漏分析,相關工具使用:
分析內存泄漏,第一步得復現問題,然後抓取hprof
文件。
1、androidStudio Hprof
分析Activity
泄漏:利用Android studio
的Monitors
的工具抓取 點擊Start Allocation Tracking
抓取hprof
文件。 具體怎樣使用該工具 可參考如下鏈接。
hporf 分析
例如如下分析結果,存在 HandleActivity
存在泄漏。 然後直接參考上面的鏈接,找出對應引起泄漏的點。 下圖是HandlerActivity
的內部類釋放不了造成Activity
泄漏的。
2、 MAT
使用:MAT
工具打開hprof
文件時,需要先利用hprof-conv
工具將hprof
文件轉換下, 利用如下命令。
hprof-conv demo.hprof demo_conv.hprof
打開hprof
文件如下:搜索關鍵字可以搜索出相關對象對象的個數,所占的內存(如下圖)。
右鍵Merge shortest paths to gc roots
選擇 exclude all phantom/weak/soft etc references
可以定位到gc root
這樣就可以確定時哪個對象沒有釋放導致HandlerActiivty
不能釋放 。
對比兩個hprof
文件,查看某個對象的個數對比如下圖:
通過上圖就可以看出HandlerActivity
沒有被釋放。
常見內存泄漏類型以及解決方案:
靜態引用造成的內存泄漏
private static DeviceUtil stance;![Alt text](./mat_2.png)
private Context context;
....
public static DeviceUtil getStance(Context context) {
if(stance == null){
synchronized (DeviceUtil.class){
stance = new DeviceUtil(context);
}
}
return stance;
}
private DeviceUtil(Context context){
this.context = context;
}
調用的地方:
private void showVersionCode(){
final int versionCode = DeviceUtil.getStance(HandlerActivity.this)
.getVersionCode(getPackageName());
Toast.makeText(HandlerActivity.this,
"versionCode="+versionCode,Toast.LENGTH_LONG).show();
}
在上面的例子中DeviceUtil
持有了Activity
從而導致其釋放不了。
解決方案: 將Activity context
修改成Application context
. 因為Applicaition
時全局的,生命周期時和app 生命周期一樣的。
非靜態內部類造成內存泄漏:
public class HandlerActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_handler);
startThread();
}
class InnerThread extends Thread{
@Override
public void run() {
super.run();
int index = 0;
while (index < Integer.MAX_VALUE){
index ++;
try {
sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
private void startThread(){
InnerThread thread = new InnerThread();
thread.start();
}
如上面代碼中InnerThread
會隱式 的持有外部類的引用,因為在這裏InnerThread
線程的生命周期超過了Activity
的生命周期,當finish
當前Activity
時InnerThread
並不會停止且持有了當前Activity
從而導致HandleActivity
泄漏。
解決方案: 將內部類修改為靜態內部類的方式 。 如下:
static class InnerThread extends Thread{
@Override
public void run() {
super.run();
int index = 0;
while (index < Integer.MAX_VALUE){
index ++;
try {
sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
匿名內部類造成的內存泄漏
public class HandlerActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_handler);
//startThread();
Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
final int what = msg.what;
if(what == 0){
Toast.makeText(HandlerActivity.this,"receive message",Toast.LENGTH_LONG).show();
}
super.handleMessage(msg);
}
};
Message message = handler.obtainMessage(0);
handler.sendMessageDelayed(message,1000*10);
}
在這裏Message 會在主線程中存在10s , 且Message 會持有handler 對象,而handler 會隱式持有Activity ,從而導致Activity 泄漏。
修改方案: 將匿名內部類修改成靜態內部類即可,參考上面的例子。
不需要的監聽未移除導致的內存溢出:
常見的如:廣播監聽沒有被移除,所以需要註冊和解除註冊成對出現 。
Native
泄漏:
在android 中Native
泄漏都大部分是通過jni
調用Native
方法,所以只需要檢查Java
端調用JNI
的地方即可。
binder
泄漏:
資源對象未關閉 :
資源性對象如Cursor、File、Socket,應該在使用後及時關閉。未在finally中關閉,會導致異常情況下資源對象未被釋放的隱患。
總結:一般來說,內存泄漏都是因為泄漏對象的引用被傳遞到該對象的範圍之外,或者說內存泄漏是因為持有對象的長期引用,導致對象無法被 GC 回收。為了避免這種情況,我們可以選擇在對象生命周期結束的時候,解除綁定,將引用置為空,或者使用弱引用。
Android 內存分析指北