日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網(wǎng)為廣大站長提供免費收錄網(wǎng)站服務(wù),提交前請做好本站友鏈:【 網(wǎng)站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(wù)(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網(wǎng)站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

前言

內(nèi)存泄漏可以說是Android/ target=_blank class=infotextkey>安卓開發(fā)中常遇到的問題,追溯和排查其問題根源是進階的程序猿必須具備的一項技能。小盆友今天便與大家分享一下這方面的一些見解,如有理解錯誤或是不同見解,可以于評論區(qū)留言我們進行討論,如果喜歡給個贊鼓勵下吧。

1、JAVA內(nèi)存解析

要想知道內(nèi)存泄漏,需要先了解java中運行時內(nèi)存是怎么構(gòu)成的,才能知道是哪個地方導(dǎo)致。話不多說,先上圖

 

一篇文章教你搞定內(nèi)存泄漏與排查流程——安卓性能優(yōu)化

 

運行時的java內(nèi)存分為兩大塊:線程私有(藍色區(qū)域)、共享數(shù)據(jù)區(qū)(黃色區(qū)域)

線程私有:主要用于存儲各個線程私有的一些信息,包括:程序計數(shù)器、虛擬機棧、本地方法棧

共享數(shù)據(jù)區(qū):主要用于存儲公用的一些信息,包括:方法區(qū)(內(nèi)含常量池)、堆

  1. 程序計數(shù)器:讓程序中各個線程知道自己接下來需要執(zhí)行哪一行。在java中多線程為搶占式(因為cpu在某一時刻只會執(zhí)行一條線程),當線程切換時,需要繼續(xù)哪一行便由程序計數(shù)器告知。
  2. 舉個例子:A、B兩條線程,此時CPU執(zhí)行從A切換至B,過了段時間從B切換回A,此時A需要從上次暫停的地方繼續(xù)執(zhí)行,此時從哪一行執(zhí)行就是由程序計數(shù)器來提供。
  3. 值得一提
  4. (1)若執(zhí)行java函數(shù)時,程序計數(shù)器記錄的是虛擬機字節(jié)碼的地址;
  5. (2)若執(zhí)行native方法時,程序計數(shù)器便置為了null。
  6. (3)在java虛擬機規(guī)范中,程序計數(shù)器是唯一沒有定義OutOfMemoryError。
  7. 虛擬機棧:描述的是java方法的內(nèi)存模型,平時說的“棧”其實就是虛擬機棧,其生命周期與線程相同。每個方法(不包含native方法)執(zhí)行的同時都會創(chuàng)建一個棧幀用于存儲局部變量表、操作數(shù)棧、動態(tài)鏈接、方法出口等信息。
  8. 值得一提:在java虛擬機規(guī)范中,此處定義了兩個異常
  9. (1)StackOverFlowError (在遞歸中常看到,遞歸層級過深)
  10. (2)OutOfMemoryError
  11. 本地方法棧:是為虛擬機使用到的Native方法提供內(nèi)存空間。 有些虛擬機的實現(xiàn)直接把本地方法棧和虛擬機棧合二為一,比如主流的HotSpot虛擬機。
  12. 值得一提:在java虛擬機規(guī)范中,此處定義了兩個異常
  13. (1)StackOverFlowError (在遞歸中常看到,遞歸層級過深)
  14. (2)OutOfMemoryError
  15. 方法區(qū):主要存儲已加載是類信息(由ClassLoader加載)、常量、靜態(tài)變量、編譯后的代碼的一些信息。 GC在這里比較少出現(xiàn)在這塊區(qū)域。
  16. 堆:存放的是幾乎所有的對象實例和數(shù)組數(shù)據(jù)。 是虛擬機管理的最大的一塊內(nèi)存,是GC的主戰(zhàn)場,所以也叫“GC堆”、“垃圾堆” 。
  17. 值得一提:在java虛擬機規(guī)范中,此處定義了一個異常
  18. (1)OutOfMemoryError
  19. 運行時常量池:屬于“方法區(qū)”的一部分,用于存放編譯器生成的各種字面量和符號引用。
  20. 字面量:與Java語言層面的常量概念相近,包含文本字符串、聲明為final的常量值等。
  21. 符號引用:編譯語言層面的概念,包括以下3類:
  22. (1) 類和接口的全限定名
  23. (2)字段的名稱和描述符
  24. (3)方法的名稱和描述符

2、JAVA回收機制

java中是通過GC(Garbage Collection)來進行回收內(nèi)存,那jvm是如何確定一個對象能否被回收的呢?這里就需講到其回收使用的算法

(1) 引用計數(shù)算法

引用計數(shù)是垃圾收集器中的早期策略。在這種方法中,堆中每個對象實例都有一個引用計數(shù)。當一個對象被創(chuàng)建時,且將該對象實例分配給一個變量,該變量計數(shù)設(shè)置為1。當任何其它變量被賦值為這個對象的引用時,計數(shù)加1(a = b,則b引用的對象實例的計數(shù)器+1),當一個對象實例的某個引用超過了生命周期或者被設(shè)置為一個新值時,對象實例的引用計數(shù)器減1。任何引用計數(shù)器為0的對象實例可以被當作垃圾收集。當一個對象實例被垃圾收集時,它引用的任何對象實例的引用計數(shù)器減1。

優(yōu)點:

引用計數(shù)收集器可以很快的執(zhí)行,交織在程序運行中。對程序需要不被長時間打斷的實時環(huán)境比較有利。

缺點:

無法檢測出循環(huán)引用。如父對象有一個對子對象的引用,子對象反過來引用父對象。這樣,他們的引用計數(shù)永遠不可能為0。例如下面代碼片段中,最后的Object實例已經(jīng)不在我們的代碼可控范圍內(nèi),但其引用仍為1,此時內(nèi)存便產(chǎn)生泄漏

/**舉個例子**/
Object o1 = new Object() //Object的引用+1,此時計數(shù)器為1
Object o2;
o2.o = o1; //Object的引用+1,此時計數(shù)器為2
o2 = null;
o1 = null; //Object的引用-1,此時計數(shù)器為1

(2) 可達性分析算法

 

一篇文章教你搞定內(nèi)存泄漏與排查流程——安卓性能優(yōu)化

 

 

可達性分析算法是現(xiàn)在java的主流方法,通過一系列的GC ROOT為起始點,從一個GC ROOT開始,尋找對應(yīng)的引用節(jié)點,找到這個節(jié)點以后,繼續(xù)尋找這個節(jié)點的引用節(jié)點,當所有的引用節(jié)點尋找完畢之后,剩余的節(jié)點則被認為是沒有被引用到的節(jié)點,即無用的節(jié)點(即圖中的ObjD、ObjE、ObjF)。由此可知,即時引用成環(huán)也不會導(dǎo)致泄漏。

java中可作為GC Root的對象有:

1、方法區(qū)中靜態(tài)屬性引用的對象

2、方法區(qū)中常量引用的對象

3、本地方法棧JNI中引用的對象(Native對象)

4、虛擬機棧(本地變量表)中正在運行使用的引用

但是,可達性分析算法中不可達的對象,也并非一定要被回收。當GC第一次掃過這些對象的時候,他們處于“死緩”的階段。要真正執(zhí)行死刑,至少需要經(jīng)過兩次標記過程。 如果對象經(jīng)過可達性分析之后發(fā)現(xiàn)沒有與GC Roots相關(guān)聯(lián)的引用鏈,那他會被第一次標記,并經(jīng)歷一次篩選,這個對象的finalize方法會被執(zhí)行。如果對象沒有覆蓋finalize或者已經(jīng)被執(zhí)行過了。虛擬機也不會去執(zhí)行finalize方法。Finalize是對象逃獄的最后一次機會。

3、四種引用

說到底,內(nèi)存泄漏是因為引用的處理不正當導(dǎo)致的。所以,我們接下來需要老生常談一下java中四種引用,即:強軟弱虛(引用強度依次減弱)。

(1)強引用(Strong reference): 一般我們使用的都是強引用,例如:Object o = new Object();只要強引用還在,垃圾收集器就不會回收被引用的對象。

(2)軟引用(Soft Reference): 用來定義一些還有用但并非必須的對象。對于軟引用關(guān)聯(lián)著的對象,在系統(tǒng)將要內(nèi)存溢出之前,會將這些對象列入回收范圍進行第二次回收,如果回收后還是內(nèi)存不足,才會拋出內(nèi)存溢出。(即在內(nèi)存緊張時,會對其軟引用回收)

(3)弱引用(Weak Reference): 用來描述非必須對象。被弱引用關(guān)聯(lián)的對象只能生存到下一次垃圾收集發(fā)生之前。當垃圾收集器回收時,無論內(nèi)存是否足夠,都會回收掉被弱引用關(guān)聯(lián)的對象。(即GC掃過時,便將弱引用帶走)

(4)虛引用(Phantom Reference): 也稱為幽靈引用或者幻影引用,是最弱的引用關(guān)系。一個對象的虛引用根本不影響其生存時間,也不能通過虛引用獲得一個對象實例。虛引用的唯一作用就是這個對象被GC時可以收到一條系統(tǒng)通知。

軟引用與弱引用的抉擇

如果只是想避免OutOfMemory異常的發(fā)生,則可以使用軟引用。如果對于應(yīng)用的性能更在意,想盡快回收一些占用內(nèi)存比較大的對象,則可以使用弱引用。另外可以根據(jù)對象是否經(jīng)常使用來判斷選擇軟引用還是弱引用。如果該對象可能會經(jīng)常使用的,就盡量用軟引用。如果該對象不被使用的可能性更大些,就可以用弱引用。

4、小結(jié)

至此,我們知道內(nèi)存泄漏是因為堆內(nèi)存中的長生命周期的對象持有短生命周期對象的引用,盡管短生命周期對象已經(jīng)不再需要,但是因為長生命周期對象持有它的引用而導(dǎo)致不能被回收。

5、安卓內(nèi)存泄漏排查工具

所謂工欲善其事必先利其器,這一小節(jié)先簡述下所需借用到的內(nèi)存泄漏排查工具,如果已經(jīng)熟悉的話可以跳過。

(1) Android Profiler

這一工具是Android Studio自帶,可以查看cpu、內(nèi)存使用、網(wǎng)絡(luò)使用情況,Android Studio3.0中用于替代Android Monitor

 

一篇文章教你搞定內(nèi)存泄漏與排查流程——安卓性能優(yōu)化

 

 

① 強制執(zhí)行垃圾收集事件的按鈕。

② 捕獲堆轉(zhuǎn)儲的按鈕。

③ 記錄內(nèi)存分配的按鈕。

④ 放大時間線的按鈕。

⑤ 跳轉(zhuǎn)到實時內(nèi)存數(shù)據(jù)的按鈕。

⑥ 事件時間線顯示活動狀態(tài)、用戶輸入事件和屏幕旋轉(zhuǎn)事件。

⑦ 內(nèi)存使用時間表,其中包括以下內(nèi)容:

• 每個內(nèi)存類別使用多少內(nèi)存的堆棧圖,如左邊的y軸和頂部的顏色鍵所示。

• 虛線表示已分配對象的數(shù)量,如右側(cè)y軸所示。

• 每個垃圾收集事件的圖標。

(2) MAT(Memory Analyzer Tool)

MAT用于鎖定哪里泄漏。因為從Android Profiler中,知道了泄漏,但比較難鎖定具體哪個地方導(dǎo)致了泄漏,所以借助MAT來鎖定,具體使用待會會借助一個例子配合Android Profiler來介紹,稍安勿躁。

下載地址:www.eclipse.org/mat/downloa…

6、內(nèi)存泄漏檢查與解決流程

經(jīng)過前面的一段理論,可能很多小伙伴都有些不耐煩了,現(xiàn)在便來真正的操作。

溫馨提示:理論是進階中必要的支持,否則只是知其然而不知其所以然

(1)第一步:對待檢測功能掃雷式操作

當我們需要檢查一塊模塊,或是整個App哪個地方有內(nèi)存泄漏時,有時會比較茫然,有些大海撈針的感覺,畢竟泄漏不是每個頁面都會有,而且有時是一個功能才會導(dǎo)致泄漏,所以我們可以采取“掃雷式操作”,也就是在需要檢查的頁面和功能中隨便先使用一番,舉個例子:假設(shè)檢查MainActivity泄漏情況,可以登錄進入后,此時來到了MainActivity,后又登出,再次登錄進入MainActivity。

(2)第二步:借助 Android Profiler獲得內(nèi)存快照

使用Android Profiler的GC功能,強制進行垃圾回收,再dump下內(nèi)存("Android Profiler功能簡介"圖的②按鈕)。然后等待一段時間,會出現(xiàn)圖中紅色框部分:

一篇文章教你搞定內(nèi)存泄漏與排查流程——安卓性能優(yōu)化

 

在這里得到的頁面,其實比較難直觀獲得內(nèi)存分析的數(shù)據(jù),最多只是選擇“Arrange by package”按照包進行排序,然后進到自己的包下,查看應(yīng)用內(nèi)的activity的引用數(shù)是否正常,來判斷其是否有正常回收

一篇文章教你搞定內(nèi)存泄漏與排查流程——安卓性能優(yōu)化

 

圖中列的說明

Alloc Cout : 對象數(shù)

Shallow Size : 對象占用內(nèi)存大小

Retained Set : 對象引用組占用內(nèi)存大小(包含了這個對象引用的其他對象)

(3)第三步:借助Android Studio分析

至此,我們還是沒得到直觀的內(nèi)存分析數(shù)據(jù),我們需要借助更專業(yè)的工具。我們現(xiàn)將通過下圖中紅框內(nèi)的按鈕,將剛才的內(nèi)存快照保存為hprof文件。

 

一篇文章教你搞定內(nèi)存泄漏與排查流程——安卓性能優(yōu)化

 

將保存好的hprof文件拖進AS中,勾選“Detect Leaked Activities”,然后點擊綠色按鈕進行分析。

一篇文章教你搞定內(nèi)存泄漏與排查流程——安卓性能優(yōu)化

 

如果有內(nèi)存泄漏的話,會出現(xiàn)如下圖的情況。圖中很清晰的可以看到,這里出現(xiàn)了MainActivity的泄漏。并且觀察到這個MainActivity可能不止一個對象存在,可能是我們上次退出程序的時候發(fā)生了泄漏,導(dǎo)致它不能回收。而在此打開app,系統(tǒng)會創(chuàng)建新的MainActivity。但至此我們只是知道MainActivity泄漏了,不知具體是哪里導(dǎo)致了MainActivity泄漏,所以需要借助MAT來進一步分析。

一篇文章教你搞定內(nèi)存泄漏與排查流程——安卓性能優(yōu)化

 

(4)第四步:hprof文件轉(zhuǎn)換

在使用MAT打開hprof文件前先要對剛才保存的hprof文件進行轉(zhuǎn)換。通過終端,借助轉(zhuǎn)換工具hprof-conv(在sdk/platform-tools/hprof-conv),使用命令行:

hprof-conv -z src dst

-z:排除不是app的內(nèi)存,比如Zygote

src:需要進行轉(zhuǎn)換的hprof的文件路徑

dst:轉(zhuǎn)換后的文件路徑(文件后綴還是.hprof)

(5)第五步:通過MAT進行具體分析 在MAT中打開轉(zhuǎn)換了的hprof文件,如下圖

一篇文章教你搞定內(nèi)存泄漏與排查流程——安卓性能優(yōu)化

 

打開后會看到如下圖!

我們需要進入到"Histogram"來分析,點擊下圖中的按鈕

一篇文章教你搞定內(nèi)存泄漏與排查流程——安卓性能優(yōu)化

 

打開"Histogram"后,會看到下圖,在紅框中輸入在AS中觀察到的泄漏的類,例如上面得知的MainActivity

一篇文章教你搞定內(nèi)存泄漏與排查流程——安卓性能優(yōu)化

 

然后將搜索得到的結(jié)果進行合并,排除“軟”、“弱”、“虛”引用對象,右鍵點擊搜索到的結(jié)果,選擇如下圖的選項

一篇文章教你搞定內(nèi)存泄漏與排查流程——安卓性能優(yōu)化

 

得到合并結(jié)果如下

 

一篇文章教你搞定內(nèi)存泄漏與排查流程——安卓性能優(yōu)化

 

從分析結(jié)果可知,MainActivity是因為com.netease.nimlib.g.e中的一個hashMap持有導(dǎo)致,這里的e類是第三方庫的類,顯然已被混淆,造成泄漏無非兩種可能,一種是第三方庫的bug,一種是自己使用不當,例如忘記解綁操作等。具體的打斷這個持有需要按照自己的代碼進行分析,實例中的問題是因為使用第三方庫注冊后,在退出頁面沒有進行注銷導(dǎo)致的。

當我們解決完后,可以再次進行一輪內(nèi)存快照,直到?jīng)]有內(nèi)存泄漏,過程會比較枯燥,但一點點的解決泄漏最終會給app一個質(zhì)的飛躍。

7、常見的內(nèi)存泄漏原因

(1)集合類

集合類如果僅僅有添加元素的方法,而沒有相應(yīng)的刪除機制,導(dǎo)致內(nèi)存被占用。如果這個集合類是全局性的變量 (比如類中的靜態(tài)屬性,全局性的 map 等即有靜態(tài)引用或 final 一直指向它),那么沒有相應(yīng)的刪除機制,很可能導(dǎo)致集合所占用的內(nèi)存只增不減。

(2)單例模式

不正確使用單例模式是引起內(nèi)存泄露的一個常見問題,單例對象在被初始化后將在 JVM 的整個生命周期中存在(以靜態(tài)變量的方式),如果單例對象持有外部對象的引用,那么這個外部對象將不能被 JVM 正常回收,導(dǎo)致內(nèi)存泄露。

public class SingleTest{
 private static SingleTest instance;
 private Context context;
 private SingleTest(Context context){
 this.context = context;
 }
 public static SingleTest getInstance(Context context){
 if(instance != null){
 instance = new SingleTest(context);
 }
 return instance;
 }
}

這里如果傳遞Activity作為Context來獲得單例對象,那么單例持有Activity的引用,導(dǎo)致Activity不能被釋放。 不要直接對 Activity 進行直接引用作為成員變量,如果允許可以使用Application。 如果不得不需要Activity作為Context,可以使用弱引用WeakReference,相同的,對于Service 等其他有自己生命周期的對象來說,直接引用都需要謹慎考慮是否會存在內(nèi)存泄露的可能。

(3)未關(guān)閉或釋放資源

BroadcastReceiver,ContentObserver,F(xiàn)ileObserver,Cursor,Callback等在 Activity onDestroy 或者某類生命周期結(jié)束之后一定要 unregister 或者 close 掉,否則這個 Activity 類會被 system 強引用,不會被內(nèi)存回收。值得注意的是,關(guān)閉的語句必須在finally中進行關(guān)閉,否則有可能因為異常未關(guān)閉資源,致使activity泄漏

(4)Handler

只要 Handler 發(fā)送的 Message 尚未被處理,則該 Message 及發(fā)送它的 Handler 對象將被線程 MessageQueue 一直持有。特別是handler執(zhí)行延遲任務(wù)。所以,Handler 的使用要尤為小心,否則將很容易導(dǎo)致內(nèi)存泄露的發(fā)生。

public class MainActivity extends AppCompatActivity {
 private Handler mHandler = new Handler(){
 @Override
 public void handleMessage(Message msg) {
 //do something
 }
 };
 private void loadData(){
 //do request
 Message message = Message.obtain();
 mHandler.sendMessage(message);
 }
 @Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_main);
 loadData();
 }
}

這種創(chuàng)建Handler的方式會造成內(nèi)存泄漏,由于mHandler是Handler的非靜態(tài)匿名內(nèi)部類的實例,所以它持有外部類Activity的引用,我們知道消息隊列是在一個Looper線程中不斷輪詢處理消息,那么當這個Activity退出時消息隊列中還有未處理的消息或者正在處理消息,而消息隊列中的Message持有mHandler實例的引用,mHandler又持有Activity的引用,所以導(dǎo)致該Activity的內(nèi)存資源無法及時回收,引發(fā)內(nèi)存泄漏,所以另外一種做法為:

public class MainActivity extends AppCompatActivity {
 private MyHandler mHandler = new MyHandler(this);
 private void loadData() {
 //do request
 Message message = Message.obtain();
 mHandler.sendMessage(message);
 }
 private static class MyHandler extends Handler {
 private WeakReference<Context> reference;
 public MyHandler(Context context) {
 reference = new WeakReference<Context>(context);
 }
 @Override
 public void handleMessage(Message msg) {
 super.handleMessage(msg);
 MainActivity mainActivity = (MainActivity) reference.get();
 if (mainActivity != null) {
 //do something to update UI via mainActivity
 }
 }
 }
 @Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_main);
 loadData();
 }
}

創(chuàng)建一個靜態(tài)Handler內(nèi)部類,然后對Handler持有的對象使用弱引用,這樣在回收時也可以回收Handler持有的對象,這樣雖然避免了Activity泄漏,不過Looper線程的消息隊列中還是可能會有待處理的消息,所以我們在Activity的Destroy時或者Stop時應(yīng)該移除消息隊列中的消息,

@Override
protected void onDestroy() {
 super.onDestroy();
 mHandler.removeCallbacksAndMessages(null);
}

使用mHandler.removeCallbacksAndMessages(null);是移除消息隊列中所有消息和所有的Runnable。當然也可以使用mHandler.removeCallbacks();或mHandler.removeMessages();來移除指定的Runnable和Message。

(5)Thread

和handler一樣,線程也是造成內(nèi)存泄露的一個重要的源頭。線程產(chǎn)生內(nèi)存泄露的主要原因在于線程生命周期的不可控。比如線程是 Activity 的內(nèi)部類,則線程對象中保存了 Activity 的一個引用,當線程的 run 函數(shù)耗時較長沒有結(jié)束時,線程對象是不會被銷毀的,因此它所引用的老的 Activity 也不會被銷毀,因此就出現(xiàn)了內(nèi)存泄露的問題。

(6)系統(tǒng)bug

比如InputMethodManager,會持有activity而沒釋放,導(dǎo)致泄漏,需要通過反射進行打斷。

最后

好啦,文章寫到這里就結(jié)束了,如果你覺得文章寫得不錯就給個贊唄?如果你覺得那里值得改進的,請給我留言。一定會認真查詢,修正不足。謝謝。

分享到:
標簽:泄漏 內(nèi)存
用戶無頭像

網(wǎng)友整理

注冊時間:

網(wǎng)站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網(wǎng)站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網(wǎng)站吧!
最新入駐小程序

數(shù)獨大挑戰(zhàn)2018-06-03

數(shù)獨一種數(shù)學(xué)游戲,玩家需要根據(jù)9

答題星2018-06-03

您可以通過答題星輕松地創(chuàng)建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學(xué)四六

運動步數(shù)有氧達人2018-06-03

記錄運動步數(shù),積累氧氣值。還可偷

每日養(yǎng)生app2018-06-03

每日養(yǎng)生,天天健康

體育訓(xùn)練成績評定2018-06-03

通用課目體育訓(xùn)練成績評定