開發中內存泄漏的問題一直是比較棘手的,寫代碼只要稍不經心就會出現側漏,自己都不知道在哪側漏的,最後導致翻車。
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銷燬時沒有反註冊就會導致引用一直被持有。