一樣的,先來個思惟導圖預覽一下本文結構。java
1、運行時數據區域
首先來看看Java虛擬機所管理的內存包括哪些區域,就像咱們要了解一個房子,咱們得先知道這個房子大致構造。根據《Java虛擬機規範(Java SE 7 版)》的規定,請看下圖:算法
1.1 程序計數器
程序計數器是一塊較小的內存空間,它能夠看做是當前線程所執行的字節碼的行號指示器。編程
因爲 Java 虛擬機的多線程是經過線程輪流切換並分配處理器執行時間的方式來實現的,在任何一個肯定的時刻,一個處理器(對於多核處理器來講是一個內核)都只會執行一條線程中的指令。
爲了線程切換後能恢復到正確的執行位置,每條線程都須要有一個獨立的程序計數器,各條線程之間計數器互不影響,獨立存儲,咱們稱這類內存區域爲「線程私有」的內存。
此內存區域是惟一一個在 Java 虛擬機規範中沒有規定任何 OutOfMemoryError 狀況的區域。
1.2 Java 虛擬機棧
與程序計數器同樣,Java 虛擬機棧也是線程私有的,它的生命週期與線程相同。虛擬機棧描述的是 Java 方法執行的內存模型:每一個方法在執行的同時都會建立一個棧幀用於存儲局部變量表、操做數棧、動態連接、方法出口等信息。每個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。請看下圖:數組
有人把 Java 內存區分爲堆內存和棧內存,而所指的「棧」就是這裏的虛擬機棧,或者說是虛擬機棧中局部變量表部分。
局部變量表存放了編譯期可知的各類基本數據類型(boolean、byte、char、short、int、float、long、double)、對象引用和 returnAddress 類型(指向了一條字節碼指令的地址),其中64位長度的 long 和 double 類型的數據佔用2個局部變量空間,其他數據類型只佔用1個。
操做數棧也常被稱爲操做棧,它是一個後入先出棧。當一個方法剛剛執行的時候,這個方法的操做數棧是空的,在方法執行的過程當中,會有各類字節碼指向操做數棧中寫入和提取值,也就是入棧與出棧操做。
每一個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是爲了支持方法調用過程當中的動態鏈接。在Class文件的常量池中存有大量的符號引用,字節碼中的方法調用指令就以常量池中指向方法的符號引用爲參數。這些符號引用一部分會在類加載階段或第一次使用的時候轉化爲直接引用,這種轉化稱爲靜態解析。另一部分將在每一次的運行期期間轉化爲直接引用,這部分稱爲動態鏈接。
當一個方法執行完畢以後,要返回以前調用它的地方,所以在棧幀中必須保存一個方法返回地址。方法退出的過程實際上等同於把當前棧幀出棧,所以退出時可能執行的操做有:恢復上層方法的局部變量表和操做數棧,把返回值(若是有的話)壓入調用都棧幀的操做數棧中,調用PC計數器的值以指向方法調用指令後面的一條指令等。
虛擬機規範容許具體的虛擬機實現增長一些規範裏沒有描述的信息到棧幀中,例如與高度相關的信息,這部分信息徹底取決於具體的虛擬機實現。在實際開發中,通常會把動態鏈接,方法返回地址與其它附加信息所有歸爲一類,稱爲棧幀信息。
在 Java 虛擬機規範中,規定了兩種異常情況:若是線程請求的棧深度大於虛擬機所容許的深度,將拋出 StackOverflowError 異常;若是虛擬機棧能夠動態擴展,當擴展時沒法申請到足夠的內存,就會拋出 OutOfMemoryError 異常。
1.2.1 虛擬機棧溢出
若是線程請求的棧深度大於虛擬機所容許的最大深度,將拋出 StackOverflowError 異常。
若是虛擬機在擴展棧時沒法申請到足夠的內存空間,則拋出 OutOfMemoryError 異常。
當棧空間沒法繼續分配時,究竟是內存過小,仍是已使用的棧空間太大,其本質上只是對同一件事情的兩種描述而已。
系統分配給每一個進程的內存是有限制的,除去 Java 堆、方法區、程序計數器,若是虛擬機進程自己耗費的內存不計算在內,剩下內存就由虛擬機棧和本地方法棧「瓜分」了。每一個線程分配到的棧容量越大,能夠創建的線程數量天然就越少,創建線程時就越容易把剩下的內存耗盡。
出現 StackOverflowError 異常時有錯誤棧能夠閱讀,棧深度在大多數狀況下達到1000~2000徹底沒有問題,對於正常的方法調用(包括遞歸),這個深度應該徹底夠用了。
可是,若是是創建過多線程致使的內存溢出,在不能減小線程數或者更換 64 位虛擬機的狀況下,就只能經過減小最大堆和減小棧容量來換取更多的線程。
1.3 本地方法棧
本地方法棧與虛擬機棧所發揮的做用很是類似,它們之間的區別是虛擬機棧爲虛擬機執行 Java 方法服務,而本地方法棧則爲虛擬機棧使用到的 Native 方法服務。
與虛擬機棧同樣,本地方法棧區域也會拋出 StackOverflowError 和 OutOfMemoryError 異常。
1.4 Java 堆
Java 堆是被全部線程共享的一塊內存區域,在虛擬機啓動時建立。此內存區域的惟一目的就是存放對象實例,幾乎全部的對象實例都在這裏分配內存(可是,隨着技術發展,全部對象都分配在堆上也漸漸變得不是那麼「絕對」了)。請看下圖:緩存
對於大多數應用來講,Java 堆是 Java 虛擬機所管理的內存中最大的一塊。
Java 堆是垃圾收集器管理的主要區域,也被稱爲「GC堆」。
Java 堆能夠細分爲新生代、老年代、永久代;再細緻一點能夠分爲 Eden、From Survivor、To Survivor、Tenured、Permanent 。
Java 堆能夠處於物理上不連續的內存空間中,只要邏輯上是連續的便可,就像磁盤空間同樣。
從內存分配的角度來看,線程共享的 Java 堆中可能劃分出多個線程私有的分配緩衝區(TLAB)。
若是在堆中沒有內存完成實例分配,而且堆也沒法再擴展時,將會拋出 OutOfMemoryError 異常。
1.4.1 Java 堆溢出
Java 堆用於存儲對象實例,只要不斷地建立對象,而且保證 GC Roots 到對象之間有可達路徑來避免垃圾回收機制清除這些對象,那麼在對象數量到達最大堆的容量限制後就會產生內存溢出異常。
Java 堆內存的 OOM 異常是實際應用中常見的內存溢出異常狀況。當出現 Java 堆內存溢出時,異常堆棧信息 「java.lang.OutOfMemoryError」 會跟着進一步提示 「Java heap space」 。
一般是先經過內存映像分析工具對 Dump 出來的堆轉儲快照進行分析,重點是確認內存中的對象是不是必要的,也就是要先分清楚究竟是出現了內存泄漏仍是內存溢出。
若是是內存泄漏,可進一步經過工具查看泄露對象到 GC Roots 的引用鏈。因而就能找到泄露對象的類型信息及 GC Roots 引用鏈的信息,就能夠比較準確地定位出泄露代碼的位置。
若是不存在泄露,就是內存中的對象確實都還必須存活着,那就應當檢查虛擬機的堆參數(-Xmx 與 -Xms),與機器物理內存對比看是否還能夠調大,從代碼上檢查是否存在某些對象生命週期過長、持有狀態時間過長的狀況,嘗試減小程序運行期的內存消耗。
1.5 方法區
方法區與 Java 堆同樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。安全
Java 虛擬機規範對方法區的限制很是寬鬆,除了和 Java 堆同樣不須要連續的內存和能夠選擇固定大小或者可擴展外,還能夠選擇不實現垃圾收集。
這區域的內存回收目標主要是針對常量池的回收和對類型的卸載。
當方法區沒法知足內存分配需求時,將拋出 OutOfMemoryError 異常。
1.5.1 運行時常量池
運行時常量池是方法區的一部分。
常量池用於存放編譯期生成的各類字面量和符號引用,這部份內容將在類加載後進入方法區的運行時常量池中存放。
運行時常量池相對於 Class 文件常量池的一個重要特徵是具有動態性,Java 語言並不要求常量必定只有編譯期才能產生,也就是並不是預置入 Class 文件中常量池的內容才能進入方法區運行時常量池,運行期間也可能將新的常量放入池中,這種特性被開發人員利用得比較多的即是 String 類的 intern() 方法。
當常量池沒法再申請到內存時會拋出 OutOfMemoryError 異常。在 OutOfMemoryError 後面跟隨的提示信息時 「PermGen space」 。
1.6 直接內存
直接內存並非虛擬機運行時數據區的一部分,也不是 Java 虛擬機規範中定義的內存區域。可是這部份內存也被頻繁地使用,並且也可能致使 OutOfMemoryError 異常出現。
NIO 類,一種基於通道與緩衝區的 I/O 方式,它可使用 Native 函數庫直接分配堆外內存,而後經過一個存儲在 Java 堆中的 DirectByteBuffer 對象做爲這塊內存的引用進行操做。這樣能在一些場景中顯著提升性能,由於避免了在 Java 堆和 Native 堆中來回複製數據。
本機直接內存的分配不會受到 Java 堆大小的限制,可是,既然是內存,確定仍是會受到本機總內存(包括 RAM 以及 SWAP 區或者分頁文件)大小以及處理器尋址空間的限制。
由 DirectMemory 致使的內存溢出,一個明顯的特徵是在 Heap Dump 文件中不會看見明顯的異常,若是咱們發現 OOM 以後 Dump 文件很小,而程序中有直接或間接使用了 NIO ,那就能夠考慮檢查一下是否是這方面的緣由。
2、內存分配策略
對象的內存分配,往大方向講,就是在堆上分配(但也可能通過 JIT 編譯後被拆散爲標量類型並間接地棧上分配),對象主要分配在新生代的 Eden 區上,若是啓動了本地線程分配緩衝,將按線程優先在 TLAB 上分配。少數狀況下也可能會直接分配在老年代中,分配的規則並非固定的,其細節取決於當前使用的是哪種垃圾收集器組合,還有虛擬機中與內存相關的參數的設置。性能優化
2.1 對象優先在 Eden 分配
大多數狀況下,對象在新生代 Eden 區中分配。當 Eden 區沒有足夠的空間進行分配時,虛擬機將發起一次 Minor GC 。舉個例子,看下面的代碼:bash
private static final int _1MB = 1024 * 1024;
/**
* VM 參數:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
*/
private static void testAllocation () {
byte[] allocation1, allocation2, allocation3, allocation4;
allocation1 = new byte[2 * _1MB];
allocation2 = new byte[2 * _1MB];
allocation3 = new byte[2 * _1MB];
allocation4 = new byte[4 * _1MB];//出現一次 Minor GC
}
複製代碼
執行上面的testAllocation() 代碼,當分配 allocation4 對象的語句時會發生一次 Minor GC ,此次 GC 的結果是新生代 6651KB 變爲 148KB ,而總內存佔用量則幾乎沒有減小(由於 allocation一、allocation二、allocation3 三個對象都是存活的,虛擬機幾乎沒有找到可回收的對象)。此次 GC 發生的緣由是給 allocation4 分配內存時,發現 Eden 已經被佔用了 6MB ,剩餘空間已不足以分配 allocation4 所需的 4MB 內存,所以發生 Minor GC 。GC 期間虛擬機又發現已有的 3 個 2MB 大小的對象所有沒法放入 Survivor 空間(從上圖中可看出 Survivor 空間只有 1MB 大小),因此只好經過分配擔保機制提早轉移到老年代去。markdown
2.2 大對象直接進入老年代
所謂的對象是指,須要大量連續內存空間的 Java 對象,最典型的大對象就是那種很長的字符串以及數組。常常出現大對象容易致使內存還有很多空間時就提早觸發垃圾收集以獲取足夠的連續空間來「安置」它們。
虛擬機提供了一個 -XX:PretenureSizeThreshold 參數,令大於這個設置值的對象直接在老年代分配。這樣作的目的是避免在 Eden 區及兩個 Survivor 區之間發生大量的內存複製(新生代採用複製算法收集內存)。
2.3 長期存活的對象將進入老年代
既然虛擬機採用了分代收集的思想來管理內存,那麼內存回收時就必須能識別到哪些對象應放在新生代,哪些對象應放在老年代中。爲了作到這點,虛擬機給每一個對象定義了一個對象年齡計數器。若是對象在 Eden 出生並通過第一次 Minor GC 後仍然存活,而且能被 Survivor 容納的話,將被移動到 Survivor 空間中,而且對象年齡設爲 1 。對象在 Survivor 區中每「熬過」一次 Minor GC,年齡就增長1歲,當它的年齡增長到必定程度(默認15歲),就會被晉升到老年代中。對象晉升老年代的年齡閾值,能夠經過參數 -XX:MaxTenuringThreshold 設置。數據結構
2.4 動態對象年齡斷定
爲了能更好地適應不一樣程序的內存情況,虛擬機並非永遠地要求對象的年齡必須達到了 MaxTenuringThreshold 才能晉升老年代,若是在 Survivor 空間中相同年齡全部對象大小的總和大於 Survivor 空間的一半,年齡大於或等於該年齡的對象就能夠直接進入老年代,無須等到 MaxTenuringThreshold 中的要求的年齡。
2.5 空間分配擔保機制
在發生 Minor GC 以前,虛擬機會先檢查老年代最大可用的連續空間是否大於新生代全部對象總空間,若是這個條件成立,那麼 Minor GC 能夠確保是安全的。若是不成立,則虛擬機會查看 HandlePromotionFailure 設置值是否容許擔保失敗。若是容許,那麼會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小,若是大於,將嘗試着進行一次 Minor GC ,儘管此次 Minor GC 是有風險的;若是小於,或者 HandlePromotionFailure 設置不容許冒險,那此次也要改成進行一次 Full GC。
上面提到的「冒險」指的是,因爲新生代使用複製收集算法,但爲了內存利用率,只使用其中一個 Survivor 空間來做爲輪換備份,所以當出現大量對象在 Minor GC 後仍然存活的狀況,把 Survivor 沒法容納的對象直接進入老年代。老年代要進行這樣的擔保,前提是老年代自己還有容納這些對象的剩餘空間,一共有多少對象會活下來在實際完成內存回收以前是沒法明確知道的,因此只好取以前每一次回收晉升到老年代對象容量的平均大小值做爲經驗值,與老年代的剩餘空間進行比較,決定是否進行 Full GC 來讓老年代騰出更多空間。
取平均值進行比較其實仍然是一種動態機率的手段,也就是說,若是某次 Minor GC 存活後的對象突增,遠遠高於平均值的話,依然會致使擔保失敗。
若是出現了HandlePromotionFailure 失敗,那就只好在失敗後從新發起一次 Full GC。雖然擔保失敗時繞的圈子是最大的,但大部分狀況下都仍是會將 HandlePromotionFailure 開關打開,避免 Full GC 過於頻繁。
但在 JDK 6 Update 24 以後,HandlePromotionFailure 參數不會再影響到虛擬機的控件分配擔保策略,只要老年代的連續空間大於新生代對象總大小或者歷次晉升的平均大小就會進行 Minor GC ,不然將進行 Full GC。
3、內存回收策略
新生代 GC(Minor GC) :指發生在新生代的垃圾收集動做,由於 Java 對象大多都具有朝生夕滅的特性,因此 Minor GC 很是頻繁,通常回收速度也比較快。
老年代 GC(Major GC / Full GC):值發生在老年代的 GC,出現了 Major GC,常常會伴隨至少一次的 Minor GC(但非絕對)。Major GC 的速度通常會比 Minor GC 慢 10 倍以上。
3.1 內存回收關注的區域
上面已經介紹 Java 內存運行時區域的各個部分,其中程序計數器、虛擬機棧、本地方法棧3個區域隨線程而生,隨線程而滅。
棧中的棧幀隨着方法的進入和退出而有條不紊地執行者出棧和入棧操做。每個棧幀中分配多少內存基本上是在類結構肯定下來時就已知的。
所以這幾個區域的內存分配和回收都具有肯定性,在這幾個區域內就不須要過多考慮回收的問題,由於方法結束或者線程結束時,內存天然就跟隨着回收了。
而 Java 堆和方法區則不同,一個接口中的多個實現類須要的內存可能不同,一個方法中的多個分支須要的內存也可能不同,咱們只有在程序處於運行期間時才能知道會建立哪些對象,這部份內存的分配和回收都是動態的,垃圾收集器所關注的是這部份內存。
3.2 對象存活判斷
3.2.1 引用計數算法
給對象添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任什麼時候刻計數器爲 0 的對象就是不可能再被使用的。
這種算法的實現簡單,斷定效率也很高,在大部分狀況下它都是一個不錯的算法,但它很難解決對象之間相互循環引用的問題。
舉個例子,對象 objA 和 objB 都有字段 instance,賦值令 objA.instance = objB 及 objB.instance = objA ,除此以外,這兩個對象再無任何引用,實際上,這兩個對象已經不可能再被訪問,可是它們由於相互引用着對方,致使它們的引用計數都不爲 0,因而引用計數算法沒法通知 GC 收集器回收它們。
3.2.2 可達性分析算法
這個算法的基本思路就是經過一系列額稱爲「GC Roots」 的對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈,當一個對象到 GC Roots 沒有任何引用鏈相連或者說這個對象不可達時,則證實此對象是不可用的。
在 Java 語言中,可做爲 GC Roots 的對象包括如下:
虛擬機棧(棧幀中的本地變量表)中引用的對象
方法區中類靜態屬性引用的對象
方法區中常量引用的對象
本地方法棧中 JNI 引用的對象
請看下圖:
3.3 方法區的回收
方法區(HotSpot 虛擬機中的永久代)的垃圾收集主要回收兩部份內容:廢棄常量和無用的類。回收廢棄常量與回收 Java 堆的對象很是相似。
斷定一個類是不是「無用的類」須要同時知足下面3個條件:
該類的全部的實例都已經被回收,也就是 Java 堆中不存在該類的任何實例。
加載該類的 ClassLoader 已經被回收。
該類對應的 java.lang.Class 對象沒有在任何地方被引用,沒法在任何地方經過反射訪問該類的方法。
虛擬機能夠對知足上述3個條件的無用類進行回收,這裏說的僅僅是「能夠」,而並非和對象同樣,不使用了就必然回收。
3.4 垃圾收集算法
3.4.1 標記—清除算法
算法分爲 「標記」 和 「清除」 兩個階段:首先標記出全部須要回收的對象,在標記完成後統一回收全部被標記的對象。
它主要有兩個不足的地方:一個是效率問題,標記和清除兩個過程的效率都不高;另外一個是空間問題,標記清除以後會產生大量不連續的內存碎片,空間碎片太多可能會致使之後在程序運行過程當中須要分配較大對象時,沒法找到足夠的連續內存而得不到提早觸發另外一次垃圾收集動做。
這是最基礎的收集算法,後續的收集算法都是基於這種思路並對其不足進行改進而獲得的。
3.4.2 複製算法
爲了解決效率問題,「複製」算法應運而生,它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中一塊。
當這一塊的內存用完了,就將還存活着的對象複製到另一塊上面,而後再把已使用過的內存空間一次清理掉。
這樣使得每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等複雜狀況,只要移動堆頂指針,按順序分配內存便可,實現簡單,運行高效。
不足之處是,將內存縮小爲原來的一半,代價過高。
舉個優化例子:新生代中的對象98%是「朝生夕死」的,因此並不須要按照 1:1 的比例來劃份內存空間,而是將內存分爲一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden 和其中一塊 Survivor。當回收時,將 Eden 和 Survivor 中還存活着的對象一次性地複製到另外一塊 Survivor 空間上,最後清理掉 Eden 和剛纔用過的 Survivor 空間。
再舉個優化例子:將 Eden 和 Survivor 的大小比例設爲 8:1 ,也就是每次新生代中可用內存空間爲整個新生代容器的 90%,只有10% 的內存做爲保留區域。固然 98% 的對象可回收只是通常場景下的數據,咱們沒有辦法保證每次回收都只有很少於 10% 的對象存活,當 Survivor 空間不夠用時,須要依賴其餘內存(這裏指老年代)進行分配擔保(空間分配擔保機制在上面,瞭解一下)。
3.4.3 標記—整理算法
複製收集算法在對象存活率較高時就要進行較多的複製操做,效率將會變低。因此在老年代通常不能直接選用複製收集算法。
根據老年代的特色,「標記—整理」 算法應運而生。
標記過程仍然與 「標記—清除」 算法同樣,但後續步驟不是直接對可回收對象進行清理,而是讓全部存活的對象都向一端移動,而後直接清理掉端邊界之外的內存。
3.4.4 分代收集算法
根據對象存活週期的不一樣將內存劃分爲幾塊,通常是把 Java 堆分爲新生代和老年代,這樣就能夠根據各個年代的特色採用最適當的收集算法。
在新生代中,每次垃圾收集時都發現有大批對象死去,只有少許存活,那就選用複製算法,只須要付出少許存活對象的複製成本就能夠完成收集。
而老年代中由於對象存活率高、沒有額外空間對它進行分配擔保,就必須使用 「標記—清除」 或者 「標記—整理」 算法來進行回收。
當前商業虛擬機的垃圾收集都採用 「分代收集」 算法。
4、編程中的內存優化
相信你們在編程中都會注意到內存使用的問題,下面我就簡單列一下在實際操做當中須要注意的地方。
4.1 減少對象的內存佔用
咱們能夠考慮使用 ArrayMap / SparseArray 而不是 HashMap 等傳統數據結構。(我在老項目中,根據 Lint 提示,將 HashMap 替換成 ArrayMap / SparseArray 以後,在 Android Profiler 中顯示運行時內存比以前直接少了幾M,仍是挺可觀的。)
避免使用 Enum
減少 Bitmap 對象的內存佔用
inSampleSize :縮放比例,在把圖片載入內存以前,咱們須要先計算出一個合適的縮放比例,避免沒必要要的大圖載入。
decode format:解碼格式,選擇 ARGB_8888 / RBG_565 / ARGB_4444 / ALPHA_8,存在很大差別。
使用更小的圖片:儘可能使用更小的圖片不只僅能夠減小內存的使用,還能夠避免出現大量的 InflationException。
4.2 內存對象的重複利用
複用系統自帶的資源:Android系統自己內置了不少的資源,例如字符串/顏色/圖片/動畫/樣式以及簡單佈局等等,這些資源均可以在應用程序中直接引用。
注意在 ListView / GridView 等出現大量重複子組件的視圖裏面對 ConvertView 的複用
Bitmap 對象的複用
避免在 onDraw 方法裏面執行對象的建立:相似 onDraw() 等頻繁調用的方法,必定須要注意避免在這裏作建立對象的操做,由於他會迅速增長內存的使用,並且很容易引發頻繁的 GC,甚至是內存抖動。
StringBuilder:在有些時候,代碼中會須要使用到大量的字符串拼接的操做,這種時候有必要考慮使用 StringBuilder 來替代頻繁的 「+」 。
4.3 避免對象的內存泄露
內部類引用致使 Activity 的泄漏
Activity Context 被傳遞到其餘實例中,這可能致使自身被引用而發生泄漏。
考慮使用 Application Context 而不是 Activity Context :對於大部分非必須使用 Activity Context 的狀況(Dialog 的 Context 就必須是 Activity Context),咱們均可以考慮使用 Application Context 而不是 Activity 的 Context,這樣能夠避免不經意的 Activity 泄露。
注意臨時 Bitmap 對象的及時回收:例如臨時建立的某個相對比較大的 bitmap 對象,在通過變換獲得新的 bitmap 對象以後,應該儘快回收原始的 bitmap,這樣可以更快釋放原始 bitmap 所佔用的空間。
注意監聽器的註銷:在 Android 程序裏面存在不少須要 register 與 unregister 的監聽器,咱們須要確保在合適的時候及時 unregister 那些監聽器。本身手動 add 的 listener,須要記得及時 remove 這個 listener。
注意緩存容器中的對象泄漏:咱們爲了提升對象的複用性把某些對象放到緩存容器中,但是若是這些對象沒有及時從容器中清除,也是有可能致使內存泄漏的。
注意 WebView 的泄漏:一般根治這個問題的辦法是爲 WebView 開啓另一個進程,經過 AIDL 與主進程進行通訊,WebView 所在的進程能夠根據業務的須要選擇合適的時機進行銷燬,從而達到內存的完整釋放。
注意 Cursor 對象是否及時關閉
4.4 內存使用策略優化
資源文件須要選擇合適的文件夾進行存放
Try catch 某些大內存分配的操做:在某些狀況下,咱們須要事先評估那些可能發生 OOM 的代碼,對於這些可能發生 OOM 的代碼,加入 catch 機制,能夠考慮在 catch 裏面嘗試一次降級的內存分配操做。例如 decode bitmap 的時候,catch 到 OOM,能夠嘗試把採樣比例再增長一倍以後,再次嘗試 decode。
謹慎使用 static 對象:由於static的生命週期過長,和應用的進程保持一致,使用不當極可能致使對象泄漏。
特別留意單例對象中不合理的持有:由於單例的生命週期和應用保持一致,使用不合理很容易出現持有對象的泄漏。
珍惜Services資源:建議使用 IntentService
優化佈局層次,減小內存消耗:越扁平化的視圖佈局,佔用的內存就越少,效率越高。咱們須要儘可能保證佈局足夠扁平化,當使用系統提供的 View 沒法實現足夠扁平的時候考慮使用自定義 View 來達到目的。
謹慎使用 「抽象」 編程
使用 nano protobufs 序列化數據
謹慎使用依賴注入框架
謹慎使用多進程
使用 ProGuard 來剔除不須要的代碼
謹慎使用第三方 libraries
考慮不一樣的實現方式來優化內存佔用
5、內存檢測工具
最後給推薦幾個內存檢測的工具,具體使用方法,能夠自行搜索。固然除了下面這些工具,應該還有更多更好用的工具,只是我尚未發現,若有建議,能夠在文章下面評論留言,你們一塊兒學習分享一下。
Systrace
Traceview
Android Studio 3.0 的 Android Profiler 分析器
LeakCanary
後續
學習資料
《深刻理解Java虛擬機:JVM高級特性與最佳實踐》
Android性能優化典範