女人自慰AV免费观看内涵网,日韩国产剧情在线观看网址,神马电影网特片网,最新一级电影欧美,在线观看亚洲欧美日韩,黄色视频在线播放免费观看,ABO涨奶期羡澄,第一导航fulione,美女主播操b

0
  • 聊天消息
  • 系統消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發帖/加入社區
會員中心
創作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

Android內存泄漏問題如何優化?

工程師 ? 來源:CSDN博客 ? 作者:CSDN博客 ? 2020-09-11 15:55 ? 次閱讀

作者:無名之輩FTER

來源:CSDN博客

眾所周知,Java因其擁有獨特的虛擬機(JVM)設計,使其成為一門跨平臺、內存自動管理的高級開發語言。所謂跨平臺,即“一次編譯,多次運行”,從而解決了不同平臺由于編譯器不同導致無法運行問題;所謂內存自動管理,即Java不像C/C++那樣需要開發者來分配、釋放內存,它擁有一套垃圾回收機制來管理內存,這套機制減輕了很多潛在的內存回收不當問題。然而,雖然Java的垃圾回收機制非常優秀,但當我們在寫程序過程中有一些不好的習慣可能會導致部分內存無法被垃圾回收器回收,而我們自己又無法進行回收,從而導致這部分內存長期被占用無法釋放,并且隨著這部分內存的增大,極大的影響了程序的性能,這種情況被稱之為“內存泄漏”。

01

Java虛擬機(JVM)

虛擬機是一種虛構出來的抽象化計算機,是通過在實際的計算機上仿真模擬各種計算機功能來實現的,它擁有自己完善的虛擬硬件架構,如處理器、堆棧、寄存器等,而且還具有相應的指令系統。Java虛擬機就是這么一種虛擬機。Java虛擬機,即Java Virtual Machine(JVM),是運行所有Java程序的抽象計算機,是Java語言的運行環境,它屏蔽了與具體操作系統平臺相關的信息,使得Java程序只需生成在Java虛擬機上運行的目標代碼(字節碼)。任何平臺只要裝有針對于該平臺的Java虛擬機,字節碼文件(.class)就可以在該平臺上運行,即“一次編譯,多次運行”,正是因為如此,從而使得Java語言具有跨平臺移植的特性。

Java虛擬機本質上就是一個程序,Java程序的運行依靠具體的Java虛擬機實例,每個Java應用程序都對應著一個Java虛擬機實例,且Java程序與其對應Java虛擬機實例生命周期一致。在Java虛擬機規范中,JVM主要包括五大模塊,即類裝載器子系統、運行時數據區、執行引擎、本地方法接口和垃圾收集模塊。其中,類加載器子系統,用于加載字節碼文件到內存,就是JVM中的runtime data area(運行時數據區)的method area方法區,整個過程中裝載器只負責文件結構格式能夠被裝入,并不負責能不能運行;運行時存儲區,即JVM內存區域,JVM運行程序的時候使用;執行引擎,在不同的虛擬機實現里面,執行執行引擎可能會有解釋器解釋執行字節碼文件或即時編譯器編譯產生本地代碼執行字節碼文件,可能兩種都有;本地方法接口,即Java Native Interface(JNI),用于與本地庫(native library)交互,是Java與其他編程語言(C/C++)交互的“橋梁”;垃圾收集,用于對已分配的內存資源進行回收,主要是Java堆和方法區的內存。JVM架構如下圖所示:

1.1 JVM內存管理

Java虛擬機在執行Java程序時會把它所管理的內存劃分若干個不同的數據區域,這些區域的用途各不相同,創建、銷毀的時間也各有區別,比如有的隨著Java虛擬機進程的啟動而存在、有的區域則依賴于用戶線程的啟動和結束而創建、銷毀,但它們有一個共同的“名字”,即運行時數據區域。Java虛擬機管理的內存主要有:程序計數器、Java虛擬機棧、本地方法棧、Java堆、方法區以及直接內存等,其中,程序計數器、虛擬機棧和本地方法棧為線程私有,方法區和Java堆為線程間共享。

程序計數器

程序計數器(Program Counter Register)是內存中的一塊較小的區域,它可以看作成是 當前線程 所執行的字節碼行號指示器,依賴于 用戶線程 的啟動和結束而創建、銷毀,是 線程私有 內存數據區域。由于Java虛擬機的多線程是通過線程輪流切換并分配處理器執行時間的方式來實現的,在任何一確定的時刻,一個處理器都只會執行一條線程中的指令,因此,為了線程切換后能恢復到正確的執行位置,每條線程都需要有一個獨立的程序計數器,各個線程之間計數器互不影響、獨立存儲。需要注意的是,如果線程正在執行的是一個 Java方法 ,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;如果線程正在執行的是 Native方法 ,那么這個計數器的值為空。

在Java虛擬機規范中,程序計數器是唯一一個沒有規定任何 OutOfMemoryError 情況的區域。

Java虛擬機棧

類似于程序計數器,Java虛擬機棧(Java Virtual Machine Stacks)也是 線程私有 ,生命周期與用戶線程周期相同,它描述的是Java方法執行的內存模型,即 每個Java方法 在執行時JVM會為其在這部分內存中創建一個 棧幀(Stack Frame) 用于存儲 局部變量表 、 操作數棧 、 動態鏈接 以及 方法出口信息 等,每一個方法從調用到執行完成的過程,就對應著一個棧幀在虛擬機棧中入棧和出棧過程。局部變量表是我們在開發過程中接觸較多的部分,它存放了 編譯器可知 的各種 基本數據類型(byte/boolean/char/int/short/long/float/double) 、 對象引用(reference類型) 和 returnAddress類型 ,其中,64位長度的long和double類型的數據占用2個局部變量空間,其他的類型占1個( 4個字節 )。局部變量表所需的內存空間在 編譯期 間完成分配,當進入一個方法時,這個方法需要在幀中分配多大的局部變量空間是完成確定的,在方法 運行期間不會改變局部變量表的大小 。下圖是虛擬機棧存儲示意圖:

在Java虛擬機規范中,虛擬機??赡軙霈F兩種異常情況,即 StackOverflowError 和 OufOfMemoryError ,其中,StackOverflowError出現在 如果線程請求的棧深度大于虛擬機所允許的深度 ;OufOfMemoryError出現在 如果虛擬機??梢詣討B擴展,但是擴展后仍然無法申請到足夠的內存 。

本地方法棧

本地方法棧(Native Method Stack)與虛擬機棧所發揮的作用非常相似,它們之間的區別不過是虛擬機棧為虛擬機執行Java方法(即 字節碼 ),而本地方法棧則為虛擬機使用到的 Native方法 服務。下圖演示了一個線程調用Java方法和本地方法時的棧,以及虛擬機棧和本地方法棧之間毫無障礙的跳轉。示意圖如下:

在Java虛擬機規范中,本地方法棧也會出現 StackOverflowError 和 OufOfMemoryError 異常情況。

Java堆

Java堆(Java Heap)是Java虛擬機所管理的內存中最大的一塊,它在 JVM啟動 時被創建,生命周期與JVM相同,是被所有 線程所共享 的一塊區域,此區域唯一的目的是存放 對象實例 和 數組 ,幾乎所有的對象實例都在這里分配內存。Java堆是垃圾收集器管理的主要區域,也是“ 內存泄漏 ”集中出現的地方。由于JVM中的垃圾收集器大部分采用分代收集算法,因此,Java堆又被細分為:新生代和老年代,其中, 新生代 區域存放創建不久的對象, 老年代 存放經歷過多次GC后仍然存活的對象。實際上,根據JVM規范,Java堆還可被繼續細分為 Eden 空間、 From Survivor空間 以及 To Survivor空間 等,這個我們在垃圾回收機制模塊詳細闡述。下圖是Java堆內存劃分示意圖:

在JVM規范中,如果堆可以動態擴展,但是擴展后仍然無法申請到足夠的內存,就會拋出OutOfMemoryError異常。當然,我們可以通過 -Xmx 和 -Xms 來控制堆內存的大小,其中, -Xmx 用于設置Java堆起始的大小, -Xms 用于設置Java堆可擴展到最大值。

方法區

像Java堆一樣,方法區(Method Area)是各個 線程共享 的內存區域,它的生命周期與虛擬機相同,即隨著虛擬機的啟動和結束而創建、銷毀。方法主要用于存放 已被虛擬機加載的類信息、常量、靜態變量以及即時編譯器(JIT)編譯后的代碼 等數據,它的大小決定了系統能夠加載多少個類,如果定義的類太多,導致方法區拋出OutOfMemoryError異常。需要注意的是,對于JDK1.7來說,在HotSpot虛擬機中方法區可被理解為“永久區”,但是JDK1.8以后,方法區已被取消,替代的是 元數據區 。元數據區是一塊堆外的直接內存,與永久區不同,如果不指定大小,默認情況下在類加載時虛擬機會盡可能加載更多的類,直至系統內存被消耗殆盡。當然,我們可以使用參數 -XX:MaxMetaspaceSzie 來指定元數據區的大小。

運行時常量池用于存放編譯期生成的各種字面量和符號引用。

直接內存

直接內存(Direct Memory)不是虛擬機運行時數據區的一部分,也不是Java虛擬機規范中定義的內存區域,它是在JDK1.4中新加入的NIO(New Input/Output)類,通過引入了一種基于通道與緩沖區的I/O方式,使用Native函數庫直接分配得到的堆外內存。對于這部分內存區域,主要通過存儲在Java堆中的 DirectByteBuffer 對象作為這塊內存的引用進行操作,直接內存的存在避免了在Java堆和Native堆中來回復制數據,從而在某些場景能夠顯著地提高性能。

本機直接內存的分配不會受到Java堆大小的限制,但是仍然會受到本機總內存( 包括RAM以及SWAP或者分頁文件 )大小以及處理器尋址空間的限制。如果申請分配的內存總和(包括直接內存)超過了物理內存的限制,就會導致動態擴展時出現OutOfMemoryError異常。

1.2 垃圾回收器與內存分配策略

在上一節中我們詳細分析了JVM的運行時內存區域,了解到程序計數器、虛擬機棧、本地方法棧是線程的私有區域,當線程結束時這部分所占內存資源將會自動釋放,而線程的共享區域 Java堆 是存放所有對象實體的地方,因此是垃圾回收器回收( GC,Garbage Collection )的主要區域。(方法區也會有GC,但是一般我們討論的是Java堆)

1.2.1 如何判斷對象“已死”?

垃圾收集器在回收一個對象,第一件事就是確定這些對象之中有哪些是“存活”的,哪些已經“死亡”,而垃圾回收器回收的就是那些已經“死亡”的對象。如何確定對象是否已經死亡呢?通常,我們可能會說當一個對象沒被任何地方引用時,就認為該對象已死。但是,這種表述貌似不夠準確,因此,JVM規范中給出了兩種判斷對象是否死亡的方法,即 引用計數法 和 可達性分析 。

引用計數法

引用計數法實現比較簡單,它的實現原理: 給對象一個引用計數器,每當一個地方引用它時,計數器就加1;當引用失效時,計數器值就減1;任何時刻計數器為0的對象就會被認為已經死亡 ??陀^地說,引用計數器法效率確實比較高,也容易實現,但是它也有不足之處,就是 無用對象之間相互引用 的問題,這種情況的出現會導致相互引用的對象均無法被垃圾回收器回收。

可達性分析

為了解決 引用計數法 的無用對象循環引用導致無法被回收情況,JVM中又引入了 可達性分析 算法來判斷對象是否存活,這種算法也是普遍被應用的方式。可達性分析基本思想:通過一系列被稱為“ GC Roots ”的對象作為起始點,從這些節點開始向下搜索,搜索走過的路徑稱為 引用鏈 。當一個對象到 GC Roots 沒有任何 引用鏈 相連接時,則證明此對象不可用,即被判斷可回收對象??蛇_性分析算法示意圖如下圖所示:

那么,哪些對象可以作為 “GC Roots”呢?

虛擬機棧中( 局部變量表 )引用的對象;

方法區中類靜態屬性引用的對象;

方法區中常量引用的對象;

本地方法棧中Native方法引用的對象;

1.2.2 垃圾收集算法

前面我們通過 引用計數法 或 可達性分析 找到了哪些對象是可以被回收的,本節將重點闡述JVM中的垃圾回收器是如何將這些不可用對象進行回收,即垃圾收集算法,主要包括 標記-清除算法 、 復制算法 、 標記-整理 以及 分代收集 等。相關介紹如下:

標記-清除算法

標記-清理算法是最基礎的垃圾收集算法,它的實現分為兩個階段,即 “標記” 和 “清除” ,其中,標記的作用為通過引用計數法或可達性分析算法標記出所有需要回收的對象;清除的作用為在標記完成后統一回收所有被標記的對象。這種算法比較簡單,但是缺陷也比較明顯,主要表現為兩個方面: 一是標記和清理的效率比較低;二是標記清理之后會產生大量不連續的內存碎片,空間碎片太大可能會導致以后在程序運行過程中需要分配較大對象時,無法找到足夠的連續內存而不得不觸發另一次GC。 標記-清除算法執行過程如下圖所示:

復制算法

為了解決標記-清理算法效率不高問題,人們提出了一種 復制算法 ,它的基本原理: 將可用內存容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活的對象復制到另一塊上,然后再把已使用的內存空間一次性清理掉。 這種方式實現簡單,運行高效,且緩解了內存碎片的問題,但是由于其只對整個半區進行內存分配、回收,從而導致可使用的內存縮小為整個內存的一半。復制算法執行過程如下圖所示:

在HotSpot虛擬機中,整個內存空間被分為一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden 空間和其中的一塊 Survivor 空間。當回收時,將 Eden 和 Survivor 中還存活著的對象一次性復制到另一塊 Survivor 空間上,最后清理掉 Eden 和剛才用過的 Survivor 空間。HotSpot虛擬機默認 Eden 和 Surivor 的大小比例為 8:1:1 ,也就是每次新生代中可用內存空間為整個內存空間的90%,這就意味著有剩余的10%不可用。當 Survivor 空間不夠用時,就需要依賴其他內存( 老年代 )進行分擔擔保。

注 :新生代是指剛創建不久的對象;老年代指被多次GC仍然存活的對象。

標記-整理算法

雖說復制算法有效地提高了標記-清除算法效率不高問題,但是在對象存活率較高的情況下,就需要進行較多的復制操作(復制對象),尤其是所有對象都100%存活的極端情況,這種復r制算法效率將會大大降低,因此,老年代區域通常不會直接選用這種算法。根據老年代的特點,有人提出了 標記-整理算法 ,該算法基于標記-清除算法發展而來,其中,標記同標記-清除算法一致,整理為 將所有存活的對象都向一端移動,然后直接清理掉端邊界以外的內存。 標記-整理算法執行過程如下圖所示:

分代收集算法

分代收集算法是目前大部分虛擬機的垃圾收集器采用的算法,這種算法的思想是 根據對象的存活周期的不同將Java堆內存劃分為幾塊,即新生代區域和老年代區域,然后對不同的區域采用合適的算法。 由于新生代每次GC時都會有大批對象死去,只有少量的對象存活,因此通常選用 復制算法 ;而老年代中因為存活率高、沒有額外空間對它進行分配擔保,就必須使用“ 標記-清理 “或” 標記-整理 ”算法進行回收。分代收集算法模型如下圖所示:

哪些對象能夠進入老年代?

大對象;

每次Eden進行GC后對象年齡加1進入Survivor,對象年齡達到15時進入老年代;

如果Survivor空間中相同年齡所有對象大小的總和大于survivor空間的一半,年齡大于等于該年齡的對象就直接進入老年代。

如果survivor空間不能容納Eden中存活的對象,由于擔保機制會進入老年代。如果survivor中的對象存活很多,擔保失敗,那么會進行一次Full GC。

什么是Minor GC、Major GC和Full GC?

Minor GC從新生代空間(Eden和Survivor區域)回收內存;

Major GC是清理永久代;

Full GC是清理整個堆內存空間,包括新生代和永久代。

1.2.3 內存分配與回收策略

Java的自動內存管理歸結于兩方面,即 為對象分配內存 和 回收分配給對象的內存 ,其中,在上一小節中我們詳細闡述了回收內存的具體細節,這里不再討論。對于對象的內存分配,實際上就是在Java堆中為對象分配內存,準確來說是在新生代的 Eden 區上,如果啟動了本地線程分配緩沖,將按線程優先在TLAB上分配,并且,少數情況下也可能會直接分配在老年代中??傊?,JVM中對對象內存的分配不是固定的模式,其細節取決于使用哪種垃圾收集器,和虛擬機中與內存相關的參數設置。常見的內存分配策略:

對象優先在Eden分配

大多數情況下,對象在Java堆的新生代Eden區中分配,當Eden區沒有足夠空間進行分配時,虛擬機將發起一次 Minor GC 。

大對象直接進入老年代

所謂的大對象是指需要大量連續內存空間的Java對象,最典型的大對象就是那種 很長的字符串 以及 數組 。對于內存分配來說,大對象也是一個很棘手的東西,尤其是“短命大對象”,經常出現在內存空間還較多的情況下,大對象直接導致提前出發垃圾收集器以獲取足夠的連續空間來“安置”它們。

虛擬機提供了一個 -XX:PretenureSizeThreshold 參數,使得大于這個設置值得對象直接在老年代內存區域分配,這樣做的目的在于避免在Eden區及兩個Survivor區之間發生大量的內存復制。

長期存活的對象將進入老年代

虛擬機給每個對象定義了一個對象年齡(Age)計數器,如果對象在Eden出生并經過第一次Minor GC后仍然存活,并且能被Survivor容納的話,將被移動到Survivor空間中,并且對象年齡設為1.對象在Survivor區中每“熬過”一次Minor GC,年齡就增加1歲,當它的年齡增加到一定程度( 默認15歲 ),就將會被晉升到老年代中。虛擬機提供了一個 -XX:MaxTenuringThreshold 參數設置老年代年齡閾值。

虛擬機并不是永遠地要求對象的年齡必須達到了 -XX:MaxTenuringThreshold 才能晉升到老年代,如果在Survivor空間中相同年齡所有對象的大小的總和大于Survivor空間的一半,年齡大于或者等于該年齡的對象就可以直接進入老年代,無須等到 -XX:MaxTenuringThreshold 中要求的年齡。

空間分配擔保

在發生 Minor GC 之前,虛擬機會先檢查老年代最大可用的連續空間是否大于新生代所有對象總空間,如果這個條件成立,那么 Minor GC 可以確保是安全的;如果不成立,則虛擬機會查看 HandlePromotionFailure 設置值是否允許擔保失敗。如果允許,那么會繼續檢查老年代最大可用的連續空間是否大于歷次晉升到老年代對象的平均大小,如果大于,將嘗試著進行一次 Minor GC ,盡管這次 Minor GC 是有風險的;如果小于,或者 HandlePromotionFailure 設置不允許冒險,那么這時就需要進行一次 Full GC。

1.3 JVM的類加載機制

類從被加載到虛擬機內存中開始,到卸載出內存為止,它的整個生命周期包括: 加載(Loading) 、 驗證(Verification) 、 準備(Preparation) 、 解析(Resolution) 、 初始化(Initialization) 、 使用(Using) 和 卸載(Unloading) 7個階段,其中,驗證、準備、解析3個部分統稱為連接(Linking)。類的生命周期如下圖所示:

在虛擬機中,我們常說的類的加載過程是指 加載(Loading) 、 驗證(Verification) 、 準備(Preparation) 、 解析(Resolution) 、 初始化(Initialization) 這五個階段,它們的具體作用為:

加載

加載過程是將二進制字節流(Class字節碼文件)通過 類加載器 加載到內存并實例化Class對象的過程( 加載到方法區內 )。這個過程獨立于虛擬機之外,并且二進制流可以從不同的環境內獲取或者由其他文件生成。

驗證

驗證Class文件的字節流是否符合虛擬機的要求,以免造成虛擬機出現異常。包括: 文件格式驗證 、 元數據驗證 、 字節碼驗證 、 符號引用驗證 。

準備

為靜態變量( 被final關鍵字修飾 )分配內存空間、賦值和設置類變量初始化(自動初始化)。

解析

將常量池內的 符號引用 替換為 直接引用 的過程。解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄以及調用點限定符7類符號引用進行。

初始化

執行類構造器 《clinit》 方法的過程,變量的聲明初始化就在這個階段進行。

虛擬機類加載的時機?

1)遇到new、getstatic、putstatic或者invokestatic 這四條字節碼指令的時候,且該類沒有進行初始化則進行該類的初始化;

2)使用反射機制的時候;

3)初始化類的父類;

4)初始化虛擬機要執行主類;

5)使用動態語言特性的時候;

總之,當對一個類進行主動引用的時候就會進行初始化操作,而進行被動引用的時候便不會觸發類初始化操作,比如通過子類引用父類靜態字段時子類不會被初始化。

02

常見內存泄漏與優化

2.1 內存泄漏

當一個對象已經不需要再使用本該被回收時,另外一個正在使用的對象持有它的引用從而導致它不能被垃圾收集器回收,結果它們就一直存在于內存中(通常指Java堆內存),占用有效空間,永遠無法被刪除。隨著內存不斷泄漏,堆中的可用空間就不斷變小,這意味著為了執行常用的程序,垃圾清理需要啟動的次數越來越多,非常嚴重的話會直接造成應用程序報OOM異常。

優化/避免內存泄漏原則:

涉及到使用Context時,盡量使用Application的Context;

對于非靜態內部類、匿名內部類,需將其獨立出來或者改為靜態類;

在靜態內部類中持有外部類(非靜態)的對象引用,使用弱引用來處理;

不再使用的資源(對象),需顯示釋放資源(對象置為null),如果是集合需要清空;

保持對對象生命周期的敏感,尤其注意單例、靜態對象、全局性集合等的生命周期;

2.2 常見內存泄漏與優化

(1) 單例造成的內存泄漏

案例

/** 工具類,單例模式 * @Auther: Jiangdg * @Date: 2019/10/8 17:23 * @Description: */public class CommonUtils { private static CommonUtils instance; private Context mCtx; private CommonUtils(Context context){ this.mCtx = context; } public static CommonUtils getInstance(Context context) { if(instance == null) { instance = new CommonUtils(context); } return instance; }}/**使用單例模式時造成內存泄漏 * * @Auther: Jiangdg * @Date: 2019/10/8 17:24 * @Description: */public class SingleActivity extends AppCompatActivity { private CommonUtils mUtils; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); mUtils = CommonUtils.getInstance(this); }}

分析與優化

在上述示例中,當SingleActivity實例化Commontils對象完畢后,Commontils將持有SingleActivity對象的引用,而由于單例模式的靜態特性,Commontils對象的生命周期將于應用進程的一致,這就會導致在應用未退出的情況下,如果SingleActivity對象已經不再需要了,而Commontils對象該持有該對象的引用就會使得GC無法對其進行正?;厥?,從而導致了內存泄漏。優化:對于需要傳入Context參數的情況,盡量使用Application的Context,因為它會伴隨著應用進程的存在而存在。

public class SingleActivity extends AppCompatActivity { private CommonUtils mUtils; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 造成內存泄漏 //mUtils = CommonUtils.getInstance(this); mUtils = CommonUtils.getInstance(this.getApplicationContext()); }}

(2) Handler造成的內存泄漏

案例

/** 使用Handler造成內存泄漏 * @Auther: Jiangdg * @Date: 2019/10/8 17:55 * @Description: */public class HandlerActivity extends AppCompatActivity { // 匿名內部類 private Handler mUIHandler = new Handler() { @Override public void handleMessage(Message msg) { super.handleMessage(msg); } }; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); new Thread(new Runnable() { @Override public void run() { // 處理耗時任務 // 。.. mUIHandler.sendEmptyMessage(0x00); } }); }}

分析與優化

在剖析Handler消息機制原理一文中我們知道,在Android應用啟動時,應用程序的主線程會為其自動創建一個 Looper 對象和與之關聯的 MessageQueue ,當主線程實例化一個 Handler 對象后,它就自動與主線程的 MessageQueue 關聯起來,所有發送到 MessageQueue 的 Message (消息)都會持有Handler的引用。由于 主線程的Looper對象會隨著應用進程一直存在的且Java類中的非靜態內部類和匿名內部類默認持有外部類的引用 ,假如 HandlerActivity 提前出棧不使用了,但 MessageQueue 中仍然還有未處理的 Message , Looper 就會不斷地從 MessageQueue 取出消息交給 Handler 來處理,就會導致 Handler 對象一直持有 HandlerActivity 對象的引用,從而出現 HandlerActivity 對象無法被GC正常回收,進而造成內存泄漏。優化:將Handler類獨立出來,或者使用靜態內部類,因為靜態內部類不持有外部類的引用。

public class HandlerActivity extends AppCompatActivity {// 匿名內部類默認持有HandlerActivity的引用// 造成內存泄漏// private Handler mUIHandler = new Handler() {// @Override// public void handleMessage(Message msg) {// super.handleMessage(msg);// }// }; // 優化,使用靜態內部類 // 假如要持有HandlerActivity,以便在UIHandler中訪問其成員變量或成員方法 // 需要使用弱引用處理 private UIHandler mUIHandler; static class UIHandler extends Handler { private WeakReference《HandlerActivity》 mWfActivity; public UIHandler(HandlerActivity activity) { mWfActivity = new WeakReference《》(activity); } @Override public void handleMessage(Message msg) { super.handleMessage(msg); } } @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); mUIHandler = new UIHandler(this); }}

Java中四種引用關系:

強引用

用來描述永遠不會被垃圾收集器回收掉的對象,類似“Object obj = new Object”

軟引用

用來描述一些還有用但并非必須的對象,由 SoftReference 類實現。被軟引用關聯著的對象會在系統將要發生OOM之前,垃圾收集器才會回收掉這些對象。

弱引用

用來描述非必須的對象,比軟引用更弱一些,由 WeakReference 類實現。被弱引用的對象只能生產到下一次垃圾收集發生之前,無論當前內存是否足夠。

虛引用

最弱的引種引用關系,由 PhantomReference 類實現。一個對象是否有虛引用的存在,完全不會對其生存時間產生影響,也無法通過虛引用來獲取該對象實例。為一個對象設置虛引用關聯的唯一目的是能在這個 對象被垃圾收集器回收時收到一個系統通知 。

(3) 線程(非靜態內部類或匿名內部類)造成的內存泄漏

案例

/** 使用線程造成的內存泄漏 * @Auther: Jiangdg * @Date: 2019/10/9 10:04 * @Description: */public class ThreadActivity extends AppCompatActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 開啟一個子線程 new Thread(new MyRunnable()).start(); // 開啟一個異步任務 new MyAsyncTask(this).execute(); } class MyRunnable implements Runnable { @Override public void run() { } } class MyAsyncTask extends AsyncTask { private Context mCtx; public MyAsyncTask(Context context) { this.mCtx = context; } @Override protected Object doInBackground(Object[] objects) { return null; } }}

分析與優化

在之前的分析中可知,Java類中的非靜態內部類和匿名內部類默認持有外部類的引用。對于上述示例中的 MyRunnable 和 MyAsyncTask 來說,它們是一個 非靜態內部類 ,將默認持有 ThreadActivity 對象的引用。假如子線程的任務在 ThreadActivity 銷毀之前還未完成,就會導致 ThreadActivity 無法被GC正常回收,造成內存泄漏。優化:將MyRunnable和MyAsyncTask獨立出來,或使用靜態內部類,因為靜態內部類不持有外部類的引用。

public class ThreadActivity extends AppCompatActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 開啟一個子線程 new Thread(new MyRunnable()).start(); // 開啟一個異步任務 // 優化:使用Application的Context new MyAsyncTask(this.getApplicationContext()).execute(); } // 優化:使用靜態內部類 static class MyRunnable implements Runnable { @Override public void run() { } } // 優化:使用靜態內部類 // 如果需要傳入Context,使用Application的Context static class MyAsyncTask extends AsyncTask { private Context mCtx; public MyAsyncTask(Context context) { this.mCtx = context; } @Override protected Object doInBackground(Object[] objects) { return null; } }}

(4) 靜態實例造成的內存泄漏

案例

/**非靜態內部類創建靜態實例造成的內存泄漏 * @Auther: Jiangdg * @Date: 2019/10/9 10:43 * @Description: */public class StaticInstanceActivity extends AppCompatActivity { private static SomeResources mSomeResources; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); if(mSomeResources == null) { mSomeResources = new SomeResources(this); } } class SomeResources { private Context mCtx; public SomeResources(Context context) { this.mCtx = context; } }}

分析與優化

在上述案例中,演示了防止 StaticInstanceActivity 重建,比如橫豎屏切換,導致反復創建 SomeResources 實例的問題,這里使用了 static 修飾關鍵字將 SomeResources 實例聲明了靜態實例,以確保該實例始終存在的是同一個,且它的生命周期與應用相同。然而,由于 SomeResources 是一個 非靜態內部類 ,其對象默認持有外部類 StaticInstanceActivity 的引用,就會導 SomeResources 的對象一直持有該引用,造成內存泄漏。優化:使用單例模式實現SomeResources,或者將其改成靜態內部類。如果需要傳入Context參數,必須使用Application的Context。

public class StaticInstanceActivity extends AppCompatActivity { private static SomeResources mSomeResources; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); if(mSomeResources == null) { // 優化,使用Application的Context mSomeResources = new SomeResources(this.getApplicationContext()); } } // 優化:使用靜態內部類 static class SomeResources { private Context mCtx; public SomeResources(Context context) { this.mCtx = context; } }}

(5) 資源未關閉或監聽器未移除(注銷)引起的內存泄露情況

在開發中,如果使用了 BraodcastReceiver , ContentObserver , File , Cursor , Stream , Bitmap 、 自定義屬性attributeattr 、傳感器等資源,應該在Activity銷毀時及時關閉或者注銷,否則這些資源將不會被回收,從而造成內存泄漏。比如:

// 使用傳感器等資源,需要注銷SensorManager sensorManager = getSystemService(SENSOR_SERVICE);Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);sensorManager.registerListener(this,sensor,SensorManager.SENSOR_DELAY_FASTEST);sensorManager.unregisterListener(listener);// 使用BraodcastReceiver,需要注銷Myreceiver recevier = new Myreceiver();intentFilter = new IntentFilter();intentFilter.addAction(“android.net.conn.CONNECTIVITY_CHANGE”);registerReceiver(recevier,intentFilter);unRegisterReceiver(recevier);// 自定義屬性,需要recycleTypedArray a = context.obtainStyledAttributes(attrs, R.styleable.AttrDeclareView);int color = a.getColor(R.styleable.AttrDeclareView_background_color, 0);a.recycle();

除了上述常見的5種內存泄漏外,還有包括 無限循環動畫 、 使用ListView 、 使用集合容器 以及 使用WebView 也會造成內存泄漏,其中,無限循環動畫造成泄漏的原因是沒有再Activity的onDestory中停止動畫;使用ListView造成泄漏的原因是構造Adapter時沒有使用緩存的convertView;使用集合容器造成泄漏的原因是在不使用相關對象時,沒有清理掉集合中存儲的對象引用。在優化時,在退出程序之前將集合中的元素(引用)全部清理掉,再置為null;使用WebView造成泄漏的原因是在不使用WebView時沒有調用其destory方法來銷毀它,導致其長期占用內存且不能被回收。在優化時,可以為WebView開啟另外一個進程,通過AIDL與主線程進行通信,便于WebVIew所在的進程可以根據業務需要選擇合適的時機進行銷毀。

聲明:本文內容及配圖由入駐作者撰寫或者入駐合作網站授權轉載。文章觀點僅代表作者本人,不代表電子發燒友網立場。文章及其配圖僅供工程師學習之用,如有內容侵權或者其他違規問題,請聯系本站處理。 舉報投訴
  • Android
    +關注

    關注

    12

    文章

    3962

    瀏覽量

    129500
  • 內存
    +關注

    關注

    8

    文章

    3107

    瀏覽量

    74968
  • JAVA
    +關注

    關注

    20

    文章

    2984

    瀏覽量

    106788
收藏 人收藏

    評論

    相關推薦
    熱點推薦

    HarmonyOS優化應用內存占用問題性能優化

    一、使用purgeable優化C++內存 Purgeable Memory是HarmonyOS中native層常用的內存管理機制,可用于圖像處理的Bitmap、流媒體應用的一次性數據、圖片等
    發表于 05-24 17:20

    HarmonyOS優化應用內存占用問題性能優化

    應用開發過程中注重內存管理,積極采取措施來減少內存占用,以優化應用程序的性能和用戶體驗。 HarmonyOS提供了一些內存管理的工具和接口,幫助開發者有效地管理
    發表于 05-21 11:27

    使用OpenVINO?進行推理時的內存泄漏怎么解決?

    使用 OpenVINO? 進行推理時,內存會隨著時間的推移而增加,并導致程序崩潰。
    發表于 03-06 08:29

    內存泄漏檢測工具Sanitizer介紹

    內存泄漏,我們經常會遇到,如何檢測內存泄漏,除了我們之前講過的 valgrind,還可以使用 gcc 自帶的工具 sanitizer。
    的頭像 發表于 03-01 14:52 ?512次閱讀

    hyper 內存,Hyper內存:如何監控與優化hyper-v虛擬機的內存使用

    :如何監控與優化hyper-v虛擬機的內存使用。 ? ?在虛擬化環境中,合理監控和優化Hyper-V虛擬機的內存使用對于提升性能和資源利用率至關重要。本文將詳細介紹如何監控Hyper-
    的頭像 發表于 01-24 14:15 ?913次閱讀
    hyper <b class='flag-5'>內存</b>,Hyper<b class='flag-5'>內存</b>:如何監控與<b class='flag-5'>優化</b>hyper-v虛擬機的<b class='flag-5'>內存</b>使用

    虛擬內存溢出該怎么處理 虛擬內存在服務器中的應用

    、虛擬內存溢出的原因 內存泄漏 :程序中未正確釋放的內存會導致內存泄漏,隨著時間的推移,這些
    的頭像 發表于 12-04 09:49 ?679次閱讀

    什么是泄漏電流試驗?

    泄漏電流的概念泄漏電流(leakagecurrent)也叫接觸電流,是指在沒有故障施加電壓的情況下,電氣中相互絕緣的金屬零件之間,或帶電零件與接地零件之間,通過其周圍介質或絕緣表面所形成的電流。泄漏
    的頭像 發表于 11-26 11:48 ?1409次閱讀
    什么是<b class='flag-5'>泄漏</b>電流試驗?

    如何優化RAM內存使用

    優化RAM內存使用是一個重要的任務,特別是對于那些擁有有限內存資源的用戶。以下是一些優化RAM內存使用的策略,這些策略可以幫助您更有效地使用
    的頭像 發表于 11-11 09:58 ?1218次閱讀

    DRA7xx器件上的Android啟動優化

    電子發燒友網站提供《DRA7xx器件上的Android啟動優化.pdf》資料免費下載
    發表于 10-11 09:41 ?0次下載
    DRA7xx器件上的<b class='flag-5'>Android</b>啟動<b class='flag-5'>優化</b>

    TinyMaix框架的內存需求超過了APM32F411的可用內存,導致運行失敗,怎么能成功優化?

    TinyMaix框架的內存需求超過了APM32F411的可用內存,導致運行失敗。怎么能成功優化?
    發表于 09-27 09:44

    堆棧和內存的基本知識

    本文主要聊聊關于堆棧的內容。包括堆棧和內存的基本知識。常見和堆棧相關的 bug,如棧溢出,內存泄漏,堆內存分配失敗等。后面介紹軟件中堆棧統計的重要性,以及如何使用工具工具軟件中堆棧使用
    的頭像 發表于 08-29 14:10 ?899次閱讀
    堆棧和<b class='flag-5'>內存</b>的基本知識

    如何檢測內存泄漏

    檢測內存泄漏是軟件開發過程中一項至關重要的任務,它有助于識別和解決那些導致程序占用過多內存資源,從而影響程序性能甚至導致程序崩潰的問題。以下將詳細闡述幾種常見的內存
    的頭像 發表于 07-30 11:50 ?3130次閱讀

    NONOS 1.5.3/1.5.4 SSL內存泄漏的原因?

    我已經通過隨附的代碼驗證了當發生 SSL 握手錯誤時,會生成內存泄漏 此外,espconn_reconnect_callback不稱為信令ESPCONN_HANDSHAKE - TCP SSL 握手
    發表于 07-18 07:24

    使用system_show_malloc()檢查內存泄漏遇到異常怎么解決?

    我想使用system_show_malloc()檢查內存泄漏,但是當我調用該函數時,我得到了致命的異常: 致命異常 28 (LoadProhibitedCause): epc1
    發表于 07-10 06:32

    mesh的內存占用能否優化

    余110kb可用。 請問,mesh的內存占用問題能否優化?為何系統剩余大概60K0內存以下的時候系統會因內存不足重啟?
    發表于 06-28 15:32