內存管理的目的就是讓我們在開發(fā)中怎么有效的避免我們的應用出現(xiàn)內存泄漏的問題袄膏。內存泄漏大家都不陌生了默色,簡單粗俗的講谎势,就是該被釋放的對象沒有釋放藤韵,一直被某個或某些實例所持有卻不再被使用導致 GC 不能回收虐沥。最近自己閱讀了大量相關的文檔資料,打算做個 總結 沉淀下來跟大家一起分享和學習泽艘,也給自己一個警示欲险,以后 coding 時怎么避免這些情況,提高應用的體驗和質量匹涮。
我會從 java 內存泄漏的基礎知識開始盯荤,并通過具體例子來說明 Android 引起內存泄漏的各種原因,以及如何利用工具來分析應用內存泄漏焕盟,最后再做總結秋秤。
Java 內存分配策略
Java 程序運行時的內存分配策略有三種,分別是靜態(tài)分配,棧式分配,和堆式分配,對應的脚翘,三種存儲策略使用的內存空間主要分別是靜態(tài)存儲區(qū)(也稱方法區(qū))灼卢、棧區(qū)和堆區(qū)。
靜態(tài)存儲區(qū)(方法區(qū)):主要存放靜態(tài)數據来农、全局 static 數據和常量鞋真。這塊內存在程序編譯時就已經分配好,并且在程序整個運行期間都存在沃于。
棧區(qū) :當方法被執(zhí)行時涩咖,方法體內的局部變量(其中包括基礎數據類型海诲、對象的引用)都在棧上創(chuàng)建,并在方法執(zhí)行結束時這些局部變量所持有的內存將會自動被釋放檩互。因為棧內存分配運算內置于處理器的指令集中特幔,效率很高,但是分配的內存容量有限闸昨。
堆區(qū) : 又稱動態(tài)內存分配蚯斯,通常就是指在程序運行時直接 new 出來的內存,也就是對象的實例饵较。這部分內存在不使用時將會由 Java 垃圾回收器來負責回收拍嵌。
棧與堆的區(qū)別:
在方法體內定義的(局部變量)一些基本類型的變量和對象的引用變量都是在方法的棧內存中分配的。當在一段方法塊中定義一個變量時循诉,Java 就會在棧中為該變量分配內存空間横辆,當超過該變量的作用域后,該變量也就無效了茄猫,分配給它的內存空間也將被釋放掉龄糊,該內存空間可以被重新使用。
堆內存用來存放所有由 new 創(chuàng)建的對象(包括該對象其中的所有成員變量)和數組募疮。在堆中分配的內存炫惩,將由 Java 垃圾回收器來自動管理。在堆中產生了一個數組或者對象后阿浓,還可以在棧中定義一個特殊的變量他嚷,這個變量的取值等于數組或者對象在堆內存中的首地址,這個特殊的變量就是我們上面說的引用變量芭毙。我們可以通過這個引用變量來訪問堆中的對象或者數組筋蓖。
舉個例子:
public class Sample {
int s1 = 0;
Sample mSample1 = new Sample();
public void method() {
int s2 = 1;
Sample mSample2 = new Sample();
}
}
Sample mSample3 = new Sample();
Sample 類的局部變量 s2 和引用變量 mSample2 都是存在于棧中,但 mSample2 指向的對象是存在于堆上的退敦。
mSample3 指向的對象實體存放在堆上粘咖,包括這個對象的所有成員變量 s1 和 mSample1,而它自己存在于棧中侈百。
結論:
局部變量的基本數據類型和引用存儲于棧中瓮下,引用的對象實體存儲于堆中《塾颍—— 因為它們屬于方法中的變量讽坏,生命周期隨方法而結束。
成員變量全部存儲與堆中(包括基本數據類型例证,引用和引用的對象實體)—— 因為它們屬于類路呜,類對象終究是要被new出來使用的。
了解了 Java 的內存分配之后,我們再來看看 Java 是怎么管理內存的胀葱。
Java是如何管理內存
Java的內存管理就是對象的分配和釋放問題漠秋。在 Java 中,程序員需要通過關鍵字 new 為每個對象申請內存空間 (基本類型除外)抵屿,所有的對象都在堆 (Heap)中分配空間庆锦。另外,對象的釋放是由 GC 決定和執(zhí)行的晌该。在 Java 中肥荔,內存的分配是由程序完成的绿渣,而內存的釋放是由 GC 完成的朝群,這種收支兩條線的方法確實簡化了程序員的工作。但同時中符,它也加重了JVM的工作姜胖。這也是 Java 程序運行速度較慢的原因之一。因為淀散,GC 為了能夠正確釋放對象右莱,GC 必須監(jiān)控每一個對象的運行狀態(tài),包括對象的申請档插、引用慢蜓、被引用、賦值等郭膛,GC 都需要進行監(jiān)控晨抡。
監(jiān)視對象狀態(tài)是為了更加準確地、及時地釋放對象则剃,而釋放對象的根本原則就是該對象不再被引用耘柱。
為了更好理解 GC 的工作原理,我們可以將對象考慮為有向圖的頂點棍现,將引用關系考慮為圖的有向邊调煎,有向邊從引用者指向被引對象。另外己肮,每個線程對象可以作為一個圖的起始頂點士袄,例如大多程序從 main 進程開始執(zhí)行,那么該圖就是以 main 進程頂點開始的一棵根樹谎僻。在這個有向圖中窖剑,根頂點可達的對象都是有效對象,GC將不回收這些對象戈稿。如果某個對象 (連通子圖)與這個根頂點不可達(注意西土,該圖為有向圖),那么我們認為這個(這些)對象不再被引用鞍盗,可以被 GC 回收需了。
以下跳昼,我們舉一個例子說明如何用有向圖表示內存管理。對于程序的每一個時刻肋乍,我們都有一個有向圖表示JVM的內存分配情況鹅颊。以下右圖,就是左邊程序運行到第6行的示意圖墓造。
Java使用有向圖的方式進行內存管理堪伍,可以消除引用循環(huán)的問題,例如有三個對象觅闽,相互引用帝雇,只要它們和根進程不可達的,那么GC也是可以回收它們的蛉拙。這種方式的優(yōu)點是管理內存的精度很高尸闸,但是效率較低妥色。另外一種常用的內存管理技術是使用計數器乔妈,例如COM模型采用計數器方式管理構件柱彻,它與有向圖相比缨睡,精度行低(很難處理循環(huán)引用的問題)房蝉,但執(zhí)行效率很高泡徙。
什么是Java中的內存泄露
在Java中朋腋,內存泄漏就是存在一些被分配的對象腻扇,這些對象有下面兩個特點轴脐,首先调卑,這些對象是可達的,即在有向圖中豁辉,存在通路可以與其相連令野;其次,這些對象是無用的徽级,即程序以后不會再使用這些對象气破。如果對象滿足這兩個條件,這些對象就可以判定為Java中的內存泄漏餐抢,這些對象不會被GC所回收现使,然而它卻占用內存。
在C++中旷痕,內存泄漏的范圍更大一些碳锈。有些對象被分配了內存空間,然后卻不可達欺抗,由于C++中沒有GC售碳,這些內存將永遠收不回來。在Java中,這些不可達的對象都由GC負責回收贸人,因此程序員不需要考慮這部分的內存泄露间景。
通過分析,我們得知艺智,對于C++倘要,程序員需要自己管理邊和頂點,而對于Java程序員只需要管理邊就可以了(不需要管理頂點的釋放)十拣。通過這種方式封拧,Java提高了編程的效率。
因此夭问,通過以上分析泽西,我們知道在Java中也有內存泄漏,但范圍比C++要小一些甲喝。因為Java從語言上保證尝苇,任何對象都是可達的铛只,所有的不可達對象都由GC管理埠胖。
對于程序員來說,GC基本是透明的淳玩,不可見的直撤。雖然,我們只有幾個函數可以訪問GC蜕着,例如運行GC的函數System.gc()谋竖,但是根據Java語言規(guī)范定義, 該函數不保證JVM的垃圾收集器一定會執(zhí)行承匣。因為蓖乘,不同的JVM實現(xiàn)者可能使用不同的算法管理GC。通常韧骗,GC的線程的優(yōu)先級別較低嘉抒。JVM調用GC的策略也有很多種,有的是內存使用到達一定程度時袍暴,GC才開始工作些侍,也有定時執(zhí)行的,有的是平緩執(zhí)行GC政模,有的是中斷式執(zhí)行GC岗宣。但通常來說,我們不需要關心這些淋样。除非在一些特定的場合耗式,GC的執(zhí)行影響應用程序的性能,例如對于基于Web的實時系統(tǒng),如網絡游戲等刊咳,用戶不希望GC突然中斷應用程序執(zhí)行而進行垃圾回收措嵌,那么我們需要調整GC的參數,讓GC能夠通過平緩的方式釋放內存芦缰,例如將垃圾回收分解為一系列的小步驟執(zhí)行企巢,Sun提供的HotSpot JVM就支持這一特性。
同樣給出一個 Java 內存泄漏的典型例子让蕾,
Vector v = new Vector(10);
for (int i = 1; i < 100; i++) {
Object o = new Object();
v.add(o);
o = null;
}
在這個例子中浪规,我們循環(huán)申請Object對象,并將所申請的對象放入一個 Vector 中探孝,如果我們僅僅釋放引用本身笋婿,那么 Vector 仍然引用該對象,所以這個對象對 GC 來說是不可回收的顿颅。因此缸濒,如果對象加入到Vector 后,還必須從 Vector 中刪除粱腻,最簡單的方法就是將 Vector 對象設置為 null庇配。
詳細Java中的內存泄漏
1.Java內存回收機制
不論哪種語言的內存分配方式,都需要返回所分配內存的真實地址绍些,也就是返回一個指針到內存塊的首地址捞慌。Java中對象是采用new或者反射的方法創(chuàng)建的,這些對象的創(chuàng)建都是在堆(Heap)中分配的柬批,所有對象的回收都是由Java虛擬機通過垃圾回收機制完成的啸澡。GC為了能夠正確釋放對象,會監(jiān)控每個對象的運行狀況氮帐,對他們的申請嗅虏、引用、被引用上沐、賦值等狀況進行監(jiān)控皮服,Java會使用有向圖的方法進行管理內存,實時監(jiān)控對象是否可以達到奄容,如果不可到達冰更,則就將其回收,這樣也可以消除引用循環(huán)的問題昂勒。在Java語言中蜀细,判斷一個內存空間是否符合垃圾收集標準有兩個:一個是給對象賦予了空值null,以下再沒有調用過戈盈,另一個是給對象賦予了新值奠衔,這樣重新分配了內存空間谆刨。
2.Java內存泄漏引起的原因
內存泄漏是指無用對象(不再使用的對象)持續(xù)占有內存或無用對象的內存得不到及時釋放,從而造成內存空間的浪費稱為內存泄漏归斤。內存泄露有時不嚴重且不易察覺痊夭,這樣開發(fā)者就不知道存在內存泄露,但有時也會很嚴重脏里,會提示你Out of memory她我。j
Java內存泄漏的根本原因是什么呢?長生命周期的對象持有短生命周期對象的引用就很可能發(fā)生內存泄漏迫横,盡管短生命周期對象已經不再需要番舆,但是因為長生命周期持有它的引用而導致不能被回收,這就是Java中內存泄漏的發(fā)生場景矾踱。具體主要有如下幾大類:
1恨狈、靜態(tài)集合類引起內存泄漏:
像HashMap、Vector等的使用最容易出現(xiàn)內存泄露呛讲,這些靜態(tài)變量的生命周期和應用程序一致禾怠,他們所引用的所有的對象Object也不能被釋放,因為他們也將一直被Vector等引用著贝搁。
例如
Static Vector v = new Vector(10);
for (int i = 1; i<100; i++)
{
Object o = new Object();
v.add(o);
o = null;
}
在這個例子中吗氏,循環(huán)申請Object 對象,并將所申請的對象放入一個Vector 中徘公,如果僅僅釋放引用本身(o=null)牲证,那么Vector 仍然引用該對象哮针,所以這個對象對GC 來說是不可回收的关面。因此,如果對象加入到Vector 后十厢,還必須從Vector 中刪除等太,最簡單的方法就是將Vector對象設置為null。
2蛮放、當集合里面的對象屬性被修改后缩抡,再調用remove()方法時不起作用。
例如:
public static void main(String[] args)
{
Set<Person> set = new HashSet<Person>();
Person p1 = new Person("唐僧","pwd1",25);
Person p2 = new Person("孫悟空","pwd2",26);
Person p3 = new Person("豬八戒","pwd3",27);
set.add(p1);
set.add(p2);
set.add(p3);
System.out.println("總共有:"+set.size()+" 個元素!"); //結果:總共有:3 個元素!
p3.setAge(2); //修改p3的年齡,此時p3元素對應的hashcode值發(fā)生改變
set.remove(p3); //此時remove不掉包颁,造成內存泄漏
set.add(p3); //重新添加瞻想,居然添加成功
System.out.println("總共有:"+set.size()+" 個元素!"); //結果:總共有:4 個元素!
for (Person person : set)
{
System.out.println(person);
}
}
3、監(jiān)聽器
在java 編程中娩嚼,我們都需要和監(jiān)聽器打交道蘑险,通常一個應用當中會用到很多監(jiān)聽器,我們會調用一個控件的諸如addXXXListener()等方法來增加監(jiān)聽器岳悟,但往往在釋放對象的時候卻沒有記住去刪除這些監(jiān)聽器佃迄,從而增加了內存泄漏的機會泼差。
4、各種連接
比如數據庫連接(dataSourse.getConnection())呵俏,網絡連接(socket)和io連接堆缘,除非其顯式的調用了其close()方法將其連接關閉,否則是不會自動被GC 回收的普碎。對于Resultset 和Statement 對象可以不進行顯式回收吼肥,但Connection 一定要顯式回收,因為Connection 在任何時候都無法自動回收麻车,而Connection一旦回收潜沦,Resultset 和Statement 對象就會立即為NULL。但是如果使用連接池绪氛,情況就不一樣了唆鸡,除了要顯式地關閉連接,還必須顯式地關閉Resultset Statement 對象(關閉其中一個枣察,另外一個也會關閉)争占,否則就會造成大量的Statement 對象無法釋放,從而引起內存泄漏序目。這種情況下一般都會在try里面去的連接臂痕,在finally里面釋放連接。
5猿涨、內部類和外部模塊的引用
內部類的引用是比較容易遺忘的一種握童,而且一旦沒釋放可能導致一系列的后繼類對象沒有釋放。此外程序員還要小心外部模塊不經意的引用叛赚,例如程序員A 負責A 模塊澡绩,調用了B 模塊的一個方法如:
public void registerMsg(Object b);
這種調用就要非常小心了,傳入了一個對象俺附,很可能模塊B就保持了對該對象的引用肥卡,這時候就需要注意模塊B 是否提供相應的操作去除引用。
6事镣、單例模式
不正確使用單例模式是引起內存泄漏的一個常見問題步鉴,單例對象在初始化后將在JVM的整個生命周期中存在(以靜態(tài)變量的方式),如果單例對象持有外部的引用璃哟,那么這個對象將不能被JVM正撤兆粒回收,導致內存泄漏随闪,考慮下面的例子:
class A{
public A(){
B.getInstance().setA(this);
}
....
}
//B類采用單例模式
class B{
private A a;
private static B instance=new B();
public B(){}
public static B getInstance(){
return instance;
}
public void setA(A a){
this.a=a;
}
//getter...
}
顯然B采用singleton模式阳似,它持有一個A對象的引用,而這個A類的對象將不能被回收蕴掏。想象下如果A是個比較復雜的對象或者集合類型會發(fā)生什么情況
Android中常見的內存泄漏匯總
集合類泄漏
集合類如果僅僅有添加元素的方法障般,而沒有相應的刪除機制调鲸,導致內存被占用。如果這個集合類是全局性的變量 (比如類中的靜態(tài)屬性挽荡,全局性的 map 等即有靜態(tài)引用或 final 一直指向它)藐石,那么沒有相應的刪除機制,很可能導致集合所占用的內存只增不減定拟。比如上面的典型例子就是其中一種情況于微,當然實際上我們在項目中肯定不會寫這么 2B 的代碼,但稍不注意還是很容易出現(xiàn)這種情況青自,比如我們都喜歡通過 HashMap 做一些緩存之類的事株依,這種情況就要多留一些心眼。
單例造成的內存泄漏
由于單例的靜態(tài)特性使得其生命周期跟應用的生命周期一樣長延窜,所以如果使用不恰當的話恋腕,很容易造成內存泄漏。比如下面一個典型的例子逆瑞,
public class AppManager {
private static AppManager instance;
private Context context;
private AppManager(Context context) {
this.context = context;
}
public static AppManager getInstance(Context context) {
if (instance == null) {
instance = new AppManager(context);
}
return instance;
}
}
這是一個普通的單例模式荠藤,當創(chuàng)建這個單例的時候,由于需要傳入一個Context获高,所以這個Context的生命周期的長短至關重要:
1哈肖、如果此時傳入的是 Application 的 Context,因為 Application 的生命周期就是整個應用的生命周期念秧,所以這將沒有任何問題淤井。
2、如果此時傳入的是 Activity 的 Context摊趾,當這個 Context 所對應的 Activity 退出時币狠,由于該 Context 的引用被單例對象所持有,其生命周期等于整個應用程序的生命周期严就,所以當前 Activity 退出時它的內存并不會被回收总寻,這就造成泄漏了。
正確的方式應該改為下面這種方式:
public class AppManager {
private static AppManager instance;
private Context context;
private AppManager(Context context) {
this.context = context.getApplicationContext();// 使用Application 的context
}
public static AppManager getInstance(Context context) {
if (instance == null) {
instance = new AppManager(context);
}
return instance;
}
}
或者這樣寫梢为,連 Context 都不用傳進來了:
在你的 Application 中添加一個靜態(tài)方法,getContext() 返回 Application 的 context轰坊,
...
context = getApplicationContext();
...
/**
* 獲取全局的context
* @return 返回全局context對象
*/
public static Context getContext(){
return context;
}
public class AppManager {
private static AppManager instance;
private Context context;
private AppManager() {
this.context = MyApplication.getContext();// 使用Application 的context
}
public static AppManager getInstance() {
if (instance == null) {
instance = new AppManager();
}
return instance;
}
}
匿名內部類/非靜態(tài)內部類和異步線程
非靜態(tài)內部類創(chuàng)建靜態(tài)實例造成的內存泄漏
有的時候我們可能會在啟動頻繁的Activity中铸董,為了避免重復創(chuàng)建相同的數據資源,可能會出現(xiàn)這種寫法:
public class MainActivity extends AppCompatActivity {
private static TestResource mResource = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if(mManager == null){
mManager = new TestResource();
}
//...
}
class TestResource {
//...
}
}
這樣就在Activity內部創(chuàng)建了一個非靜態(tài)內部類的單例肴沫,每次啟動Activity時都會使用該單例的數據粟害,這樣雖然避免了資源的重復創(chuàng)建,不過這種寫法卻會造成內存泄漏颤芬,因為非靜態(tài)內部類默認會持有外部類的引用悲幅,而該非靜態(tài)內部類又創(chuàng)建了一個靜態(tài)的實例套鹅,該實例的生命周期和應用的一樣長,這就導致了該靜態(tài)實例一直會持有該Activity的引用汰具,導致Activity的內存資源不能正匙柯梗回收。正確的做法為:
將該內部類設為靜態(tài)內部類或將該內部類抽取出來封裝成一個單例留荔,如果需要使用Context吟孙,請按照上面推薦的使用Application 的 Context。當然聚蝶,Application 的 context 不是萬能的杰妓,所以也不能隨便亂用,對于有些地方則必須使用 Activity 的 Context碘勉,對于Application巷挥,Service,Activity三者的Context的應用場景如下:
其中: NO1表示 Application 和 Service 可以啟動一個 Activity验靡,不過需要創(chuàng)建一個新的 task 任務隊列句各。而對于 Dialog 而言,只有在 Activity 中才能創(chuàng)建
匿名內部類
android開發(fā)經常會繼承實現(xiàn)Activity/Fragment/View晴叨,此時如果你使用了匿名類凿宾,并被異步線程持有了,那要小心了兼蕊,如果沒有任何措施這樣一定會導致泄露
public class MainActivity extends Activity {
...
Runnable ref1 = new MyRunable();
Runnable ref2 = new Runnable() {
@Override
public void run() {
}
};
...
}
ref1和ref2的區(qū)別是初厚,ref2使用了匿名內部類。我們來看看運行時這兩個引用的內存:
可以看到孙技,ref1沒什么特別的产禾。
但ref2這個匿名類的實現(xiàn)對象里面多了一個引用:
this$0這個引用指向MainActivity.this,也就是說當前的MainActivity實例會被ref2持有牵啦,如果將這個引用再傳入一個異步線程亚情,此線程和此Acitivity生命周期不一致的時候,就造成了Activity的泄露哈雏。
Handler 造成的內存泄漏
Handler 的使用造成的內存泄漏問題應該說是最為常見了楞件,很多時候我們?yōu)榱吮苊?ANR 而不在主線程進行耗時操作,在處理網絡任務或者封裝一些請求回調等api都借助Handler來處理裳瘪,但 Handler 不是萬能的土浸,對于 Handler 的使用代碼編寫一不規(guī)范即有可能造成內存泄漏。另外彭羹,我們知道 Handler黄伊、Message 和 MessageQueue 都是相互關聯(lián)在一起的,萬一 Handler 發(fā)送的 Message 尚未被處理派殷,則該 Message 及發(fā)送它的 Handler 對象將被線程 MessageQueue 一直持有还最。
由于 Handler 屬于 TLS(Thread Local Storage) 變量, 生命周期和 Activity 是不一致的墓阀。因此這種實現(xiàn)方式一般很難保證跟 View 或者 Activity 的生命周期保持一致,故很容易導致無法正確釋放拓轻。
舉個例子:
public class SampleActivity extends Activity {
private final Handler mLeakyHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// ...
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Post a message and delay its execution for 10 minutes.
mLeakyHandler.postDelayed(new Runnable() {
@Override
public void run() { /* ... */ }
}, 1000 * 60 * 10);
// Go back to the previous Activity.
finish();
}
}
在該 SampleActivity 中聲明了一個延遲10分鐘執(zhí)行的消息 Message斯撮,mLeakyHandler 將其 push 進了消息隊列 MessageQueue 里。當該 Activity 被 finish() 掉時悦即,延遲執(zhí)行任務的 Message 還會繼續(xù)存在于主線程中吮成,它持有該 Activity 的 Handler 引用,所以此時 finish() 掉的 Activity 就不會被回收了從而造成內存泄漏(因 Handler 為非靜態(tài)內部類辜梳,它會持有外部類的引用粱甫,在這里就是指 SampleActivity)。
修復方法:在 Activity 中避免使用非靜態(tài)內部類作瞄,比如上面我們將 Handler 聲明為靜態(tài)的茶宵,則其存活期跟 Activity 的生命周期就無關了。同時通過弱引用的方式引入 Activity宗挥,避免直接將 Activity 作為 context 傳進去乌庶,見下面代碼:
public class SampleActivity extends Activity {
/**
* Instances of static inner classes do not hold an implicit
* reference to their outer class.
*/
private static class MyHandler extends Handler {
private final WeakReference<SampleActivity> mActivity;
public MyHandler(SampleActivity activity) {
mActivity = new WeakReference<SampleActivity>(activity);
}
@Override
public void handleMessage(Message msg) {
SampleActivity activity = mActivity.get();
if (activity != null) {
// ...
}
}
}
private final MyHandler mHandler = new MyHandler(this);
/**
* Instances of anonymous classes do not hold an implicit
* reference to their outer class when they are "static".
*/
private static final Runnable sRunnable = new Runnable() {
@Override
public void run() { /* ... */ }
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Post a message and delay its execution for 10 minutes.
mHandler.postDelayed(sRunnable, 1000 * 60 * 10);
// Go back to the previous Activity.
finish();
}
}
綜述,即推薦使用靜態(tài)內部類 + WeakReference 這種方式契耿。每次使用前注意判空瞒大。
前面提到了 WeakReference,所以這里就簡單的說一下 Java 對象的幾種引用類型搪桂。
Java對引用的分類有 Strong reference, SoftReference, WeakReference, PhatomReference 四種透敌。
在Android應用的開發(fā)中,為了防止內存溢出踢械,在處理一些占用內存大而且聲明周期較長的對象時候酗电,可以盡量應用軟引用和弱引用技術。
軟/弱引用可以和一個引用隊列(ReferenceQueue)聯(lián)合使用内列,如果軟引用所引用的對象被垃圾回收器回收撵术,Java虛擬機就會把這個軟引用加入到與之關聯(lián)的引用隊列中。利用這個隊列可以得知被回收的軟/弱引用的對象列表话瞧,從而為緩沖器清除已失效的軟/弱引用嫩与。
假設我們的應用會用到大量的默認圖片,比如應用中有默認的頭像移稳,默認游戲圖標等等蕴纳,這些圖片很多地方會用到。如果每次都去讀取圖片个粱,由于讀取文件需要硬件操作,速度較慢翻翩,會導致性能較低都许。所以我們考慮將圖片緩存起來稻薇,需要的時候直接從內存中讀取。但是胶征,由于圖片占用內存空間比較大塞椎,緩存很多圖片需要很多的內存,就可能比較容易發(fā)生OutOfMemory異常睛低。這時案狠,我們可以考慮使用軟/弱引用技術來避免這個問題發(fā)生。以下就是高速緩沖器的雛形:
首先定義一個HashMap钱雷,保存軟引用對象骂铁。
private Map <String, SoftReference<Bitmap>> imageCache = new HashMap <String, SoftReference<Bitmap>> ();
再來定義一個方法,保存Bitmap的軟引用到HashMap罩抗。
使用軟引用以后拉庵,在OutOfMemory異常發(fā)生之前,這些緩存的圖片資源的內存空間可以被釋放掉的套蒂,從而避免內存達到上限钞支,避免Crash發(fā)生。
如果只是想避免OutOfMemory異常的發(fā)生操刀,則可以使用軟引用烁挟。如果對于應用的性能更在意,想盡快回收一些占用內存比較大的對象骨坑,則可以使用弱引用撼嗓。
另外可以根據對象是否經常使用來判斷選擇軟引用還是弱引用。如果該對象可能會經常使用的卡啰,就盡量用軟引用静稻。如果該對象不被使用的可能性更大些,就可以用弱引用匈辱。
ok振湾,繼續(xù)回到主題。前面所說的亡脸,創(chuàng)建一個靜態(tài)Handler內部類押搪,然后對 Handler 持有的對象使用弱引用,這樣在回收時也可以回收 Handler 持有的對象浅碾,但是這樣做雖然避免了 Activity 泄漏大州,不過 Looper 線程的消息隊列中還是可能會有待處理的消息,所以我們在 Activity 的 Destroy 時或者 Stop 時應該移除消息隊列 MessageQueue 中的消息垂谢。
下面幾個方法都可以移除 Message:
public final void removeCallbacks(Runnable r);
public final void removeCallbacks(Runnable r, Object token);
public final void removeCallbacksAndMessages(Object token);
public final void removeMessages(int what);
public final void removeMessages(int what, Object object);
盡量避免使用 static 成員變量
如果成員變量被聲明為 static厦画,那我們都知道其生命周期將與整個app進程生命周期一樣。
這會導致一系列問題,如果你的app進程設計上是長駐內存的根暑,那即使app切到后臺力试,這部分內存也不會被釋放。按照現(xiàn)在手機app內存管理機制排嫌,占內存較大的后臺進程將優(yōu)先回收畸裳,yi'wei如果此app做過進程互保保活淳地,那會造成app在后臺頻繁重啟怖糊。當手機安裝了你參與開發(fā)的app以后一夜時間手機被消耗空了電量、流量颇象,你的app不得不被用戶卸載或者靜默伍伤。
這里修復的方法是:
不要在類初始時初始化靜態(tài)成員『坏剑可以考慮lazy初始化嚷缭。
架構設計上要思考是否真的有必要這樣做,盡量避免耍贾。如果架構需要這么設計阅爽,那么此對象的生命周期你有責任管理起來。
避免 override finalize()
1荐开、finalize 方法被執(zhí)行的時間不確定付翁,不能依賴與它來釋放緊缺的資源。時間不確定的原因是:
虛擬機調用GC的時間不確定
Finalize daemon線程被調度到的時間不確定
2晃听、finalize 方法只會被執(zhí)行一次百侧,即使對象被復活,如果已經執(zhí)行過了 finalize 方法能扒,再次被 GC 時也不會再執(zhí)行了,原因是:
含有 finalize 方法的 object 是在 new 的時候由虛擬機生成了一個 finalize reference 在來引用到該Object的初斑,而在 finalize 方法執(zhí)行的時候,該 object 所對應的 finalize Reference 會被釋放掉砂竖,即使在這個時候把該 object 復活(即用強引用引用住該 object ),再第二次被 GC 的時候由于沒有了 finalize reference 與之對應鹃答,所以 finalize 方法不會再執(zhí)行乎澄。
3测摔、含有Finalize方法的object需要至少經過兩輪GC才有可能被釋放。
資源未關閉造成的內存泄漏
對于使用了BraodcastReceiver,ContentObserver舟肉,F(xiàn)ile修噪,游標 Cursor查库,Stream,Bitmap等資源的使用整慎,應該在Activity銷毀時及時關閉或者注銷围苫,否則這些資源將不會被回收,造成內存泄漏剂府。
一些不良代碼造成的內存壓力
有些代碼并不造成內存泄露,但是它們淤袜,或是對沒使用的內存沒進行有效及時的釋放衰伯,或是沒有有效的利用已有的對象而是頻繁的申請新內存。
比如:
Bitmap 沒調用 recycle()方法烦周,對于 Bitmap 對象在不使用時,我們應該先調用 recycle() 釋放內存怎顾,然后才它設置為 null. 因為加載 Bitmap 對象的內存空間,一部分是 java 的槐雾,一部分 C 的(因為 Bitmap 分配的底層是通過 JNI 調用的 )蚜退。 而這個 recyle() 就是針對 C 部分的內存釋放。
構造 Adapter 時钻注,沒有使用緩存的 convertView ,每次都在創(chuàng)建新的 converView。這里推薦使用 ViewHolder杏死。
總結
對 Activity 等組件的引用應該控制在 Activity 的生命周期之內; 如果不能就考慮使用 getApplicationContext 或者 getApplication腐巢,以避免 Activity 被外部長生命周期的對象引用而泄露玄括。
盡量不要在靜態(tài)變量或者靜態(tài)內部類中使用非靜態(tài)外部成員變量(包括context ),即使要使用遭京,也要考慮適時把外部成員變量置空哪雕;也可以在內部類中使用弱引用來引用外部類的變量。
對于生命周期比Activity長的內部類對象斯嚎,并且內部類中使用了外部類的成員變量,可以這樣做避免內存泄漏:
將內部類改為靜態(tài)內部類
靜態(tài)內部類中使用弱引用來引用外部類的成員變量
Handler 的持有的引用對象最好使用弱引用糠惫,資源釋放時也可以清空 Handler 里面的消息苦始。比如在 Activity onStop 或者 onDestroy 的時候,取消掉該 Handler 對象的 Message和 Runnable.
在 Java 的實現(xiàn)過程中理郑,也要考慮其對象釋放咨油,最好的方法是在不使用某對象時,顯式地將此對象賦值為 null赚爵,比如使用完Bitmap 后先調用 recycle()法瑟,再賦為null,清空對圖片等資源有直接引用或者間接引用的數組(使用 array.clear() ; array = null)等,最好遵循誰創(chuàng)建誰釋放的原則窝剖。
正確關閉資源酥夭,對于使用了BraodcastReceiver脊奋,ContentObserver疙描,F(xiàn)ile,游標 Cursor久又,Stream待错,Bitmap等資源的使用,應該在Activity銷毀時及時關閉或者注銷。
保持對對象生命周期的敏感讲冠,特別注意單例、靜態(tài)對象谱仪、全局性集合等的生命周期否彩。