前言
本篇文章是《全面理解Android內(nèi)存優(yōu)化》系列文章的第一篇饶米。系列的主要目的是希望將Android開(kāi)發(fā)中涉及性能優(yōu)化的部分做一次系統(tǒng)的歸納恒界、總結(jié)和學(xué)習(xí)睦刃。本系列文章包含理論基礎(chǔ)、工具使用十酣、項(xiàng)目實(shí)踐三個(gè)部分眯勾。
理論基礎(chǔ):「全面理解Android內(nèi)存優(yōu)化 1」-Android的內(nèi)存機(jī)制與管理建議枣宫,主要講解Android性能優(yōu)化時(shí)涉及到的各種基礎(chǔ)知識(shí)
工具使用:「全面理解Android內(nèi)存優(yōu)化 2」-內(nèi)存優(yōu)化工具的使用,主要講解Android性能優(yōu)化時(shí)各種常用工具的使用吃环。
項(xiàng)目實(shí)踐:「全面理解Android內(nèi)存優(yōu)化 3」-從理論到實(shí)踐,以一個(gè)實(shí)際APP為例洋幻,總結(jié)在在開(kāi)發(fā)中會(huì)被忽視的內(nèi)存問(wèn)題郁轻。
本文中實(shí)戰(zhàn)時(shí)使用的項(xiàng)目地址:https://github.com/linux-link/Fan,可以先閱讀這篇文章了解這個(gè)項(xiàng)目一次組件化與Android Jetpack的實(shí)踐
本篇屬于三個(gè)部分中的理論基礎(chǔ)部分文留。
目錄
- Java的內(nèi)存分配區(qū)域
- Java的引用類型
- Java的內(nèi)存回收機(jī)制
- 常見(jiàn)的內(nèi)存泄漏場(chǎng)景
- 內(nèi)存管理的技巧與建議
- 總結(jié)
正文
一好唯、Java的內(nèi)存分配區(qū)域
Java內(nèi)存分配主要包括以下幾個(gè)區(qū)域:
方法區(qū):存儲(chǔ)每個(gè)類的信息(包括類的名稱、方法信息燥翅、字段信息)骑篙、靜態(tài)變量、常量以及編譯器編譯后的代碼等森书。這是所有線程都共享的區(qū)域靶端。
虛擬機(jī)棧:用來(lái)存儲(chǔ)方法中的局部變量(包括在方法中聲明的非靜態(tài)變量以及函數(shù)形參)。對(duì)于基本數(shù)據(jù)類型的變量凛膏,則直接存儲(chǔ)它的值杨名,對(duì)于引用類型的變量,則存的是指向?qū)ο蟮囊貌痢>植孔兞勘淼拇笮≡诰幾g器就可以確定其大小了台谍,因此在程序執(zhí)行期間局部變量表的大小是不會(huì)改變的。每個(gè)線程都會(huì)有一個(gè)自己的棧吁断。
本地方法棧:存儲(chǔ)native方法的棧
堆:內(nèi)存分配中最大的一塊區(qū)域趁蕊。Java中的堆是用來(lái)存儲(chǔ)對(duì)象本身的以及數(shù)組(當(dāng)然,數(shù)組引用是存放在虛擬機(jī)棧中的)仔役。只不過(guò)和C語(yǔ)言中的不同是在Java中掷伙,程序員基本不用去關(guān)心空間釋放的問(wèn)題,Java的垃圾回收機(jī)制會(huì)自動(dòng)進(jìn)行處理骂因。因此這部分空間也是Java垃圾收集器管理的主要區(qū)域炎咖,內(nèi)存泄漏也是發(fā)生在這個(gè)區(qū)域的。另外寒波,堆是被所有線程共享的乘盼,在JVM中只有一個(gè)堆。
程序計(jì)數(shù)器:多線程是通過(guò)線程輪流切換來(lái)獲得CPU執(zhí)行時(shí)間的俄烁,因此绸栅,在任一具體時(shí)刻,一個(gè)CPU的內(nèi)核只會(huì)執(zhí)行一條線程中的指令页屠,為了能夠使得每個(gè)線程都在線程切換后能夠恢復(fù)在切換之前的程序執(zhí)行位置粹胯,每個(gè)線程都需要有自己獨(dú)立的程序計(jì)數(shù)器蓖柔,并且不能互相被干擾,否則就會(huì)影響到程序的正常執(zhí)行次序风纠。因此况鸣,可以這么說(shuō),程序計(jì)數(shù)器是每個(gè)線程所私有的竹观。如果你對(duì)這塊區(qū)域有興趣镐捧,請(qǐng)回看我以前寫(xiě)過(guò)的文章-深入了解Android多線程(一)Java線程基礎(chǔ)
不同的文章中會(huì)有的不同的分配方式,但是大多數(shù)都是類似的臭增,有的文章中還會(huì)提到靜態(tài)域和常量池懂酱,這兩個(gè)部分都屬于方法區(qū)(在JDK1.6中是這樣的。JDK1.7中字符串常量池誊抛,存放在堆內(nèi)存中列牺。在JDK1.8之后,字符串常量池是在本地內(nèi)存當(dāng)中拗窃。Android程序員其實(shí)并不需要關(guān)心它屬于哪個(gè)區(qū)域)瞎领。
靜態(tài)域:方法區(qū)的一部分。這里的“靜態(tài)”是指“在固定的位置”并炮。靜態(tài)存儲(chǔ)里存放程序運(yùn)行時(shí)一直存在的數(shù)據(jù)默刚。你可用關(guān)鍵字static來(lái)標(biāo)識(shí)一個(gè)對(duì)象的特定元素是靜態(tài)的,但JAVA對(duì)象本身從來(lái)不會(huì)存放在靜態(tài)存儲(chǔ)空間里逃魄。
常量池:方法區(qū)的一部分荤西。常量值通常直接存放在程序代碼內(nèi)部,這樣做是安全的伍俘,因?yàn)樗鼈冇肋h(yuǎn)不會(huì)被改變邪锌。用于存放字符串常量和基本類型常量。
注意:在Java中字符串的內(nèi)存分配比較特別癌瘾,需要額外注意觅丰。字符串對(duì)象的引用都是存儲(chǔ)在虛擬機(jī)棧中的。如果是編譯期已經(jīng)創(chuàng)建好(直接用雙引號(hào)定義的)的就存儲(chǔ)在常量池中妨退,如果是運(yùn)行期(new出來(lái)的)才能確定的就存儲(chǔ)在堆中妇萄。對(duì)于equals相等的字符串,在常量池中永遠(yuǎn)只有一份咬荷,在堆中有多份冠句。
上面講述了Java運(yùn)行時(shí)的內(nèi)存分配區(qū)域,作為Android程序員幸乒,我們更多的需要關(guān)注棧和堆懦底。不過(guò)我們可能會(huì)產(chǎn)生這樣的疑問(wèn),棧和堆有什么區(qū)別呢罕扎?為什么要同時(shí)存在這兩塊區(qū)域聚唐?帶這樣的疑問(wèn)丐重,我們回過(guò)頭來(lái)再來(lái)看看這兩個(gè)區(qū)域。
棧(stack)
棧位于通用RAM中杆查。Java中存在一個(gè)虛擬的“棧指針”扮惦,“棧指針”若向下移動(dòng),則分配新的內(nèi)存亲桦;若向上移動(dòng)径缅,則釋放那些內(nèi)存。這是一種快速高效的內(nèi)存分配方式烙肺,僅次于寄存器。
這種內(nèi)存分配方式氧卧,決定了在創(chuàng)建程序時(shí)候桃笙,Java編譯器必須知道存儲(chǔ)在棧內(nèi)所有數(shù)據(jù)的確切大小和生命周期,因?yàn)樗仨毶上鄳?yīng)的代碼沙绝,以便上下移動(dòng)“棧指針”搏明。棧區(qū)為了快速分配內(nèi)存,限制了程序的靈活性闪檬,所以該區(qū)域只存放java基本類型數(shù)據(jù)和對(duì)象星著、數(shù)組的引用,對(duì)象本身則存放在堆或常量池中
堆(heap)
堆也位在于通用RAM中粗悯,用于存放所有的Java對(duì)象虚循。堆與棧的不同之處在于,編譯器不需要知道要從堆里分配多少存儲(chǔ)區(qū)域样傍,也不必知道存儲(chǔ)的數(shù)據(jù)在堆里存活多長(zhǎng)時(shí)間横缔。
因此,在堆里分配存儲(chǔ)有很大的靈活性衫哥。當(dāng)你需要?jiǎng)?chuàng)建一個(gè)對(duì)象的時(shí)候茎刚,只需要new寫(xiě)一行簡(jiǎn)單的代碼,當(dāng)執(zhí)行這行代碼時(shí)撤逢,會(huì)自動(dòng)在堆里進(jìn)行內(nèi)存分配膛锭。為了這種靈活性,用堆進(jìn)行存儲(chǔ)分配比用棧進(jìn)行內(nèi)存分配需要更多的時(shí)間蚊荣。
二初狰、Java的引用類型
在程序編譯完,Jvm虛擬機(jī)給每個(gè)對(duì)象分配完內(nèi)存后妇押,Java的垃圾回收機(jī)制會(huì)監(jiān)控每一個(gè)對(duì)象在內(nèi)存中的運(yùn)行狀態(tài)跷究,包括對(duì)象的申請(qǐng)、引用敲霍、被引用俊马、賦值等丁存。當(dāng)某個(gè)對(duì)象不再被引用變量所引用時(shí),垃圾回收機(jī)制就會(huì)將其回收柴我,并釋放內(nèi)存空間解寝。
Java中一個(gè)對(duì)象可以被一個(gè)局部變量所引用,也可以被其他類的靜態(tài)變量引用艘儒,或者被其他對(duì)象的實(shí)例變量引用聋伦。當(dāng)對(duì)象被靜態(tài)變量引用時(shí),只有該類被銷毀界睁,該對(duì)象才會(huì)被銷毀觉增、回收。當(dāng)對(duì)象被其他對(duì)象的實(shí)例變量引用時(shí)翻斟,只有當(dāng)引用該對(duì)象的對(duì)象被銷毀或不再被引用時(shí)逾礁,該對(duì)象才會(huì)被銷毀、回收访惜。
為了更好的管理對(duì)象的引用嘹履,JDK中提供了四種引用方式,分別是強(qiáng)引用债热、軟引用砾嫉、弱引用、虛引用窒篱。下面分別介紹這幾種引用方式和適用場(chǎng)景
- 強(qiáng)引用
這是java默認(rèn)的引用對(duì)象方式焕刮,例如:
Object object=new Object();
這里的object就是以強(qiáng)引用的方式引用Object對(duì)象,被強(qiáng)引用所引用的java對(duì)象舌剂,即使內(nèi)存不足時(shí)也絕對(duì)不會(huì)垃圾回收機(jī)制回收济锄。
- 軟引用
軟引用需要通過(guò)SoftReference類實(shí)現(xiàn),例如:
SoftReference<Object> object=new SoftReference<>();
被弱引用所引用的java對(duì)象霍转,在內(nèi)存充足時(shí)荐绝,它與強(qiáng)引用相同是不會(huì)被jvm的垃圾回收機(jī)制回收的,但是當(dāng)系統(tǒng)內(nèi)存不足時(shí)避消,垃圾回收機(jī)制就會(huì)將其回收低滩。
在Android中軟引用非常常用,例如:從網(wǎng)絡(luò)中獲取的圖片岩喷,會(huì)將其暫時(shí)緩存在內(nèi)存中恕沫,當(dāng)下次再用時(shí)就可以直接從內(nèi)存中,一般為了防止造成內(nèi)存泄露纱意,會(huì)將其設(shè)為軟引用婶溯。
- 弱引用
弱引用與軟引用有些相似,區(qū)別在于弱引用所引用的的對(duì)象生命周期更短。弱引用通過(guò)WeakReference類實(shí)現(xiàn)迄委,例如:
Object object=new Object();
WeakReference<Object> wObject=new WeakReference<>(object);
對(duì)于弱引用的對(duì)象而言褐筛,當(dāng)jvm的垃圾回收機(jī)制運(yùn)行時(shí),不管內(nèi)存是否足夠叙身,總會(huì)回收該對(duì)象所占用的內(nèi)存渔扎。
- 虛引用
軟引用和弱引用可以單獨(dú)使用,但是虛引用卻不能單獨(dú)使用信轿,虛引用的主要作用是跟蹤對(duì)象被垃圾回收的狀態(tài)晃痴。
被虛引用引用的對(duì)象本身并沒(méi)的太大的意義,對(duì)象甚至感覺(jué)不到引用的存在财忽,使用虛引用的get()方法也總是為空倘核。
在Android開(kāi)發(fā)中此類引用非常少見(jiàn),故不做過(guò)多介紹即彪。
三笤虫、Java的垃圾回收機(jī)制
Java語(yǔ)言中一個(gè)顯著的特點(diǎn)就是引入了垃圾回收機(jī)制,使c++程序員最頭疼的內(nèi)存管理的問(wèn)題迎刃而解祖凫,它使得Java程序員在編寫(xiě)程序的時(shí)候不再需要考慮內(nèi)存管理。由于有個(gè)垃圾回收機(jī)制酬凳,Java中的對(duì)象不再有“作用域”的概念惠况,只有對(duì)象的引用才有“作用域”。垃圾回收可以有效的防止內(nèi)存泄露宁仔,高效的使用空閑的內(nèi)存稠屠。
垃圾回收機(jī)制在Java中主要有一下兩個(gè)作用:
1.跟蹤并監(jiān)視每個(gè)java對(duì)象,當(dāng)某個(gè)對(duì)象失去引用時(shí)翎苫,回收該對(duì)象所占的內(nèi)存权埠。
2.清理內(nèi)存分配、回收過(guò)程中產(chǎn)生的內(nèi)存碎片煎谍。
垃圾回收機(jī)制所需要完成的工作量都不算小攘蔽,因此垃圾回收的算法就成了限制java程序運(yùn)行效率的重要因素。而這也是Android App運(yùn)行過(guò)程中卡頓的一個(gè)主要原因之一呐粘。
1.垃圾回收算法
為了高效的完成內(nèi)存的回收工作满俗,在Java中設(shè)計(jì)幾種不同的垃圾回收算法:
-
標(biāo)記清除算法
垃圾回收器先從根開(kāi)始訪問(wèn)所有可達(dá)對(duì)象,將他們標(biāo)記為可達(dá)狀態(tài)作岖,然后再遍歷一次整個(gè)內(nèi)存區(qū)域唆垃,把所有沒(méi)有標(biāo)記的對(duì)象進(jìn)行回收整理。
優(yōu)點(diǎn):不需要大規(guī)模的復(fù)制操作痘儡,內(nèi)存利用效率高
缺點(diǎn):需要遍歷兩次堆空間辕万,因此會(huì)造成應(yīng)用程序暫停的時(shí)間會(huì)隨著堆內(nèi)存空間的增大而增大,而且垃圾回收回來(lái)的內(nèi)存往往是不連續(xù)的,因此整理后的堆內(nèi)存里碎片很多渐尿。
-
復(fù)制算法
將堆內(nèi)存分成兩塊相同的空間醉途,從根開(kāi)始訪問(wèn)每一個(gè)關(guān)聯(lián)的可達(dá)對(duì)象,將空間A的可達(dá)對(duì)象復(fù)制到空間B涡戳,然后回收整個(gè)空間A结蟋。
優(yōu)點(diǎn):對(duì)于復(fù)制算法而言,因?yàn)橹恍枰L問(wèn)所有存在引用的對(duì)象渔彰,將所有存在引用的對(duì)象復(fù)制走之后就可以回收整個(gè)內(nèi)存空間嵌屎,完全不用理會(huì)那些不存在引用的對(duì)象,所以遍歷空間的時(shí)間成本比較小恍涂。
缺點(diǎn):浪費(fèi)了一半內(nèi)存宝惰,復(fù)制對(duì)象需要額外的時(shí)間成本。
-
標(biāo)記整理算法
標(biāo)記整理算法充分利用上述兩種的算法的優(yōu)點(diǎn)再沧,垃圾回收器先從根開(kāi)始訪問(wèn)所有可達(dá)對(duì)象尼夺,將它們標(biāo)記為可達(dá)狀態(tài)。接下來(lái)垃圾回收器會(huì)將這些活動(dòng)對(duì)象搬遷在一起炒瘸,這一過(guò)程也被稱之為內(nèi)存壓縮淤堵,然后垃圾回收機(jī)制再次回收那些不可達(dá)對(duì)象占用的內(nèi)存空間,這樣就避免了回收產(chǎn)生的內(nèi)存碎片顷扩。
從上面敘述可以看出不論采用哪種內(nèi)存回收算法拐邪,總是利弊參半,因此隘截,實(shí)現(xiàn)垃圾回收時(shí)總會(huì)綜合使用多種設(shè)計(jì)方式扎阶。也就是針對(duì)不同的情況采用不同的垃圾回收實(shí)現(xiàn)。
2.內(nèi)存的分代回收
現(xiàn)行的垃圾回收器用分代的方式來(lái)采用不同回收設(shè)計(jì)婶芭。分代的基本思路是根據(jù)對(duì)象生存時(shí)間的長(zhǎng)短东臀,把堆內(nèi)存分成3個(gè)代
Young(年輕代)
初次分配內(nèi)存空間的對(duì)象(非靜態(tài))都會(huì)被劃為Young代。Young代中的大多數(shù)對(duì)象很快就會(huì)失去引用變?yōu)槔鴮?duì)象犀农,只有少部分對(duì)象會(huì)在垃圾回收時(shí)依然存在引用惰赋。而垃圾回收器只需要保留Young代中的存在引用的對(duì)象即可,少量的對(duì)象復(fù)制成本很小呵哨,可以充分發(fā)揮復(fù)制算法的優(yōu)點(diǎn)谤逼。所以,對(duì)于Young代主要使用復(fù)制算法來(lái)回收對(duì)象仇穗。-
Old(老年代)
如果Young代中的對(duì)象經(jīng)過(guò)多次垃圾回收依然沒(méi)有被回收掉流部,垃圾回收器會(huì)將這個(gè)對(duì)象移動(dòng)到Old代。隨著程序的持續(xù)運(yùn)行纹坐,Old代中對(duì)象會(huì)越來(lái)越多枝冀,因此Old代的空間比Young代要大。
Old代垃圾回收有兩個(gè)特征:Old代垃圾回收?qǐng)?zhí)行頻率無(wú)需太高,因?yàn)楹苌儆袑?duì)象會(huì)死掉果漾;每次對(duì)Old代執(zhí)行垃圾回收需要更長(zhǎng)的時(shí)間球切。基于以上考慮,對(duì)于Old代主要使用標(biāo)記整理算法绒障。這種算可以避免復(fù)制Old代的大量對(duì)象吨凑,而且由于Old代的對(duì)象不會(huì)很快死亡,回收過(guò)程也不會(huì)產(chǎn)生大量的內(nèi)存碎片
-
Permanent(永久代)
JDK1.8之后Permanent代被移除了户辱,而且垃圾回收器通常不會(huì)回收Permanent代的對(duì)象鸵钝,所以這里就不再介紹了。
總結(jié)來(lái)看庐镐,Young代的內(nèi)存會(huì)先被回收恩商,而且會(huì)使用專門(mén)的回收算法(復(fù)制算法)來(lái)回收Young代的內(nèi)存;對(duì)于Old代的回收頻率則要低得多必逆,主要使用標(biāo)記整理算法怠堪。
3.Android的內(nèi)存管理機(jī)制
在Android系統(tǒng)中每個(gè)APP都有一個(gè)獨(dú)立的主進(jìn)程,系統(tǒng)給每個(gè)進(jìn)程分配的內(nèi)存是大小在出廠時(shí)就被固定了,不同品牌、內(nèi)存、系統(tǒng)的手機(jī)都是不一樣的。一般來(lái)說(shuō)虽画,手機(jī)的出廠內(nèi)存越大,系統(tǒng)能分配給每個(gè)進(jìn)程的內(nèi)存上限就越大。
眾所周知,Android 5.0之后,Google給Android系統(tǒng)更換了一個(gè)更高效的虛擬機(jī)-ART
绘证。早期的Dalvik虛擬機(jī)僅有一種內(nèi)存回收算法隧膏,對(duì)于內(nèi)存的回收效率也很低。ART虛擬機(jī)則根據(jù)APP是運(yùn)行時(shí)的不同情況嚷那,采用了多種不同的垃圾回收算法胞枕,用來(lái)高效的回收內(nèi)存。
Android對(duì)于內(nèi)存回收還存在一套Low Memory Killer的機(jī)制魏宽,當(dāng)系統(tǒng)的可用內(nèi)存出現(xiàn)緊張的時(shí)候腐泻,這套機(jī)制會(huì)全局檢查所有正在運(yùn)行的進(jìn)程,并根據(jù)所需要的內(nèi)存大小队询,殺死那些權(quán)重較低的進(jìn)程派桩,并回收它的內(nèi)存。
在Android中按進(jìn)程的權(quán)重從高到低依次分為:前臺(tái)進(jìn)程(正在與用戶交互)蚌斩,可見(jiàn)進(jìn)程(不在與用戶交互)铆惑,服務(wù)進(jìn)程,后臺(tái)進(jìn)程和空進(jìn)程。
其實(shí)從Android6.0之后员魏,對(duì)于內(nèi)存的管理也是越發(fā)的嚴(yán)格丑蛤,對(duì)于用戶來(lái)說(shuō),手機(jī)會(huì)更加的流暢撕阎,不會(huì)因?yàn)閮?nèi)存不足受裹,而產(chǎn)生各種停止運(yùn)行。對(duì)于開(kāi)發(fā)者來(lái)說(shuō)虏束,我們不必絞盡腦汁關(guān)心內(nèi)存不足的問(wèn)題棉饶,但是弊端就是開(kāi)發(fā)中常用的各種進(jìn)程保活措施大多數(shù)也都已經(jīng)失效了魄眉。
四砰盐、常見(jiàn)的內(nèi)存泄漏場(chǎng)景
1.什么是內(nèi)存泄漏
內(nèi)存泄露也是個(gè)Android開(kāi)發(fā)、優(yōu)化中繞不開(kāi)的話題坑律。在學(xué)習(xí)Java的程序開(kāi)發(fā)的時(shí)候岩梳,我們不必像C\C++那樣手動(dòng)釋放對(duì)象占據(jù)的內(nèi)存,JVM的垃圾回收器會(huì)自動(dòng)回收無(wú)用對(duì)象所占的內(nèi)存空間晃择,這會(huì)給人一種錯(cuò)覺(jué)冀值,Java不會(huì)有內(nèi)存泄露的問(wèn)題,但實(shí)際上Java開(kāi)發(fā)中使用不當(dāng)宫屠,一樣會(huì)存在內(nèi)存泄露列疗。
首先我們需要簡(jiǎn)單了解一下 什么是內(nèi)存泄漏。
在程序的運(yùn)行過(guò)程中會(huì)不斷地為對(duì)象浪蹂、變量抵栈、數(shù)組等分配內(nèi)存空間,當(dāng)這些被分配出去的內(nèi)存空間不再被使用時(shí)坤次,垃圾回收器及時(shí)回收它們的內(nèi)存古劲,保證內(nèi)存區(qū)域可以再次使用。但是當(dāng)這些不再被使用的內(nèi)存空間既不能被回收缰猴,新的對(duì)象也不能使用這塊內(nèi)存空間時(shí)产艾,這就發(fā)生了內(nèi)存泄露。久而久之系統(tǒng)的可用內(nèi)存會(huì)越來(lái)越少滑绒,直到?jīng)]有可用的內(nèi)存闷堡,在Android中就會(huì)發(fā)生OutOfMemory的異常。
2.常見(jiàn)的內(nèi)存泄漏場(chǎng)景
看到這里你是否有一個(gè)疑問(wèn)疑故,為什么這些不再被使用的內(nèi)存空間不能被回收呢杠览?原因我們?cè)贘ava的垃圾回收機(jī)制中以及提到了,如果一個(gè)對(duì)象在垃圾回收時(shí)依然被一個(gè)外部引用持有纵势,那么垃圾回收器就不能回收這個(gè)對(duì)象占據(jù)的內(nèi)存空間倦零,即使這外部引用永遠(yuǎn)都不會(huì)再使用了误续。
下面我們就來(lái)介紹幾種在Android中常見(jiàn)的內(nèi)存泄漏的案例:
- 需要回收的對(duì)象被靜態(tài)變量持有
比較典型的例子就是單例中需要傳入Context時(shí),我們傳入了當(dāng)前Activity的Context
class Example {
private static volatile Example ourInstance;
private Context mContext;
static Example getInstance(Context context) {
if (ourInstance == null) {
synchronized (Example.class) {
if (ourInstance == null) {
ourInstance = new Example(context);
}
}
}
return ourInstance;
}
private Example(Context context) {
mContext = context;
}
}
如果我們代碼中如果我們傳入Activity的Context的那么該Activity占用的內(nèi)存在app運(yùn)行周期將無(wú)法被回收扫茅,具體原因請(qǐng)繼續(xù)往下看蹋嵌。
這里的Context我們可以用Application的Context替換,因?yàn)锳pplication的生命周期就是App的運(yùn)行周期葫隙。
private Example(Context context) {
mContext = context.getApplicationContext();
}
- 非靜態(tài)內(nèi)部類持有外部類的引用
在java中內(nèi)部類會(huì)隱式持有外部類的引用栽烂,一般情況下這并不會(huì)造成內(nèi)存的泄露,但是如果內(nèi)部類中執(zhí)行了耗時(shí)操作恋脚,就有可能會(huì)產(chǎn)生內(nèi)存泄露腺办。
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_welcome);
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(20000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
}
上面代碼中,Thread隱式持有了Activity類的引用糟描,當(dāng)Activity退出時(shí)怀喉,thread依然在后臺(tái)執(zhí)行,那么Activity就會(huì)因?yàn)楸缓笈_(tái)線程持有而無(wú)法正炒欤回收躬拢。
上述的例子只是用Thread舉例耗時(shí)操作,像AsyncTask见间,Handler等都存在這樣的問(wèn)題聊闯,不過(guò)隨著耗時(shí)操作的執(zhí)行完畢,線程被正常釋放米诉,Activity是可以被正沉馐撸回收的。這種在Activity中使用內(nèi)部類執(zhí)行耗時(shí)操作的做法本身就是錯(cuò)誤的史侣,也有可能導(dǎo)致其它異常情況的纏身拴泌,不提倡這種寫(xiě)法。
如果你一定要這么寫(xiě)惊橱,可以改成下面的做法:
static class MyThread extends Thread {
private SoftReference<Activity> mActivity;
public MyThread(Activity activity) {
mActivity = new SoftReference<>(activity);
}
@Override
public void run() {
super.run();
try {
Thread.sleep(20000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
將內(nèi)部類更改為靜態(tài)內(nèi)部類蚪腐,靜態(tài)內(nèi)部類不會(huì)持有外部類的引用,需要我們自行傳入李皇,這時(shí)我們用外部類的引用設(shè)置為軟引用,這個(gè)jvm在做垃圾回收時(shí)宙枷,就會(huì)回收掉內(nèi)部類對(duì)于外部類(Activity)的引用掉房,這樣Activity就可以正常銷毀了(需要注意一點(diǎn)的是,上述例子是一個(gè)在后臺(tái)執(zhí)行的線程慰丛,即使Activity被回收了卓囚,線程本身并不會(huì)被回收)。
- 資源對(duì)象未關(guān)閉
在使用IO诅病、File流或者Sqlite哪亿、Cursor等資源時(shí)要及時(shí)關(guān)閉粥烁。這些資源在進(jìn)行讀寫(xiě)操作時(shí)通常都使用了緩沖,如果及時(shí)不關(guān)閉蝇棉,這些緩沖對(duì)象就會(huì)一直被占用而得不到釋放讨阻,以致發(fā)生內(nèi)存泄露。因此我們?cè)诓恍枰褂盟鼈兊臅r(shí)候就及時(shí)關(guān)閉篡殷,以便緩沖能及時(shí)得到釋放钝吮,從而避免內(nèi)存泄露。
- 屬性動(dòng)畫(huà)造成內(nèi)存泄露
動(dòng)畫(huà)同樣是一個(gè)耗時(shí)任務(wù)板辽,比如在Activity中啟動(dòng)了屬性動(dòng)畫(huà)(ObjectAnimator)奇瘦,但是在銷毀的時(shí)候,沒(méi)有調(diào)用cancle方法劲弦,雖然我們看不到動(dòng)畫(huà)了耳标,但是這個(gè)動(dòng)畫(huà)依然會(huì)不斷地播放下去,動(dòng)畫(huà)引用所在的控件邑跪,所在的控件引用Activity次坡,這就造成Activity無(wú)法正常釋放。因此同樣要在Activity銷毀的時(shí)候cancel掉屬性動(dòng)畫(huà)呀袱,避免發(fā)生內(nèi)存泄漏
在Android中甚至是Java中贸毕,因?yàn)榇a編寫(xiě)不當(dāng),造成內(nèi)存泄露的地方有很多夜赵,這里不再枚舉明棍,會(huì)在后續(xù)的文章中講解如何監(jiān)測(cè)內(nèi)存泄露,并一步步還原出內(nèi)存泄露的原因寇僧。
五摊腋、內(nèi)存管理的技巧與建議
根據(jù)前面介紹的內(nèi)存回收機(jī)制,下面給出幾個(gè)Java內(nèi)存管理方面的小技巧嘁傀。
- 盡量使用直接量
當(dāng)使用字符串和Byte兴蒸、Short、Integer细办、Long橙凳、Float、Double笑撞、Boolean岛啸、Character包裝類的實(shí)例時(shí),不應(yīng)該使用new的方式創(chuàng)建對(duì)象茴肥,而是采用直接量來(lái)創(chuàng)建他們
例如:使用
String str=“hello”
而不是
String str=new String(“hello”);
使用直接量時(shí)Jvm的字符串緩存池會(huì)緩存這個(gè)字符串坚踩,但如果使用new去創(chuàng)建,Jvm不僅需要去進(jìn)行緩存瓤狐,而且str所引用的String對(duì)象底層還包含一個(gè)char類型數(shù)組瞬铸,造成了不必要的內(nèi)存浪費(fèi)批幌。
char[] c={‘h’,‘e’,‘l’,'l','o'};
- 使用StringBuider和StringBuffer進(jìn)字符串的拼接
學(xué)習(xí)String時(shí)我們都知道String是長(zhǎng)度不可變的,但是這樣一段程序并不會(huì)報(bào)錯(cuò)
String s1 = "hello";
s1 = s1 + " world";
這是因?yàn)閖ava在使用String對(duì)象進(jìn)行字符串拼接時(shí)會(huì)生成大量的臨時(shí)字符串嗓节,這些字符串都會(huì)占用相應(yīng)的內(nèi)存荧缘。而是用StringBuider和StringBuffer作為長(zhǎng)度可變字符串對(duì)象則不存在這樣的問(wèn)題。
- 盡早釋放無(wú)用的對(duì)象引用
一般來(lái)說(shuō)方法內(nèi)的局部變量所引用的對(duì)象生命周期很短赦政,一般不需要將對(duì)象顯式的設(shè)為null胜宇,但是有些情況除外例如
BeanNews news = new BeanNews();
//一些常規(guī)操作
……
news = null;
//耗時(shí),耗內(nèi)存的操作
……
當(dāng)局部對(duì)象之后存在耗時(shí)或耗內(nèi)存的操作時(shí)恢着,將局部對(duì)象置為null就有可能盡早釋放該對(duì)象所占用的內(nèi)存桐愉。為什么是有可能?因?yàn)槔厥帐怯蒍vm決定掰派,開(kāi)發(fā)者無(wú)法決定何時(shí)進(jìn)行回收从诲。
- 避免在循環(huán)或頻繁調(diào)用的方法中創(chuàng)建java對(duì)象
例如:
for (int i = 0; i <100 ; i++) {
BeanNews news=new BeanNews();
……
}
雖然news是局部變量,會(huì)在循環(huán)結(jié)束后回收它所占的內(nèi)存靡羡,但是在循環(huán)時(shí)也需要頻繁的給news這個(gè)引用變量分配內(nèi)存空間執(zhí)行初始化操作系洛,在這種不斷分配、回收的操作過(guò)程中略步,也會(huì)影響程序的性能描扯。
可以做如下的優(yōu)化:
BeanNews news;
for (int i = 0; i <100 ; i++) {
news=new BeanNews();
……
}
這樣就不需要為news這個(gè)引用類型的變量頻繁分配內(nèi)存趟薄,執(zhí)行初始化绽诚。
- 緩存經(jīng)常使用的對(duì)象
經(jīng)常使用的對(duì)象,我們可以考慮將該對(duì)象用緩存池保存起來(lái)杭煎,下次需要時(shí)可以直接使用恩够,不必在此進(jìn)行創(chuàng)建和初始化操作。
Android開(kāi)發(fā)過(guò)程中我們經(jīng)常使用各種集合類做為緩存容器羡铲,
……
List<BeanNews> news = new ArrayList<>();
BeanNews beanNews = new BeanNews();
……
news.add(beanNews);
……
BeanNews beanNews1 = news.get(0);
需要注意的是蜂桶,緩存是一種典型的犧牲空間換時(shí)間,我們?cè)谑褂脮r(shí)要注意不能讓緩存的容器占據(jù)過(guò)大的內(nèi)存空間也切。大型的數(shù)據(jù)緩存扑媚,一般會(huì)使用一系列淘汰算法控制緩存器占據(jù)的內(nèi)存在一個(gè)合理區(qū)間。
- 盡量不要使用finalize方法
當(dāng)一個(gè)對(duì)象在使用引用之后雷恃,在垃圾回收器回收該對(duì)象之前疆股,垃圾回收機(jī)制會(huì)先調(diào)用finalize方法進(jìn)行資源清理。
所以有的開(kāi)發(fā)者會(huì)考慮使用finalize進(jìn)行資源的清理褂萧。
但是押桃,垃圾回收的工作已經(jīng)很大了葵萎,尤其是在回收Young代的內(nèi)存時(shí)导犹,大都會(huì)引起程序的暫停唱凯。在垃圾回收已經(jīng)制約程序的運(yùn)行效率時(shí),再使用finalize進(jìn)行資源清理谎痢,將會(huì)是垃圾回收器的負(fù)擔(dān)更大磕昼,導(dǎo)致程序運(yùn)行效率更差。
- 使用軟引用(SoftReference)
當(dāng)創(chuàng)建長(zhǎng)度很大的的數(shù)組或創(chuàng)建一個(gè)占用內(nèi)存很大但是并不是十分重要的對(duì)象時(shí)节猿,都應(yīng)該考慮使用軟引用票从。使用軟引用的對(duì)象,會(huì)在內(nèi)存緊張時(shí)滨嘱,主動(dòng)“犧牲”自己峰鄙,釋放內(nèi)存空間,給之后的對(duì)象騰出寶貴的空間太雨。
不過(guò)因?yàn)檐浺玫牟淮_定性吟榴,在取出軟引用所引用的對(duì)象時(shí),要判斷一下它是否是null的囊扳。如果是null吩翻,需要嘗試重建它。
- 慎重使用靜態(tài)變量
被static修飾的變量锥咸,生命周期會(huì)與它所在的類保持一致狭瞎。在類不被卸載的情況下,那么該靜態(tài)變量本身也不會(huì)被銷毀搏予。
class BeanNews{
static Object obj=new Object();
}
上面這個(gè)例子中熊锭,Object對(duì)象會(huì)一直被靜態(tài)變量obj引用,它在堆中占據(jù)的內(nèi)存永遠(yuǎn)無(wú)法被回收缔刹,直到程序運(yùn)行結(jié)束球涛。
上面我們提到了Java的內(nèi)存分配區(qū)域中有一個(gè)方法區(qū),靜態(tài)變量和類的信息就存儲(chǔ)在這里校镐。
六亿扁、總結(jié)
到這里,對(duì)于Android內(nèi)存優(yōu)化中需要了解的理論基礎(chǔ)就簡(jiǎn)略的介紹完了鸟廓,掌握理論基礎(chǔ)从祝,一方面開(kāi)發(fā)中,我們可以避免犯一些低級(jí)錯(cuò)誤引谜,提高代碼的質(zhì)量牍陌,另一方面在日后做內(nèi)存優(yōu)化時(shí)會(huì)變得更加游刃有余,或許就不會(huì)產(chǎn)生“臥槽员咽,這樣為什么又會(huì)內(nèi)存泄漏”的疑問(wèn)了毒涧。下一篇會(huì)介紹Android內(nèi)存優(yōu)化中常見(jiàn)工具的使用「Android內(nèi)存優(yōu)化 2」-內(nèi)存優(yōu)化工具的使用,感謝您的閱讀贝室!