開發中記憶體洩漏的問題一直是比較棘手的,寫代碼隻要稍不經心就會出現側漏,自己都不知道在哪側漏的,最後導緻翻車。
app做完了,一經過大量測試,不知不覺就崩潰,一看日志-----OOM(噗~~)。
最近看了一些部落格和書籍還有視訊,簡單總結了一下,側漏的發生和原因。
下面我先舉個前些天看視訊的小栗子,然後下面再貼出一些概念。
下面的方法可以粗略的檢查出activity是否有側漏。
我建立了一個小工程,裡面就兩個activity和一個utils類,通過兩個界面間的跳轉看看出了什麼問題。
首先運作項目,打開下面工具欄中的Android Monitor:
可以看到項目運作起來了,穩定的記憶體8.32MB。
此時我們手機上按下回車回到手機的系統中,即我們的程式進入背景了,這時按下下圖中選項的按鈕。
Android studio中Android Monitor --> System Infomation --> MemoryUsage
點完之後稍等,應該會出現下圖中的檔案:
仔細看檔案裡下面這部分:
Views是0,activitys也是0。
好!此時我們手機回到應用,跳轉activity,在傳回,重複幾次之後,我們再通過上面的操作 MemoryUsage得到上面的新檔案:
發現有好多view。
而且切換之後現在的記憶體也增加了。剛開始的8.32MB,現在已經到了9.21MB:
我們點選GC按鈕:
記憶體回到了8.75MB,為什麼沒有回到最開始的?
上面我們用MemoryUsage看的是有多少View或者Actvity存活,下面我們看看更詳細的定位:
GC之後點選下圖中的按鈕:
就是GC旁邊的按鈕,點選之後等會就會出現下圖的檔案,沒出現也不要緊在右側Captures選項中的Heap Snapshot目錄下:
按照上圖切換到你自己包名目錄下,我找到了我的兩個activity。
上圖中清單項對應的意義:
Total Count --> 記憶體中該類的對象個數
Heap Count --> 堆記憶體中該類的對象個數
Sizeof --> 實體大小
Shallow size --> 該對象本身占記憶體大小
Retained Size --> 釋放該對象後,節省記憶體大小
手機上我已經回到了第一個activity了,第二個activity已經關閉了。但是從圖中可以看出我的Main2Activity還存活着。
第一行藍色代碼,context被CommUtils持有着。
先看看Main2Activity中都幹了什麼:
public class Main2Activity extends AppCompatActivity {
private CommUtils commUtils;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);
commUtils = CommUtils.getInstance(this);
}
}
初始化了一個CommUtils對象。
比較幸運這麼快就能找到,現在我們可以去看看Commutils都幹了什麼事情:
/**
* Created by ge on 2017/9/26.
*/
public class CommUtils {
private static CommUtils instance;
private Context context;
private CommUtils(Context context){
this.context = context;
}
public static CommUtils getInstance(Context context){
if (instance == null){
instance = new CommUtils(context);
}
return instance;
}
}
一個單例,相信大家一眼就可以知道為什麼了,getInstance的時候context我傳入的是Activity的context,當我們要銷毀Main2Activity的時候,CommUtils一直持有着Activity的執行個體。GC的時候,不能被銷毀,是以這就造成了記憶體的洩漏。
并且,當我們重新打開界面時,context還是上次建立的那個,如果我們在activity中有使用commUtils執行個體的地方,那麼就會出錯了。
細思極恐。。。。
我們下面将getInstance代碼稍微修改一下:
instance = new CommUtils(context.getApplicationContext());
這樣是不是就好了。
上面隻是一個簡單的栗子,簡單的定位記憶體洩漏的方法。
寫的挺長的,主要是截圖多。。。。
關于記憶體洩漏,發生的原因有很多,下面說一下一些概念,有助于了解記憶體洩漏,都是我平時看文章記下來的。
Java的四種引用:
1.強引用
強引用的對象,java甯願oom也不會回收他。
2.軟引用
比強引用弱一點的引用,在java gc的時候,如果軟引用所引用的對象被回收,首次gc失敗的話會繼而回收軟引用的對象。
軟引用适合做緩存處理,可以和引用隊列(ReferenceQueue)一起使用,當對象被回收之後儲存它的軟引用會放入引用隊列。
3.弱引用
比軟引用更弱的引用,當java執行gc的時候,如果弱引用的對象被回收,無論它有沒有用都會回收掉弱引用的對象,不過gc是一個比較低優先級的線程,不會那麼及時的回收你的對象。可以和引用隊列一起使用,當對象被回收之後儲存它的弱引用會放入引用隊列。
4.虛引用
虛引用和沒有引用是一樣的,他必須和引用隊列一起使用,當java回收一個對象的時候,如果發現它有虛引用,會在回收對象之前将他的虛引用加入到與之關聯的引用隊列中。
可以通過這個特性在一個對象被回收之前采取措施。
Java GC
目前oracle jdk和open jdk的虛拟機都是Hotsport。
android為Dalvik和Art。
曾經的GC算法:引用計數
簡單說引用計數就是對每一個對象的引用計算數字,如果引用就+1,不引用就-1,回收掉引用計數為0的對象。來達到垃圾回收。
弊端:
如果兩個對象都應該被回收,但是他倆卻互相依賴,那麼他倆的引用永遠都不會為0,那麼永遠無法回收,卻無法解決循環引用的問題。
現代GC的算法:
1.标記回收算法(Mark and Sweep GC)
從GC Root集合開始,将記憶體整個周遊一次,保留所有可能被GC Roots直接或間接引用到的對象,
剩下的對象都被當作垃圾對待并回收,這個算法需要中斷程序内其它元件的執行并且可能産生記憶體碎片。
2.複制算法(Copying)
将現有記憶體分為兩塊,每次隻使用其中一塊,在垃圾回收時将正在使用的記憶體中的存活對象複制到違背使用的記憶體塊中,
之後,清除正在使用的記憶體塊中的所有的對象,交換兩個記憶體角色,完成垃圾回收。
3.标記壓縮算法(Mark Compact)
先從根結點開始對所有可達對象做一次标記,但之後,它并不簡單的清理未标記的對象,而是将所有的存活對象壓縮到記憶體的一端。
之後清理邊界外所有的空間。這種方法避免了碎片的産生,又不需要兩塊相同的記憶體空間,是以其成本效益較高。
4.分代
将所有建立對象都放入稱為年輕代的記憶體區域,年輕代的特點是對象會很快回收,是以在年輕代就選擇效率較高的複制算法。
當一個對象經過幾次回收後依然存活,對象就會被放入稱為老年代的記憶體空間。對于新生代适用于複制算法,而對于老年代則采取
标記---壓縮算法。
并發GC和非并發GC
非并發GC:
虛拟機在執行GC的時候進行Stop the world,也就是挂起其它所有的線程,通常會持續上百毫秒,一次mark,然後直接清理。
初始化 --> stop the world --> Mark --> 回收 --> 執行GC結束操作
并發GC:
跟非并發的簡單GC來比較,一般非并發GC需要耗費上百ms的時間來進行,而并發的GC隻需要10ms左右,效率大幅提升。
但并發GC由于需進行重複的處理改動的對象,是以需要更多的cpu資源。
平時可能會造成記憶體洩漏的地方:
1.非靜态的内部類匿名類會隐式的持有外部類的引用。
修改思路:
将Handler和Runnable改成static
在外部定義,内部使用。
2.靜态變量:使用靜态變量來引用一個事物,在不使用之後沒有下掉,那麼引用存在就會一直洩漏。
3.單例: 使用單例中儲存了不應該被一直持有的對象。
4.由第三方庫使用不當:例EventBus,activity銷毀時沒有反注冊就會導緻引用一直被持有。