標籤:
概念
應用的開發離不開儲存,儲存分為網路、記憶體、SDCard檔案儲存體以及外部SDCard2檔案儲存體,開發中一定要注意好記憶體管理以免oom、卡頓等不好的使用者體驗,同時還要注意變數的回收,避免記憶體流失。下面呢先來瞭解一些基本的相關專業術語。
RAM(random access memory)隨機存取儲存空間即記憶體
寄存器(Registers):速度最快的儲存場所,因為寄存器位於處理器內部,我們在程式中無法控制
棧(Stack):存放基本類型的資料和對象的引用,但對象本身不存放在棧中,而是存放在堆中
堆(Heap):堆記憶體用來存放由new建立的對象和數組。在堆中分配的記憶體,由Java虛擬機器的自動記憶體回收行程(GC)來管理。
靜態域(static field): 靜態儲存地區就是指在固定的位置存放應用程式運行時一直存在的資料,Java在記憶體中專門劃分了一個靜態儲存地區來管理一些特殊的資料變數如靜態資料變數
常量池(constant pool):虛擬機器必須為每個被裝載的類型維護一個常量池。常量池就是該類型所用到常量的一個有序集和,包括直接常量(string,integer和floating point常量)和對其他類型,欄位和方法的符號引用。
非RAM儲存:硬碟等永久儲存空間
堆棧的特點對比
棧:當定義一個變數時,Java就在棧中為這個變數分配記憶體空間,當該變數退出該範圍後,Java會自動釋放掉為該變數所分配的記憶體空間,該記憶體空間可以立即被另作他用。
堆:當堆中的new產生數組和對象超出其範圍後,它們不會被釋放,只有在沒有引用變數指向它們的時候才變成垃圾,不能再被使用。即使這樣,所佔記憶體也不會立即釋放,而是等待被記憶體回收行程收走。這也是Java比較占記憶體的原因。
棧:存取速度比堆要快,僅次於寄存器。但缺點是,存在棧中的資料大小與生存期必須是確定的,缺乏靈活性。
堆:堆是一個運行時資料區,可以動態地分配記憶體大小,因此存取速度較慢。也正因為這個特點,堆的生存期不必事先告訴編譯器,而且Java的垃圾收集器會自動收走這些不再使用的資料。
棧:棧中的資料可以共用,它是由編譯器完成的,有利於節省空間的。
如果你對堆棧還不夠清晰明了,那麼請看下面一圖
記憶體流失也稱作“儲存滲漏”,用動態儲存裝置分配函數動態開闢的空間,在使用完畢後未釋放(堆棧開闢的儲存空間儲存的值),結果導致一直佔據該記憶體單元。直到程式結束。(其實說白了就是該記憶體空間使用完畢之後未回收)即所謂記憶體流失
記憶體分析
我們要怎麼知道記憶體發生泄漏呢,需要藉助記憶體分析工具MAT,或者使用開源項目LeakCanary,首先呢我們先到官網找到關於MAT的使用介紹,瞭解一個概要再來實踐吧。
首先我們需要安裝MAT,如果你是Eclipse開發還未轉Android Studio,那麼只需要安裝MA外掛程式即可,看(最新地址: http://download.eclipse.org/mat/1.5/update-site/)。
當然你如你用Android Studio ,但是不想用Eclipse來分析,那麼可以下載獨立的MAT,解壓後得到效果,雙擊運行開啟即可。
① Eclipse開發工具內建立一個測試的java程式,測試代碼如下
import java.util.ArrayList;import java.util.List;public class Main {/** * @param args */ public static void main(String[] args) { List<String> list = new ArrayList<String>(); while (1<2){ list.add("OutOfMemoryError soon"); } }}
運行就會爆記憶體流失問題
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.util.Arrays.copyOf(Unknown Source) at java.util.Arrays.copyOf(Unknown Source) at java.util.ArrayList.grow(Unknown Source) at java.util.ArrayList.ensureExplicitCapacity(Unknown Source) at java.util.ArrayList.ensureCapacityInternal(Unknown Source) at java.util.ArrayList.add(Unknown Source) at test.Main.main(Main.java:16)
② 產生xx.hprof檔案,利用MAT進行分析,這裡產生 xx.hprof檔案有兩種方案,eclipse 下的 java工程項目已run as >run config配置為例(另一種稍後提到)
添加VM arguments和配置輸出xx.hprof檔案的目錄,運行爆上述錯誤,開啟目錄即可看到xx.hprof檔案
當你滿心歡喜的以為即將要成功的時候,突然給你整這麼一處,這是腫麼回事呢?
原因是: android的虛擬機器匯出的記憶體檔案hprof檔案格式與標準的 java hprof檔案格式標準不一樣,根本原因兩者的虛擬機器不一致導致的。只需要使用SDK中內建的轉換工具轉換就可以了,hprof-conv 源檔案 目標檔案在尋找解決這個問題的途中,遇到一個大b坑,http://blog.csdn.net/pugongying1988/article/details/9122699該篇部落格告訴我在tools工具目錄下,我命令列怎麼都不行說找不到程式,我懷疑是不是我沒安裝外掛程式,重裝外掛程式後還是不行,幾經折騰在這裡找到它
亮瞎了這是tools目錄麼,頓時萬馬奔騰,直呼尼瑪!!迴歸正題,切換到該目錄下調用hprof-conv 命令重新輸出xx.hprof檔案(為了方便,把新舊的xx.hprof檔案都放在了改目錄下)
說好的不是版本問題(不是絕對的),經過幾次驗證,特麼就是eclipse匯出版本問題(run as config設定匯出的版本問題,具體原因沒深究),最後通過DDMS匯出的xx.hprof檔案就沒問題了(補充說明:原部落格確定了是不是版本問題這樣做是正確的,我們使用android studio開發,通過DDMS匯出那個真不是版本問題,通過上面命令就可以了)
- Histogram
列出了集合的對象執行個體,每種類型的執行個體集合的 shallow size 和 retained size . shallow size指的是對象所消耗的記憶體大小,如每個對象引起消耗4個位元組,或者8個位元組,取決於你的作業系統(32位,還是64位), retained size的概念依賴於Retained set 的概念,Retained set 指的是當對象X被回收時,所有被記憶體回收行程移除的對象集合, Retained size 即是Retained set所保持的記憶體大小。
具體操作如下,Overrview視圖下面進入
按照規則過濾後列表,接著跟蹤
再通過Path to GC Root(被JVM持有的對象,如當前啟動並執行線程對象,被systemclass loader載入的對象被稱為GC Roots, 從一個對象到GC Roots的引用鏈被稱為Path to GC Roots, 通過分析Path to GC Roots可以找出JAVA的記憶體泄露問題,當程式不在訪問該對象時仍存在到該對象的引用路徑。 )跟蹤變數沒被回收的具體位置
根據提供Demo的xx.hprof檔案跟蹤發現HomeActivity裡面調用了DrawableHelper類,內部mContext變數引起記憶體流失,這裡的mContext變數如下(由於內部其他多個方法設定屬性需要Context對象,本應該傳入一次放到構造方法裡面,private 不要static屬性就好,但是呢不同的Activity調用就會造成Context對象問題,所以最好傳入的Context對象為activity.getApplicationContext,亦或者其他今天屬性方法傳入參數,這樣DrawableHeper不用儲存Context執行個體引用,由此分析讓我明白了一點:不是什麼時候都適合用單例模式,要具體問題具體分析)
public class DrawableHelper { protected static DrawableHelper mDrawableHelper; protected static Context mContext; private DrawableHelper() { } public static DrawableHelper getInstance(Context context) { if (mDrawableHelper == null) { synchronized (DrawableHelper.class) { if (mDrawableHelper == null) { mDrawableHelper = new DrawableHelper(); } } } mContext = context; return mDrawableHelper; }
記憶體最佳化要素
① 重複字串是記憶體浪費的一個典型例子:多個字元數組具有相同的內容。字元數組的內容通常會給出如何減少重複的思想。
② 空集合空間不儲存任何資料。如果只有少數集合儲存資料,考慮延遲初始化,即只在需要時建立集合。
③集合通常是建立一個預設初始容量。許多低填充率的集合表明,初始容量可以減少。
④ 軟引用靜態資源
⑤ 使用 Andorid 架構中最佳化過的資料容器,例如 SparseArray,SparseBooleanArray 和 LongSparseArray。類似於 HashMap 這一類的容器的效率不是很高,因為在每個 Map 中對於每一次的存放資料,他都需要獨立一個單獨的 Entry 對象進行傳芳。而 SparseArray 由于禁止系統自動封裝索引值對,因此他更加有效率。並且你不需要擔心丟失掉原有資訊(AbsListView子類控制項紀錄checked屬性和position)
⑥避免依賴注入架構,使用類似於 Xutils的ViewUtils註解模組的依賴注射架構,或許會使你的代碼變得更加漂亮,因為他們能夠減少你需要寫的代碼,並且為測試或者在其他條件改變的情況下,提供一種自適應的環境。但是,這些架構在初始化的時候會因為注釋而消耗大量的工作在掃描你的代碼上,這會讓你的代碼在進行記憶體映射的時候花費更多的資源。雖然這些記憶體能夠被 Android 進行回收,但是等待整個分頁被釋放需要很長一段時間。
⑦使用混淆器ProGuard移除不必要的代碼。
⑧ 不要因為某個需求而使用大體積類庫,比如圓形頭像只需要一個圓形頭像即可沒必要使用開源的PhotoView庫,相對輕量級的CircleImageView跟適合或者自己自訂裁剪控制項。
⑨ 常量用static final修飾,(context不要用static修飾,容易造成記憶體流失)
⑩ 不用的變數,即使釋放NULL
第三方庫LeakCanary實踐
地址:https://github.com/square/leakcanary
項目依賴
dependencies { debugCompile ‘com.squareup.leakcanary:leakcanary-android:1.4-beta2‘ releaseCompile ‘com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta2‘ testCompile ‘com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta2‘ }
Application初始化庫
LeakCanary.install(this);
運行後如果程式出現記憶體流失,會有提示,找到Leaks表徵圖開啟可以查看泄露位置,分析原因,同時我們還可以通share heap dump,分享.hprof檔案到電腦,通過MAT進行詳盡的定位分析。(.hprof檔案比較大建議wifi下進行傳輸)
小結
東拼西湊還是完成了這篇blog,LeakCanary整合到項目協助我們分析泄露位置,如果還不能清晰的分析出具體原因,可以到處.hprof檔案通過MAT分析,最後針對個人做個簡短小結:
①以後開發一定要注意單例模式的運用了,太多的instance,並不是所有的類都需要。
② static 修飾常量都改為static final(個別不能final嘗試去掉static修飾)
③Context 、Activity都不要用static修飾
④ 記憶體流失:開闢的堆棧的儲存引用的管理,幹掉非存活狀態的堆棧引用,及時釋放。
參考資料
http://blog.csdn.net/a396901990/article/details/37914465
http://www.jianshu.com/p/c49f778e7acf
http://blog.csdn.net/pugongying1988/article/details/9122699
http://wiki.eclipse.org/MemoryAnalyzer#HPROF_dumps_from_Sun_Virtual_Machines
http://blog.csdn.net/xu_fu/article/details/45678373
http://blog.csdn.net/tiantangrenjian/article/details/39182293
推薦部落格
http://blog.csdn.net/a396901990/article/details/38904543
http://blog.csdn.net/a396901990/article/details/38707007
Android開發之記憶體管理