「全面理解Android內(nèi)存優(yōu)化 1」-Android的內(nèi)存機(jī)制與管理建議

前言

本篇文章是《全面理解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ū)域:

  1. 方法區(qū):存儲(chǔ)每個(gè)類的信息(包括類的名稱、方法信息燥翅、字段信息)骑篙、靜態(tài)變量、常量以及編譯器編譯后的代碼等森书。這是所有線程都共享的區(qū)域靶端。

  2. 虛擬機(jī)棧:用來(lái)存儲(chǔ)方法中的局部變量(包括在方法中聲明的非靜態(tài)變量以及函數(shù)形參)。對(duì)于基本數(shù)據(jù)類型的變量凛膏,則直接存儲(chǔ)它的值杨名,對(duì)于引用類型的變量,則存的是指向?qū)ο蟮囊貌痢>植孔兞勘淼拇笮≡诰幾g器就可以確定其大小了台谍,因此在程序執(zhí)行期間局部變量表的大小是不會(huì)改變的。每個(gè)線程都會(huì)有一個(gè)自己的棧吁断。

  3. 本地方法棧:存儲(chǔ)native方法的棧

  4. :內(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è)堆。

  5. 程序計(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ū)域)瞎领。

  1. 靜態(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ǔ)空間里逃魄。

  2. 常量池方法區(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)化工具的使用,感謝您的閱讀贝室!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末契讲,一起剝皮案震驚了整個(gè)濱河市仿吞,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌捡偏,老刑警劉巖唤冈,帶你破解...
    沈念sama閱讀 211,948評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異银伟,居然都是意外死亡你虹,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,371評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén)彤避,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)傅物,“玉大人,你說(shuō)我怎么就攤上這事琉预⌒铮” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,490評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵模孩,是天一觀的道長(zhǎng)尖阔。 經(jīng)常有香客問(wèn)我,道長(zhǎng)榨咐,這世上最難降的妖魔是什么介却? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,521評(píng)論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮块茁,結(jié)果婚禮上齿坷,老公的妹妹穿的比我還像新娘。我一直安慰自己数焊,他們只是感情好永淌,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,627評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著佩耳,像睡著了一般遂蛀。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上干厚,一...
    開(kāi)封第一講書(shū)人閱讀 49,842評(píng)論 1 290
  • 那天李滴,我揣著相機(jī)與錄音,去河邊找鬼蛮瞄。 笑死所坯,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的挂捅。 我是一名探鬼主播芹助,決...
    沈念sama閱讀 38,997評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了状土?” 一聲冷哼從身側(cè)響起苗缩,我...
    開(kāi)封第一講書(shū)人閱讀 37,741評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎声诸,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體退盯,經(jīng)...
    沈念sama閱讀 44,203評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡彼乌,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,534評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了渊迁。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片慰照。...
    茶點(diǎn)故事閱讀 38,673評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖琉朽,靈堂內(nèi)的尸體忽然破棺而出毒租,到底是詐尸還是另有隱情,我是刑警寧澤箱叁,帶...
    沈念sama閱讀 34,339評(píng)論 4 330
  • 正文 年R本政府宣布墅垮,位于F島的核電站,受9級(jí)特大地震影響耕漱,放射性物質(zhì)發(fā)生泄漏算色。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,955評(píng)論 3 313
  • 文/蒙蒙 一螟够、第九天 我趴在偏房一處隱蔽的房頂上張望灾梦。 院中可真熱鬧,春花似錦妓笙、人聲如沸若河。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,770評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)萧福。三九已至,卻和暖如春辈赋,著一層夾襖步出監(jiān)牢的瞬間统锤,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,000評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工炭庙, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留饲窿,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,394評(píng)論 2 360
  • 正文 我出身青樓焕蹄,卻偏偏與公主長(zhǎng)得像逾雄,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,562評(píng)論 2 349