通常情況下我們說的內存是指手機的RAM,它主要包括一下幾個部分
1.寄存器 :速度最快的存儲場所,因為寄存器位于處理器內部,所以在程序中我們無法控制
- 棧(Stack) :存放基本類型的對象和引用,但是對象本身不存放在棧中,而是存放在堆中
變量其實是分為兩部分的:一部分叫變量名,另外一部分叫變量值,對于局部變量(基本類型的變量和對象的引用變量)而言,統一都存放在棧中,但是變量值中存儲的內容就有在一定差異了:Java中存在8大基本類型,他們的變量值中存放的就是具體的數值,而其他的類型都叫做引用類型(對象也是引用類型,你只要記住除了基本類型,都是引用類型)他們的變量值中存放的是他們在堆中的引用(內存地址)。
在函數執行的時候,函數內部的局部變量就會在棧上創建,函數執行結束的時候這些存儲單元會被自動釋放。棧內存分配運算內置于處理器的指令集中是一塊連續的內存區域,效率很高,速度快,但是大小是操作系統預定好的所以分配的內存容量有限。
3.堆(Heap)
在堆上分配內存的過程稱作 內存動態分配過程。在java中堆用于存放由new創建的對象和數組。堆中分配的內存,由java虛擬機自動垃圾回收器(GC)來管理(可見我們要進行的內存優化主要就是對堆內存進行優化)。堆是不連續的內存區域(因為系統是用鏈表來存儲空閑內存地址,自然不是連續的),堆大小受限于計算機系統中有效的虛擬內存(32bit系統理論上是4G)
4.靜態存儲區/方法區(Static Field)
是指在固定的位置上存放應用程序運行時一直存在的數據,java在內存中專門劃分了一個靜態存儲區域來管理一些特殊的數據變量如靜態的數據變量。
5.常量池(Constant Pool)
顧名思義專門存放常量的。注意 String s = "java"中的“java”也是常量。JVM虛擬機為每個已經被轉載的類型維護一個常量池。常量池就是該類型所有用到地常量的一個有序集合包括直接常量(基本類型,String)和對其他類型、字段和方法的符號引用。
定義一個局部變量的時候,java虛擬機就會在棧中為其分配內存空間,局部變量的基本數據類型和引用存儲于棧中,引用的對象實體存儲于堆中。因為它們屬于方法中的變量,生命周期隨方法而結束。成員變量全部存儲與堆中(包括基本數據類型,引用和引用的對象實體),因為它們屬于類,類對象終究是要被new出來使用的。當堆中對象的作用域結束的時候,這部分內存也不會立刻被回收,而是等待系統GC進行回收。所謂的內存分析,就是分析Heap中的內存狀態
簡單例子
比如說這個類
public class People{
int a = 1;
Student s1 = new Student();
public void XXX(){
int b = 1;
Student s2 = new Student();
}
}
請問a的內存在哪里,b的內存在哪里,s1,s2的內存在哪里?記住下面兩句話。
成員變量全部存儲在堆中(包括基本數據類型,引用及引用的對象實體),因為他們屬于類,類對象最終還是要被new出來的。
局部變量的基本數據類型和引用存儲于棧當中,引用的對象實體存儲在堆中。因為他們屬于方法當中的變量,生命周期會隨著方法一起結束。
所以答案就是a,s1,s2對象都堆中,b和s2對象引用在棧中
內存模型
普通的Linux中啟動的應用通常和登陸用戶相關聯,同一用戶的UID相同。但是Android中給不同的應用都賦予了不同的UID,這樣不同的應用將不能相互訪問資源。對應用而言,這樣會更加封閉,安全。
在Android和Java中都存在著一個Generational系統會根據內存中不同的內存數據類型分別執行不同的GC操作
Generational Heap Memory模型主要由:Young Generation(新生代)、Old Generation(舊生代)、Permanent
其中Young Generation區域存放的是最近被創建對象,此區域最大的特點就是創建的快,被銷毀的也很快。當對象在Young Generation區域停留的時間到達一定程度的時候,它就會被移動到Old Generation區域中,同理,最后他將會被移動到Permanent Generation區域中
每一個區域的大小都是有固定值的,當進入的對象總大小到達某一級內存區域閥值的時候就會觸發GC機制,進行垃圾回收,騰出空間以便其他對象進入
不僅如此,不同級別的Generation區域GC是需要的時間也是不同的。同等對象數目下,Young Generation GC所需時間最短,Old Generation次之,Permanent Generation 需要的時間最長。當然GC執行的長短也和當前Generation區域中的對象數目有關。遍歷查找20000個對象比起遍歷50個對象自然是要慢很多的。
GC機制概述
與C++不用,在Java中,內存的分配是由程序完成的,而內存的釋放是由垃圾收集器(Garbage Collection,GC)完成的,程序員不需要通過調用函數來釋放內存,但也隨之帶來了內存泄漏的可能。簡單點說:對于 C++ 來說,內存泄漏就是new出來的對象沒有 delete,俗稱野指針;而對于 java 來說,就是 new 出來的 Object 放在 Heap 上無法被GC回收
Android使用的主要開發語言是Java所以二者的GC機制原理也大同小異,所以我們只對于常見的JVM GC機制的分析,就能達到我們的目的
新生代GC算法
由于Young Generation通常存活的時間比較短,所以Young Generation采用了Copying算法進行回收,Copying算法就是掃描出存活的對象,并復制到一塊新的空間中,Young Generation采用空閑指針的方式來控制GC觸發,指針保存最后一個分配在Young Generation中分配空間地對象的位置。當有新的對象要分配內存空間的時候,就會主動檢測空間是否足夠,不夠的情況下就出觸發GC,當連續分配對象時,對象會逐漸從Eden移動到Survivor,最后移動到Old Generation。
舊生代GC算法
Old Generation與Young Generation不同,對象存活的時間比較長,比較穩固,因此采用標記(Mark)算法來進行回收。所謂標記就是掃描出存活的對象,然后在回收未必標記的對象。回收后的剩余空間要么進行合并,要么標記出來便于下次進行分配,總之就是要減少內存碎片帶來的效率損耗
如何判斷對象是否可以被回收
引用計數器
引用計數器是垃圾收集器中的早起策略。這種方法中,每個對象實體(不是它的引用)都有一個引用計數器。當一個對象創建的時候,且將該對象分配給一個每分配給一個變量,計數器就+1,當一個對象的某個引用超過了生命周期或者被設置一個新值時,對象計數器就-1,任何引用計數器為 0 的對象可以被當作垃圾收集。當一個對象被垃圾收集時,引用的任何對象技術 - 1。
優點:執行快,交織在程序運行中,對程序不被長時間打斷的實時環境比較有利。
缺點:無法檢測出循環引用。比如:對象A中有對象B的引用,而B中同時也有A的引用
跟蹤收集器
現在的垃圾回收機制已經不太使用引用計數器的方法判斷是否可回收,而是使用跟蹤收集器方法。
現在大多數JVM采用對象引用遍歷機制從程序的主要運行對象(如靜態對象/寄存器/棧上指向的堆內存對象等)開始檢查引用鏈,去遞歸判斷對象收否可達,如果不可達,則作為垃圾回收,當然在便利階段,GC必須記住那些對象是可達的,以便刪除不可到達的對象,這稱為標記(marking)對象。
下一步,GC就要刪除這些不可達的對象,在刪除時未必標記的對象,釋放它們的內存的過程叫做清除(sweeping),而這樣會造成內存碎片化,布局已分配給新的對象,但是他們集合起來還很大。所以很多GC機制還要重新組織內存中的對象,并進行壓縮,形成大塊、可利用的空間
為了達到這個目的,GC需要停止程序的其他活動,阻塞進程。這里我們要注意的是:不要頻繁的引發GC,執行GC操作的時候,任何線程的任何操作都會需要暫停,等待GC操作完成之后,其他操作才能夠繼續運行, 故而如果程序頻繁GC, 自然會導致界面卡頓. 通常來說,單個的GC并不會占用太多時間,但是大量不停的GC操作則會顯著占用幀間隔時間(16ms)如果在幀間隔時間里面做了過多的GC操作,那么自然其他類似計算,渲染等操作的可用時間就變得少了。
Android內存泄漏分析
對于 C++ 來說,內存泄漏就是new出來的對象沒有 delete,俗稱野指針;而對于 java 來說,就是 new 出來的 Object 放在 Heap 上無法被GC回收
為什么不能被回收
GC過程與對象的引用類型是嚴重相關的,下面我們就看看Java中(Android中存在差異)對于引用的四種分類:
強引用(Strong Reference):JVM寧愿拋出OOM,也不會讓GC回收的對象,Jvm停止運行才死亡
軟引用(Soft Reference) :只有內存不足時,才會被GC回收。
弱引用(weak Reference):在GC時,一旦發現弱引用,立即回收
虛引用(Phantom Reference):任何時候都可以被GC回收,當垃圾回收器準備回收一個對象時,如果發現它還有虛引用,就會在回收對象的內存之前,把這個虛引用加入到與之關聯的引用隊列中。程序可以通過判斷引用隊列中是否存在該對象的虛引用,來了解這個對象是否將要被回收。可以用來作為GC回收Object的標志。
在Android開發過程中,我們常常使用HasMap保存對象,但是為了防止內存泄漏,在保存內存占用較大、生命周期較長的對象的時候,盡量使用LruCache代替HasMap用于保存對象
//指定最大緩存空間
private static final int MAX_SIZE = (int) (Runtime.getRuntime().maxMemory() / 8);
LruCache mBitmapLruCache = new LruCache<>(MAX_SIZE);
而造成不能回收的根本原因就是:堆內存中長生命周期的對象持有短生命周期對象的強/軟引用,盡管短生命周期對象已經不再需要,但是因為長生命周期對象持有它的引用而導致不能被回收
如何的監聽系統發生GC
系統每進行一次GC操作時,都會在LogCat中打印一條日志,我們只要去分析這條日志就可以了,日志的基本格式如下所示:
DVM中
D/dalvikvm(30615): GC FOR ALLOC freed 4442K, 25% free 20183K/26856K, paused 24ms , total 24ms
ART中
I/art(198): Explicit concurrent mark sweep GC freed 700(30KB) AllocSpace objects, 0(0B) LOS objects, 792% free, 18MB/21MB, paused 186us total 12.763ms
觸發GC操作的原因
GC_CONCURRENT: 當我們應用程序的堆內存快要滿的時候,系統會自動觸發GC操作來釋放內存(同時發生)
GC_FOR_MALLOC: 當我們的應用程序需要分配更多內存,可是現有內存已經不足的時候,系統會進行GC
GC_HPROF_DUMP_HEAP: 當生成HPROF文件的時候,系統會進行GC操作,關于HPROF文件我們下面會講到
GC_EXPLICIT: 這種情況就是我們剛才提到過的,主動通知系統去進行GC操作,比如調用System.gc()方法來通知系統。或者在DDMS中,通過工具按鈕也是可以顯式地告訴系統進行GC操作的
GC操作釋放了多少內存
Heap_stats中會顯示當前內存的空閑比例以及使用情況(活動對象所占內存 / 當前程序總內存)
Pause_time表示這次GC操作導致應用程序暫停的時間。**關于這個暫停的時間,Android在2.3的版本當中進行過一次優化,在2.3之前GC操作是不能并發進行的,也就是系統正在進行GC,那么應用程序就只能阻塞住等待GC結束。雖說這個阻塞的過程并不會很長,也就是幾百毫秒,但是用戶在使用我們的程序時還是有可能會感覺到略微的卡頓
2.3之后,GC操作改成了并發的方式進行,就是說GC的過程中不會影響到應用程序的正常運行,但是在GC操作的開始和結束的時候會短暫阻塞一段時間,不過優化到這種程度,用戶已經是完全無法察覺到了
導致GC頻繁執行有兩個原因
- Memory Churn(內存抖動),內存抖動是因為大量的對象被創建又在短時間內馬上被釋放
盡量避免在循環體內創建對象,應該把對象創建移到循環體外。
注意自定義View的onDraw()方法會被頻繁調用,所以在這里面不應該頻繁的創建對象。
當需要大量使用Bitmap的時候,試著把它們緩存在數組中實現復用。
對于能夠復用的對象,同理可以使用對象池將它們緩存起來
- 瞬間產生大量的對象會嚴重占用Young Generation的內存區域,當達到閥值,剩余空間不夠的時候,也會觸發GC。即使每次分配的對象占用了很少的內存,但是他們疊加在一起會增加 Heap的壓力,從而觸發更多其他類型的GC。這個操作有可能會影響到幀率,并使得用戶感知到性能問題
內存泄漏的檢測與處理
Android Studio界面
一般分析內存泄露, 首先運行程序,打開日志控制臺,有一個標簽Memory ,我們可以在這個界面分析當前程序使用的內存情況, 一目了然, 我們再也不需要苦苦的在logcat中尋找內存的日志了
圖中藍色區域,就是程序使用的內存, 灰色區域就是空閑內存, 當然,Android內存分配機制是對每個應用程序逐步增加, 比如你程序當前使用30M內存, 系統可能會給你分配40M, 當前就有10M空閑, 如果程序使用了50M了,系統會緊接著給當前程序增加一部分,比如達到了80M, 當前你的空閑內存就是30M了。 當然,系統如果不能再給你分配額外的內存,程序自然就會OOM(內存溢出)了。 每個應用程序最高可以申請的內存和手機密切相關,比如我當前使用的華為Mate7,極限大概是200M,算比較高的了, 一般128M 就是極限了, 甚至有的手機只有可憐的16M或者32M,這樣的手機相對于內存溢出的概率非常大了
檢測內存泄露
首先需要明白一個概念, 內存泄露就是指,本應該回收的內存,還駐留在內存中。 一般情況下,高密度的手機,一個頁面大概就會消耗20M內存,如果發現退出界面,程序內存遲遲不降低的話,可能就發生了嚴重的內存泄露。 我們可以反復進入該界面,然后點擊dump Java heap 這個按鈕,然后Android Studio就開始干活了,下面的圖就是正在dump
dump成功后會自動打開 hprof文件,文件以Snapshot+時間來命名
MAT
通過Android Studio自帶的界面,查看內存泄露還不是很智能,我們可以借助第三方工具,常見的工具就是MAT了,下載地址 https://eclipse.org/mat/downloads.php ,這里我們需要下載獨立版的MAT. 下圖是MAT一開始打開的界面, 這里需要提醒大家的是,MAT并不會準確地告訴我們哪里發生了內存泄漏,而是會提供一大堆的數據和線索,我們需要自己去分析這些數據來去判斷到底是不是真的發生了內存泄漏。
接下來我們需要用MAT打開內存分析的文件, 上文給大家介紹了使用Android Studio生成了 hprof文件, 這個文件在呢, 在Android Studio中的Captrues這個目錄中,可以找到
注意,這個文件不能直接交給MAT, MAT是不識別的, 我們需要右鍵點擊這個文件,轉換成MAT識別的。
LeakCanary
LeakCanary會檢測應用的內存回收情況,如果發現有垃圾對象沒有被回收,就會去分析當前的內存快照,也就是上邊MAT用到的.hprof文件,找到對象的引用鏈,并顯示在頁面上。這款插件的好處就是,可以在手機端直接查看內存泄露的地方,可以輔助我們檢測內存泄露
在build.gradle文件中添加,不同的編譯使用不同的引用:
dependencies {
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3'
}
在應用的Application onCreate方法中添加LeakCanary.install(this),如下
public class ExampleApplication extends Application
@Override
public void onCreate() {
super.onCreate();
LeakCanary.install(this);
}
}
在應用的Application onCreate方法中添加LeakCanary.install(this),如下
dependencies {
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3'
}
public class ExampleApplication extends Application
@Override
public void onCreate() {
super.onCreate();
LeakCanary.install(this);
}
}
追蹤內存分配
如果我們想了解內存分配更詳細的情況,可以使用Allocation Traker來查看內存到底被什么占用了。 用法很簡單:
如果我們要觀測方法執行的時間,就需要來到CPU界面
點擊Start Method Tracking, 一段時間后再點擊一次, trace文件被自動打開,
非獨占時間: 某函數占用的CPU時間,包含內部調用其它函數的CPU時間。 獨占時間: 某函數占用CPU時間,但不含內部調用其它函數所占用的CPU時間。
我們如何判斷可能有問題的方法?
通過方法的調用次數和獨占時間來查看,通常判斷方法是:
如果方法調用次數不多,但每次調用卻需要花費很長的時間的函數,可能會有問題。
如果自身占用時間不長,但調用卻非常頻繁的函數也可能會有問題。
常見內存泄漏
單例(Singleton)
為了完美解決我們在程序中反復創建同一對象的問題,我們選用了單例模式,單例在我們的程序中隨處可見,但是由于單例模式的靜態特性,使得它的生命周期和我們的應用一樣長,一不小心讓單例無限制的持有Activity的強引用就會導致內存泄漏。例如:
public class SingleTon{
private Context context;
private static SingleTon singleTon;
public static final SingleTon getInstance(Context context){
this.context = context;
return SingleHolder.INSTANCE;
}
private static class SingleHolder{
private static final SingleTon INSTANCE = new SingleTon();
}
}
運行到手機:
轉屏后多出來一些實際占用內存,5.48 -4.66 = 0.82M內存,如下:
尋找問題
解決辦法:
這個錯誤很普遍,這個是一個很正常的單利模式,但是由于傳入了一個Context,而這個Context的生命周期就的長短就尤為重要了。如果我們傳入的是某個Activity的Context,而當這個Activity推出的時候,由于該Context的強引用被單例持有,那么這個Activity就等同于擁有了整個程序的生命周期。這種情況下,當Activity退出的時候內存并沒有被回收,這就造成了內存泄漏。
正確的做法就是應該把傳入的Context改為同應用生命周期一樣長的Application中的Context。
public class BaseApplication extends Application{
private static BaseApplication baseApplication;
@Override
public void onCreate(){
super.onCreate();
baseApplication = this;
}
public static Context getContext{
baseApplication.getApplicationContext();
}
}
當然我們可以直接重寫Application,提供getContext方法,不必在依靠傳入的參數:
public static final SingleTon getInstance(Context context) {
this.context = context.getApplicationContext;
return SingleHolder.INSTANCE;
}
Handler引起的內存泄漏
Handler引起的內存泄漏在我們開發中最為常見的。我們知道Handler、Message、MessageQueue都是相互關聯在一起的,萬一Handler發送的Message尚未被處理,那么該Message以及發送它的Handler對象都會被線程MessageQueue一直持有。
由于Handler屬于TLS(Thread Local Storage)變量,生命周期和Activity是不一致的,因此這種實現方式很難保證跟Activity的生命周期一直,所以很容易無法釋放內存。比如:
public class HandlerBadActivity extends AppCompatActivity {
private final Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_handler_bad);
// 延遲5min發送一個消息
handler.postDelayed(new Runnable() {
@Override
public void run() {
// write something
}
},1000*60*5);
this.finish();
}
}
我們在例子中生命了一個延時5分鐘執行的Message,當該Activity退出的時候,延時任務(Message)還在主線成的MessageQueue中等待,此時的Message持有Handler的強引用,并且由于Handler是HandlerBadActivity的非靜態內部類,所以Handler會持有HandlerBadActivity的強引用,此時HandlerBadActivity退出時無法進行內存回收,造成內存泄漏。
解決辦法:
將Handler生命為靜態內部類,這樣它就不會持有外部來的引用了。這樣以來Handler的的生命周期就與Activity無關了。不過倘若用到Context等外部類的非static對象,還是應該通過使用Application中與應用同生命周期的Context比較合適。比如:
public class HandlerGoodActivity extends AppCompatActivity {
private static final class MyHandler extends Handler {
private Context mActivity;
public MyHandler(HandlerGoodActivity activity) {
//使用生命周期與應用同長的getApplicationContext
this.mActivity = activity.getApplicationContext();
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (mActivity != null) {
// write something
}
}
}
private final MyHandler myHandler = new MyHandler(this);
// 匿名內部類在static的時候絕對不會持有外部類的引用
private static final Runnable RUNNABLE = new Runnable() {
@Override
public void run() {
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_handler_good);
myHandler.postDelayed(RUNNABLE, 1000 * 60 * 5);
}
雖然我們結局了Activity的內存泄漏問題,但是經過Handler發送的延時消息還在MessageQueue中,Looper也在等待處理消息,所以我們要在Activity銷毀的時候處理掉隊列中的消息。
@Override
protected void onDestroy() {
super.onDestroy();
//傳入null,就表示移除所有Message和Runnable
myHandler.removeCallbacksAndMessages(null);
}
匿名內部類在異步線程中的使用
它們方便卻暗藏殺機。Android開發經常會繼承實現 Activity 或者 Fragment 或者 View。如果你使用了匿名類,而又被異步線程所引用,那得小心,如果沒有任何措施同樣會導致內存泄漏的:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_inner_bad);
Runnable runnable1 = new MyRunnable();
Runnable runnable2 = new Runnable() {
@Override
public void run() {
}
};
}
private static class MyRunnable implements Runnable{
@Override
public void run() {
}
}
}
runnable1 和 runnable2的區別就是,runnable2使用了匿名內部類,我們看看引用時的引用內存
可以看到,runnable1是沒有什么特別的。但runnable2多出了一個MainActivity的引用,若是這個引用再傳入到一個異步線程,此線程在和Activity生命周期不一致的時候,也就造成了Activity的泄露。
善用static成員變量
從前面的介紹我們知道,static修飾的變量位于內存的靜態存儲區,此變量與App的生命周期一致
這必然會導致一系列問題,如果你的app進程設計上是長駐內存的,那即使app切到后臺,這部分內存也不會被釋放。按照現在手機app內存管理機制,占內存較大的后臺進程將優先回收,因為如果此app做過進程互保保活,那會造成app在后臺頻繁重啟。當手機安裝了你參與開發的app以后一夜時間手機被消耗空了電量、流量,你的app不得不被用戶卸載或者靜默。
這里修復的方法是:
不要在類初始時初始化靜態成員。可以考慮lazy初始化(延遲加載)。架構設計上要思考是否真的有必要這樣做,盡量避免。如果架構需要這么設計,那么此對象的生命周期你有責任管理起來。
避免使用
在我們的日常代碼中,這樣的情況似乎很常見,及直接寫一個class就這么光禿禿的情況
這樣就在Activity內部創建了一個非靜態內部類的單例,每次啟動Activity時都會使用該單例的數據,這樣雖然避免了資源的重復創建,不過這種寫法卻會造成內存泄漏,因為非靜態內部類默認會持有外部類的引用,而該非靜態內部類又創建了一個靜態的實例,該實例的生命周期和應用的一樣長,這就導致了該靜態實例一直會持有該Activity的引用,導致Activity的內存資源不能正常回收。正確的做法為:
將該內部類設為靜態內部類或將該內部類抽取出來封裝成一個單例,如果需要使用Context,請按照上面推薦的使用Application 的 Context。當然,Application 的 context 不是萬能的,所以也不能隨便亂用,對于有些地方則必須使用 Activity 的 Context,對于Application,Service,Activity三者的Context的應用場景如下:
其中: NO1表示 Application 和 Service 可以啟動一個 Activity,不過需要創建一個新的 task 任務隊列。而對于 Dialog 而言,只有在 Activity 中才能創建
集合引發的內存泄漏
我們通常會把一些對象的引用加入到集合容器(比如ArrayList)中,當我們不再需要該對象時,并沒有把它的引用從集合中清理掉,當集合中的內容過于大的時候,并且是static的時候就造成了內存泄漏,所有我們最好在onDestory情況并讓其不可達
private List<String> nameList;
private List<Fragment> list;
@Override
public void onDestroy() {
super.onDestroy();
if (nameList != null){
nameList.clear();
nameList = null;
}
if (list != null){
list.clear();
list = null;
}
}
webView引發的內存泄漏
WebView解析網頁時會申請Native堆內存用于保存頁面元素,當頁面較復雜時會有很大的內存占用。如果頁面包含圖片,內存占用會更嚴重。并且打開新頁面時,為了能快速回退,之前頁面占用的內存也不會釋放。有時瀏覽十幾個網頁,都會占用幾百兆的內存。這樣加載網頁較多時,會導致系統不堪重負,最終強制關閉應用,也就是出現應用閃退或重啟。
由于占用的都是Native堆內存,所以實際占用的內存大小不會顯示在常用的DDMS Heap工具中(這里看到的只是Java虛擬機分配的內存,一般即使Native堆內存已經占用了幾百兆,這里顯示的還只是幾兆或十幾兆)。只有使用adb shell中的一些命令比如dumpsys meminfo 包名,或者在程序中使用Debug.getNativeHeapSize()才能看到。
具體可以參考下 我的另一個adb看內存:http://www.lxweimin.com/writer#/notebooks/11604270/notes/27577456
據說由于WebView的一個BUG,即使它所在的Activity(或者Service)結束也就是onDestroy()之后,或者直接調用WebView.destroy()之后,它所占用這些內存也不會被釋放。
解決這個問題最直接的方法是:把使用了WebView的Activity(或者Service)放在單獨的進程里。然后在檢測到應用占用內存過大有可能被系統干掉或者它所在的Activity(或者Service)結束后,調用System.exit(0),主動Kill掉進程。由于系統的內存分配是以進程為準的,進程關閉后,系統會自動回收所有內存。
關于WebView的跟多內容請參見 : Android WebView Memory Leak WebView內存泄漏
其他常見的引起內存泄漏原因
構造Adapter時,沒有使用緩存的 convertView
Bitmap在不使用的時候沒有使用recycle()釋放內存
非靜態內部類的靜態實例容易造成內存泄漏:即一個類中如果你不能夠控制它其中內部類的生命周期(譬如Activity中的一些特殊Handler等),則盡量使用靜態類和弱引用來處理(譬如ViewRoot的實現)。
警惕線程未終止造成的內存泄露;譬如在Activity中關聯了一個生命周期超過Activity的Thread,在退出Activity時切記結束線程。一個典型的例子就是HandlerThread的run方法是一個死循環,它不會自己結束,線程的生命周期超過了Activity生命周期,我們必須手動在Activity的銷毀方法中中調運thread.getLooper().quit();才不會泄露。
對象的注冊與反注冊沒有成對出現造成的內存泄露;譬如注冊廣播接收器、注冊觀察者(典型的譬如數據庫的監聽)等。
創建與關閉沒有成對出現造成的泄露;譬如Cursor資源必須手動關閉,WebView必須手動銷毀,流等對象必須手動關閉等。
不要在執行頻率很高的方法或者循環中創建對象(比如onMeasure),可以使用HashTable等創建一組對象容器從容器中取那些對象,而不用每次new與釋放。
避免代碼設計模式的錯誤造成內存泄露;譬如循環引用,A持有B,B持有C,C持有A,這樣的設計誰都得不到釋放。
總結
Android內存優化主要是針對堆(Heap)而言的,當堆中對象的作用域結束的時候,這部分內存也不會立刻被回收,而是等待系統GC進行回收。
Java中造成內存泄漏的根本原因是:堆內存中長生命周期的對象持有短生命周期對象的強/軟引用,盡管短生命周期對象已經不再需要,但是因為長生命周期對象持有它的引用而導致不能被回收。