作者:無名之輩FTER
來源:CSDN博客
眾所周知,Java因其擁有獨(dú)特的虛擬機(jī)(JVM)設(shè)計,使其成為一門跨平臺、內(nèi)存自動管理的高級開發(fā)語言。所謂跨平臺,即“一次編譯,多次運(yùn)行”,從而解決了不同平臺由于編譯器不同導(dǎo)致無法運(yùn)行問題;所謂內(nèi)存自動管理,即Java不像C/C++那樣需要開發(fā)者來分配、釋放內(nèi)存,它擁有一套垃圾回收機(jī)制來管理內(nèi)存,這套機(jī)制減輕了很多潛在的內(nèi)存回收不當(dāng)問題。然而,雖然Java的垃圾回收機(jī)制非常優(yōu)秀,但當(dāng)我們在寫程序過程中有一些不好的習(xí)慣可能會導(dǎo)致部分內(nèi)存無法被垃圾回收器回收,而我們自己又無法進(jìn)行回收,從而導(dǎo)致這部分內(nèi)存長期被占用無法釋放,并且隨著這部分內(nèi)存的增大,極大的影響了程序的性能,這種情況被稱之為“內(nèi)存泄漏”。
01
Java虛擬機(jī)(JVM)
虛擬機(jī)是一種虛構(gòu)出來的抽象化計算機(jī),是通過在實(shí)際的計算機(jī)上仿真模擬各種計算機(jī)功能來實(shí)現(xiàn)的,它擁有自己完善的虛擬硬件架構(gòu),如處理器、堆棧、寄存器等,而且還具有相應(yīng)的指令系統(tǒng)。Java虛擬機(jī)就是這么一種虛擬機(jī)。Java虛擬機(jī),即Java Virtual Machine(JVM),是運(yùn)行所有Java程序的抽象計算機(jī),是Java語言的運(yùn)行環(huán)境,它屏蔽了與具體操作系統(tǒng)平臺相關(guān)的信息,使得Java程序只需生成在Java虛擬機(jī)上運(yùn)行的目標(biāo)代碼(字節(jié)碼)。任何平臺只要裝有針對于該平臺的Java虛擬機(jī),字節(jié)碼文件(.class)就可以在該平臺上運(yùn)行,即“一次編譯,多次運(yùn)行”,正是因為如此,從而使得Java語言具有跨平臺移植的特性。
Java虛擬機(jī)本質(zhì)上就是一個程序,Java程序的運(yùn)行依靠具體的Java虛擬機(jī)實(shí)例,每個Java應(yīng)用程序都對應(yīng)著一個Java虛擬機(jī)實(shí)例,且Java程序與其對應(yīng)Java虛擬機(jī)實(shí)例生命周期一致。在Java虛擬機(jī)規(guī)范中,JVM主要包括五大模塊,即類裝載器子系統(tǒng)、運(yùn)行時數(shù)據(jù)區(qū)、執(zhí)行引擎、本地方法接口和垃圾收集模塊。其中,類加載器子系統(tǒng),用于加載字節(jié)碼文件到內(nèi)存,就是JVM中的runtime data area(運(yùn)行時數(shù)據(jù)區(qū))的method area方法區(qū),整個過程中裝載器只負(fù)責(zé)文件結(jié)構(gòu)格式能夠被裝入,并不負(fù)責(zé)能不能運(yùn)行;運(yùn)行時存儲區(qū),即JVM內(nèi)存區(qū)域,JVM運(yùn)行程序的時候使用;執(zhí)行引擎,在不同的虛擬機(jī)實(shí)現(xiàn)里面,執(zhí)行執(zhí)行引擎可能會有解釋器解釋執(zhí)行字節(jié)碼文件或即時編譯器編譯產(chǎn)生本地代碼執(zhí)行字節(jié)碼文件,可能兩種都有;本地方法接口,即Java Native Interface(JNI),用于與本地庫(native library)交互,是Java與其他編程語言(C/C++)交互的“橋梁”;垃圾收集,用于對已分配的內(nèi)存資源進(jìn)行回收,主要是Java堆和方法區(qū)的內(nèi)存。JVM架構(gòu)如下圖所示:
1.1 JVM內(nèi)存管理
Java虛擬機(jī)在執(zhí)行Java程序時會把它所管理的內(nèi)存劃分若干個不同的數(shù)據(jù)區(qū)域,這些區(qū)域的用途各不相同,創(chuàng)建、銷毀的時間也各有區(qū)別,比如有的隨著Java虛擬機(jī)進(jìn)程的啟動而存在、有的區(qū)域則依賴于用戶線程的啟動和結(jié)束而創(chuàng)建、銷毀,但它們有一個共同的“名字”,即運(yùn)行時數(shù)據(jù)區(qū)域。Java虛擬機(jī)管理的內(nèi)存主要有:程序計數(shù)器、Java虛擬機(jī)棧、本地方法棧、Java堆、方法區(qū)以及直接內(nèi)存等,其中,程序計數(shù)器、虛擬機(jī)棧和本地方法棧為線程私有,方法區(qū)和Java堆為線程間共享。
程序計數(shù)器
程序計數(shù)器(Program Counter Register)是內(nèi)存中的一塊較小的區(qū)域,它可以看作成是 當(dāng)前線程 所執(zhí)行的字節(jié)碼行號指示器,依賴于 用戶線程 的啟動和結(jié)束而創(chuàng)建、銷毀,是 線程私有 內(nèi)存數(shù)據(jù)區(qū)域。由于Java虛擬機(jī)的多線程是通過線程輪流切換并分配處理器執(zhí)行時間的方式來實(shí)現(xiàn)的,在任何一確定的時刻,一個處理器都只會執(zhí)行一條線程中的指令,因此,為了線程切換后能恢復(fù)到正確的執(zhí)行位置,每條線程都需要有一個獨(dú)立的程序計數(shù)器,各個線程之間計數(shù)器互不影響、獨(dú)立存儲。需要注意的是,如果線程正在執(zhí)行的是一個 Java方法 ,這個計數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址;如果線程正在執(zhí)行的是 Native方法 ,那么這個計數(shù)器的值為空。
在Java虛擬機(jī)規(guī)范中,程序計數(shù)器是唯一一個沒有規(guī)定任何 OutOfMemoryError 情況的區(qū)域。
Java虛擬機(jī)棧
類似于程序計數(shù)器,Java虛擬機(jī)棧(Java Virtual Machine Stacks)也是 線程私有 ,生命周期與用戶線程周期相同,它描述的是Java方法執(zhí)行的內(nèi)存模型,即 每個Java方法 在執(zhí)行時JVM會為其在這部分內(nèi)存中創(chuàng)建一個 棧幀(Stack Frame) 用于存儲 局部變量表 、 操作數(shù)棧 、 動態(tài)鏈接 以及 方法出口信息 等,每一個方法從調(diào)用到執(zhí)行完成的過程,就對應(yīng)著一個棧幀在虛擬機(jī)棧中入棧和出棧過程。局部變量表是我們在開發(fā)過程中接觸較多的部分,它存放了 編譯器可知 的各種 基本數(shù)據(jù)類型(byte/boolean/char/int/short/long/float/double) 、 對象引用(reference類型) 和 returnAddress類型 ,其中,64位長度的long和double類型的數(shù)據(jù)占用2個局部變量空間,其他的類型占1個( 4個字節(jié) )。局部變量表所需的內(nèi)存空間在 編譯期 間完成分配,當(dāng)進(jìn)入一個方法時,這個方法需要在幀中分配多大的局部變量空間是完成確定的,在方法 運(yùn)行期間不會改變局部變量表的大小 。下圖是虛擬機(jī)棧存儲示意圖:
在Java虛擬機(jī)規(guī)范中,虛擬機(jī)棧可能會出現(xiàn)兩種異常情況,即 StackOverflowError 和 OufOfMemoryError ,其中,StackOverflowError出現(xiàn)在 如果線程請求的棧深度大于虛擬機(jī)所允許的深度 ;OufOfMemoryError出現(xiàn)在 如果虛擬機(jī)棧可以動態(tài)擴(kuò)展,但是擴(kuò)展后仍然無法申請到足夠的內(nèi)存 。
本地方法棧
本地方法棧(Native Method Stack)與虛擬機(jī)棧所發(fā)揮的作用非常相似,它們之間的區(qū)別不過是虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java方法(即 字節(jié)碼 ),而本地方法棧則為虛擬機(jī)使用到的 Native方法 服務(wù)。下圖演示了一個線程調(diào)用Java方法和本地方法時的棧,以及虛擬機(jī)棧和本地方法棧之間毫無障礙的跳轉(zhuǎn)。示意圖如下:
在Java虛擬機(jī)規(guī)范中,本地方法棧也會出現(xiàn) StackOverflowError 和 OufOfMemoryError 異常情況。
Java堆
Java堆(Java Heap)是Java虛擬機(jī)所管理的內(nèi)存中最大的一塊,它在 JVM啟動 時被創(chuàng)建,生命周期與JVM相同,是被所有 線程所共享 的一塊區(qū)域,此區(qū)域唯一的目的是存放 對象實(shí)例 和 數(shù)組 ,幾乎所有的對象實(shí)例都在這里分配內(nèi)存。Java堆是垃圾收集器管理的主要區(qū)域,也是“ 內(nèi)存泄漏 ”集中出現(xiàn)的地方。由于JVM中的垃圾收集器大部分采用分代收集算法,因此,Java堆又被細(xì)分為:新生代和老年代,其中, 新生代 區(qū)域存放創(chuàng)建不久的對象, 老年代 存放經(jīng)歷過多次GC后仍然存活的對象。實(shí)際上,根據(jù)JVM規(guī)范,Java堆還可被繼續(xù)細(xì)分為 Eden 空間、 From Survivor空間 以及 To Survivor空間 等,這個我們在垃圾回收機(jī)制模塊詳細(xì)闡述。下圖是Java堆內(nèi)存劃分示意圖:
在JVM規(guī)范中,如果堆可以動態(tài)擴(kuò)展,但是擴(kuò)展后仍然無法申請到足夠的內(nèi)存,就會拋出OutOfMemoryError異常。當(dāng)然,我們可以通過 -Xmx 和 -Xms 來控制堆內(nèi)存的大小,其中, -Xmx 用于設(shè)置Java堆起始的大小, -Xms 用于設(shè)置Java堆可擴(kuò)展到最大值。
方法區(qū)
像Java堆一樣,方法區(qū)(Method Area)是各個 線程共享 的內(nèi)存區(qū)域,它的生命周期與虛擬機(jī)相同,即隨著虛擬機(jī)的啟動和結(jié)束而創(chuàng)建、銷毀。方法主要用于存放 已被虛擬機(jī)加載的類信息、常量、靜態(tài)變量以及即時編譯器(JIT)編譯后的代碼 等數(shù)據(jù),它的大小決定了系統(tǒng)能夠加載多少個類,如果定義的類太多,導(dǎo)致方法區(qū)拋出OutOfMemoryError異常。需要注意的是,對于JDK1.7來說,在HotSpot虛擬機(jī)中方法區(qū)可被理解為“永久區(qū)”,但是JDK1.8以后,方法區(qū)已被取消,替代的是 元數(shù)據(jù)區(qū) 。元數(shù)據(jù)區(qū)是一塊堆外的直接內(nèi)存,與永久區(qū)不同,如果不指定大小,默認(rèn)情況下在類加載時虛擬機(jī)會盡可能加載更多的類,直至系統(tǒng)內(nèi)存被消耗殆盡。當(dāng)然,我們可以使用參數(shù) -XX:MaxMetaspaceSzie 來指定元數(shù)據(jù)區(qū)的大小。
運(yùn)行時常量池用于存放編譯期生成的各種字面量和符號引用。
直接內(nèi)存
直接內(nèi)存(Direct Memory)不是虛擬機(jī)運(yùn)行時數(shù)據(jù)區(qū)的一部分,也不是Java虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域,它是在JDK1.4中新加入的NIO(New Input/Output)類,通過引入了一種基于通道與緩沖區(qū)的I/O方式,使用Native函數(shù)庫直接分配得到的堆外內(nèi)存。對于這部分內(nèi)存區(qū)域,主要通過存儲在Java堆中的 DirectByteBuffer 對象作為這塊內(nèi)存的引用進(jìn)行操作,直接內(nèi)存的存在避免了在Java堆和Native堆中來回復(fù)制數(shù)據(jù),從而在某些場景能夠顯著地提高性能。
本機(jī)直接內(nèi)存的分配不會受到Java堆大小的限制,但是仍然會受到本機(jī)總內(nèi)存( 包括RAM以及SWAP或者分頁文件 )大小以及處理器尋址空間的限制。如果申請分配的內(nèi)存總和(包括直接內(nèi)存)超過了物理內(nèi)存的限制,就會導(dǎo)致動態(tài)擴(kuò)展時出現(xiàn)OutOfMemoryError異常。
1.2 垃圾回收器與內(nèi)存分配策略
在上一節(jié)中我們詳細(xì)分析了JVM的運(yùn)行時內(nèi)存區(qū)域,了解到程序計數(shù)器、虛擬機(jī)棧、本地方法棧是線程的私有區(qū)域,當(dāng)線程結(jié)束時這部分所占內(nèi)存資源將會自動釋放,而線程的共享區(qū)域 Java堆 是存放所有對象實(shí)體的地方,因此是垃圾回收器回收( GC,Garbage Collection )的主要區(qū)域。(方法區(qū)也會有GC,但是一般我們討論的是Java堆)
1.2.1 如何判斷對象“已死”?
垃圾收集器在回收一個對象,第一件事就是確定這些對象之中有哪些是“存活”的,哪些已經(jīng)“死亡”,而垃圾回收器回收的就是那些已經(jīng)“死亡”的對象。如何確定對象是否已經(jīng)死亡呢?通常,我們可能會說當(dāng)一個對象沒被任何地方引用時,就認(rèn)為該對象已死。但是,這種表述貌似不夠準(zhǔn)確,因此,JVM規(guī)范中給出了兩種判斷對象是否死亡的方法,即 引用計數(shù)法 和 可達(dá)性分析 。
引用計數(shù)法
引用計數(shù)法實(shí)現(xiàn)比較簡單,它的實(shí)現(xiàn)原理: 給對象一個引用計數(shù)器,每當(dāng)一個地方引用它時,計數(shù)器就加1;當(dāng)引用失效時,計數(shù)器值就減1;任何時刻計數(shù)器為0的對象就會被認(rèn)為已經(jīng)死亡 。客觀地說,引用計數(shù)器法效率確實(shí)比較高,也容易實(shí)現(xiàn),但是它也有不足之處,就是 無用對象之間相互引用 的問題,這種情況的出現(xiàn)會導(dǎo)致相互引用的對象均無法被垃圾回收器回收。
可達(dá)性分析
為了解決 引用計數(shù)法 的無用對象循環(huán)引用導(dǎo)致無法被回收情況,JVM中又引入了 可達(dá)性分析 算法來判斷對象是否存活,這種算法也是普遍被應(yīng)用的方式。可達(dá)性分析基本思想:通過一系列被稱為“ GC Roots ”的對象作為起始點(diǎn),從這些節(jié)點(diǎn)開始向下搜索,搜索走過的路徑稱為 引用鏈 。當(dāng)一個對象到 GC Roots 沒有任何 引用鏈 相連接時,則證明此對象不可用,即被判斷可回收對象。可達(dá)性分析算法示意圖如下圖所示:
那么,哪些對象可以作為 “GC Roots”呢?
虛擬機(jī)棧中( 局部變量表 )引用的對象;
方法區(qū)中類靜態(tài)屬性引用的對象;
方法區(qū)中常量引用的對象;
本地方法棧中Native方法引用的對象;
1.2.2 垃圾收集算法
前面我們通過 引用計數(shù)法 或 可達(dá)性分析 找到了哪些對象是可以被回收的,本節(jié)將重點(diǎn)闡述JVM中的垃圾回收器是如何將這些不可用對象進(jìn)行回收,即垃圾收集算法,主要包括 標(biāo)記-清除算法 、 復(fù)制算法 、 標(biāo)記-整理 以及 分代收集 等。相關(guān)介紹如下:
標(biāo)記-清除算法
標(biāo)記-清理算法是最基礎(chǔ)的垃圾收集算法,它的實(shí)現(xiàn)分為兩個階段,即 “標(biāo)記” 和 “清除” ,其中,標(biāo)記的作用為通過引用計數(shù)法或可達(dá)性分析算法標(biāo)記出所有需要回收的對象;清除的作用為在標(biāo)記完成后統(tǒng)一回收所有被標(biāo)記的對象。這種算法比較簡單,但是缺陷也比較明顯,主要表現(xiàn)為兩個方面: 一是標(biāo)記和清理的效率比較低;二是標(biāo)記清理之后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太大可能會導(dǎo)致以后在程序運(yùn)行過程中需要分配較大對象時,無法找到足夠的連續(xù)內(nèi)存而不得不觸發(fā)另一次GC。 標(biāo)記-清除算法執(zhí)行過程如下圖所示:
復(fù)制算法
為了解決標(biāo)記-清理算法效率不高問題,人們提出了一種 復(fù)制算法 ,它的基本原理: 將可用內(nèi)存容量劃分為大小相等的兩塊,每次只使用其中的一塊。當(dāng)這一塊的內(nèi)存用完了,就將還存活的對象復(fù)制到另一塊上,然后再把已使用的內(nèi)存空間一次性清理掉。 這種方式實(shí)現(xiàn)簡單,運(yùn)行高效,且緩解了內(nèi)存碎片的問題,但是由于其只對整個半?yún)^(qū)進(jìn)行內(nèi)存分配、回收,從而導(dǎo)致可使用的內(nèi)存縮小為整個內(nèi)存的一半。復(fù)制算法執(zhí)行過程如下圖所示:
在HotSpot虛擬機(jī)中,整個內(nèi)存空間被分為一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden 空間和其中的一塊 Survivor 空間。當(dāng)回收時,將 Eden 和 Survivor 中還存活著的對象一次性復(fù)制到另一塊 Survivor 空間上,最后清理掉 Eden 和剛才用過的 Survivor 空間。HotSpot虛擬機(jī)默認(rèn) Eden 和 Surivor 的大小比例為 8:1:1 ,也就是每次新生代中可用內(nèi)存空間為整個內(nèi)存空間的90%,這就意味著有剩余的10%不可用。當(dāng) Survivor 空間不夠用時,就需要依賴其他內(nèi)存( 老年代 )進(jìn)行分擔(dān)擔(dān)保。
注 :新生代是指剛創(chuàng)建不久的對象;老年代指被多次GC仍然存活的對象。
標(biāo)記-整理算法
雖說復(fù)制算法有效地提高了標(biāo)記-清除算法效率不高問題,但是在對象存活率較高的情況下,就需要進(jìn)行較多的復(fù)制操作(復(fù)制對象),尤其是所有對象都100%存活的極端情況,這種復(fù)r制算法效率將會大大降低,因此,老年代區(qū)域通常不會直接選用這種算法。根據(jù)老年代的特點(diǎn),有人提出了 標(biāo)記-整理算法 ,該算法基于標(biāo)記-清除算法發(fā)展而來,其中,標(biāo)記同標(biāo)記-清除算法一致,整理為 將所有存活的對象都向一端移動,然后直接清理掉端邊界以外的內(nèi)存。 標(biāo)記-整理算法執(zhí)行過程如下圖所示:
分代收集算法
分代收集算法是目前大部分虛擬機(jī)的垃圾收集器采用的算法,這種算法的思想是 根據(jù)對象的存活周期的不同將Java堆內(nèi)存劃分為幾塊,即新生代區(qū)域和老年代區(qū)域,然后對不同的區(qū)域采用合適的算法。 由于新生代每次GC時都會有大批對象死去,只有少量的對象存活,因此通常選用 復(fù)制算法 ;而老年代中因為存活率高、沒有額外空間對它進(jìn)行分配擔(dān)保,就必須使用“ 標(biāo)記-清理 “或” 標(biāo)記-整理 ”算法進(jìn)行回收。分代收集算法模型如下圖所示:
哪些對象能夠進(jìn)入老年代?
大對象;
每次Eden進(jìn)行GC后對象年齡加1進(jìn)入Survivor,對象年齡達(dá)到15時進(jìn)入老年代;
如果Survivor空間中相同年齡所有對象大小的總和大于survivor空間的一半,年齡大于等于該年齡的對象就直接進(jìn)入老年代。
如果survivor空間不能容納Eden中存活的對象,由于擔(dān)保機(jī)制會進(jìn)入老年代。如果survivor中的對象存活很多,擔(dān)保失敗,那么會進(jìn)行一次Full GC。
什么是Minor GC、Major GC和Full GC?
Minor GC從新生代空間(Eden和Survivor區(qū)域)回收內(nèi)存;
Major GC是清理永久代;
Full GC是清理整個堆內(nèi)存空間,包括新生代和永久代。
1.2.3 內(nèi)存分配與回收策略
Java的自動內(nèi)存管理歸結(jié)于兩方面,即 為對象分配內(nèi)存 和 回收分配給對象的內(nèi)存 ,其中,在上一小節(jié)中我們詳細(xì)闡述了回收內(nèi)存的具體細(xì)節(jié),這里不再討論。對于對象的內(nèi)存分配,實(shí)際上就是在Java堆中為對象分配內(nèi)存,準(zhǔn)確來說是在新生代的 Eden 區(qū)上,如果啟動了本地線程分配緩沖,將按線程優(yōu)先在TLAB上分配,并且,少數(shù)情況下也可能會直接分配在老年代中。總之,JVM中對對象內(nèi)存的分配不是固定的模式,其細(xì)節(jié)取決于使用哪種垃圾收集器,和虛擬機(jī)中與內(nèi)存相關(guān)的參數(shù)設(shè)置。常見的內(nèi)存分配策略:
對象優(yōu)先在Eden分配
大多數(shù)情況下,對象在Java堆的新生代Eden區(qū)中分配,當(dāng)Eden區(qū)沒有足夠空間進(jìn)行分配時,虛擬機(jī)將發(fā)起一次 Minor GC 。
大對象直接進(jìn)入老年代
所謂的大對象是指需要大量連續(xù)內(nèi)存空間的Java對象,最典型的大對象就是那種 很長的字符串 以及 數(shù)組 。對于內(nèi)存分配來說,大對象也是一個很棘手的東西,尤其是“短命大對象”,經(jīng)常出現(xiàn)在內(nèi)存空間還較多的情況下,大對象直接導(dǎo)致提前出發(fā)垃圾收集器以獲取足夠的連續(xù)空間來“安置”它們。
虛擬機(jī)提供了一個 -XX:PretenureSizeThreshold 參數(shù),使得大于這個設(shè)置值得對象直接在老年代內(nèi)存區(qū)域分配,這樣做的目的在于避免在Eden區(qū)及兩個Survivor區(qū)之間發(fā)生大量的內(nèi)存復(fù)制。
長期存活的對象將進(jìn)入老年代
虛擬機(jī)給每個對象定義了一個對象年齡(Age)計數(shù)器,如果對象在Eden出生并經(jīng)過第一次Minor GC后仍然存活,并且能被Survivor容納的話,將被移動到Survivor空間中,并且對象年齡設(shè)為1.對象在Survivor區(qū)中每“熬過”一次Minor GC,年齡就增加1歲,當(dāng)它的年齡增加到一定程度( 默認(rèn)15歲 ),就將會被晉升到老年代中。虛擬機(jī)提供了一個 -XX:MaxTenuringThreshold 參數(shù)設(shè)置老年代年齡閾值。
虛擬機(jī)并不是永遠(yuǎn)地要求對象的年齡必須達(dá)到了 -XX:MaxTenuringThreshold 才能晉升到老年代,如果在Survivor空間中相同年齡所有對象的大小的總和大于Survivor空間的一半,年齡大于或者等于該年齡的對象就可以直接進(jìn)入老年代,無須等到 -XX:MaxTenuringThreshold 中要求的年齡。
空間分配擔(dān)保
在發(fā)生 Minor GC 之前,虛擬機(jī)會先檢查老年代最大可用的連續(xù)空間是否大于新生代所有對象總空間,如果這個條件成立,那么 Minor GC 可以確保是安全的;如果不成立,則虛擬機(jī)會查看 HandlePromotionFailure 設(shè)置值是否允許擔(dān)保失敗。如果允許,那么會繼續(xù)檢查老年代最大可用的連續(xù)空間是否大于歷次晉升到老年代對象的平均大小,如果大于,將嘗試著進(jìn)行一次 Minor GC ,盡管這次 Minor GC 是有風(fēng)險的;如果小于,或者 HandlePromotionFailure 設(shè)置不允許冒險,那么這時就需要進(jìn)行一次 Full GC。
1.3 JVM的類加載機(jī)制
類從被加載到虛擬機(jī)內(nèi)存中開始,到卸載出內(nèi)存為止,它的整個生命周期包括: 加載(Loading) 、 驗證(Verification) 、 準(zhǔn)備(Preparation) 、 解析(Resolution) 、 初始化(Initialization) 、 使用(Using) 和 卸載(Unloading) 7個階段,其中,驗證、準(zhǔn)備、解析3個部分統(tǒng)稱為連接(Linking)。類的生命周期如下圖所示:
在虛擬機(jī)中,我們常說的類的加載過程是指 加載(Loading) 、 驗證(Verification) 、 準(zhǔn)備(Preparation) 、 解析(Resolution) 、 初始化(Initialization) 這五個階段,它們的具體作用為:
加載
加載過程是將二進(jìn)制字節(jié)流(Class字節(jié)碼文件)通過 類加載器 加載到內(nèi)存并實(shí)例化Class對象的過程( 加載到方法區(qū)內(nèi) )。這個過程獨(dú)立于虛擬機(jī)之外,并且二進(jìn)制流可以從不同的環(huán)境內(nèi)獲取或者由其他文件生成。
驗證
驗證Class文件的字節(jié)流是否符合虛擬機(jī)的要求,以免造成虛擬機(jī)出現(xiàn)異常。包括: 文件格式驗證 、 元數(shù)據(jù)驗證 、 字節(jié)碼驗證 、 符號引用驗證 。
準(zhǔn)備
為靜態(tài)變量( 被final關(guān)鍵字修飾 )分配內(nèi)存空間、賦值和設(shè)置類變量初始化(自動初始化)。
解析
將常量池內(nèi)的 符號引用 替換為 直接引用 的過程。解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄以及調(diào)用點(diǎn)限定符7類符號引用進(jìn)行。
初始化
執(zhí)行類構(gòu)造器 《clinit》 方法的過程,變量的聲明初始化就在這個階段進(jìn)行。
虛擬機(jī)類加載的時機(jī)?
1)遇到new、getstatic、putstatic或者invokestatic 這四條字節(jié)碼指令的時候,且該類沒有進(jìn)行初始化則進(jìn)行該類的初始化;
2)使用反射機(jī)制的時候;
3)初始化類的父類;
4)初始化虛擬機(jī)要執(zhí)行主類;
5)使用動態(tài)語言特性的時候;
總之,當(dāng)對一個類進(jìn)行主動引用的時候就會進(jìn)行初始化操作,而進(jìn)行被動引用的時候便不會觸發(fā)類初始化操作,比如通過子類引用父類靜態(tài)字段時子類不會被初始化。
02
常見內(nèi)存泄漏與優(yōu)化
2.1 內(nèi)存泄漏
當(dāng)一個對象已經(jīng)不需要再使用本該被回收時,另外一個正在使用的對象持有它的引用從而導(dǎo)致它不能被垃圾收集器回收,結(jié)果它們就一直存在于內(nèi)存中(通常指Java堆內(nèi)存),占用有效空間,永遠(yuǎn)無法被刪除。隨著內(nèi)存不斷泄漏,堆中的可用空間就不斷變小,這意味著為了執(zhí)行常用的程序,垃圾清理需要啟動的次數(shù)越來越多,非常嚴(yán)重的話會直接造成應(yīng)用程序報OOM異常。
優(yōu)化/避免內(nèi)存泄漏原則:
涉及到使用Context時,盡量使用Application的Context;
對于非靜態(tài)內(nèi)部類、匿名內(nèi)部類,需將其獨(dú)立出來或者改為靜態(tài)類;
在靜態(tài)內(nèi)部類中持有外部類(非靜態(tài))的對象引用,使用弱引用來處理;
不再使用的資源(對象),需顯示釋放資源(對象置為null),如果是集合需要清空;
保持對對象生命周期的敏感,尤其注意單例、靜態(tài)對象、全局性集合等的生命周期;
2.2 常見內(nèi)存泄漏與優(yōu)化
(1) 單例造成的內(nèi)存泄漏
案例
/** 工具類,單例模式 * @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; }}/**使用單例模式時造成內(nèi)存泄漏 * * @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); }}
分析與優(yōu)化
在上述示例中,當(dāng)SingleActivity實(shí)例化Commontils對象完畢后,Commontils將持有SingleActivity對象的引用,而由于單例模式的靜態(tài)特性,Commontils對象的生命周期將于應(yīng)用進(jìn)程的一致,這就會導(dǎo)致在應(yīng)用未退出的情況下,如果SingleActivity對象已經(jīng)不再需要了,而Commontils對象該持有該對象的引用就會使得GC無法對其進(jìn)行正常回收,從而導(dǎo)致了內(nèi)存泄漏。優(yōu)化:對于需要傳入Context參數(shù)的情況,盡量使用Application的Context,因為它會伴隨著應(yīng)用進(jìn)程的存在而存在。
public class SingleActivity extends AppCompatActivity { private CommonUtils mUtils; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 造成內(nèi)存泄漏 //mUtils = CommonUtils.getInstance(this); mUtils = CommonUtils.getInstance(this.getApplicationContext()); }}
(2) Handler造成的內(nèi)存泄漏
案例
/** 使用Handler造成內(nèi)存泄漏 * @Auther: Jiangdg * @Date: 2019/10/8 17:55 * @Description: */public class HandlerActivity extends AppCompatActivity { // 匿名內(nèi)部類 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() { // 處理耗時任務(wù) // 。.. mUIHandler.sendEmptyMessage(0x00); } }); }}
分析與優(yōu)化
在剖析Handler消息機(jī)制原理一文中我們知道,在Android應(yīng)用啟動時,應(yīng)用程序的主線程會為其自動創(chuàng)建一個 Looper 對象和與之關(guān)聯(lián)的 MessageQueue ,當(dāng)主線程實(shí)例化一個 Handler 對象后,它就自動與主線程的 MessageQueue 關(guān)聯(lián)起來,所有發(fā)送到 MessageQueue 的 Message (消息)都會持有Handler的引用。由于 主線程的Looper對象會隨著應(yīng)用進(jìn)程一直存在的且Java類中的非靜態(tài)內(nèi)部類和匿名內(nèi)部類默認(rèn)持有外部類的引用 ,假如 HandlerActivity 提前出棧不使用了,但 MessageQueue 中仍然還有未處理的 Message , Looper 就會不斷地從 MessageQueue 取出消息交給 Handler 來處理,就會導(dǎo)致 Handler 對象一直持有 HandlerActivity 對象的引用,從而出現(xiàn) HandlerActivity 對象無法被GC正常回收,進(jìn)而造成內(nèi)存泄漏。優(yōu)化:將Handler類獨(dú)立出來,或者使用靜態(tài)內(nèi)部類,因為靜態(tài)內(nèi)部類不持有外部類的引用。
public class HandlerActivity extends AppCompatActivity {// 匿名內(nèi)部類默認(rèn)持有HandlerActivity的引用// 造成內(nèi)存泄漏// private Handler mUIHandler = new Handler() {// @Override// public void handleMessage(Message msg) {// super.handleMessage(msg);// }// }; // 優(yōu)化,使用靜態(tài)內(nèi)部類 // 假如要持有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中四種引用關(guān)系:
強(qiáng)引用
用來描述永遠(yuǎn)不會被垃圾收集器回收掉的對象,類似“Object obj = new Object”
軟引用
用來描述一些還有用但并非必須的對象,由 SoftReference 類實(shí)現(xiàn)。被軟引用關(guān)聯(lián)著的對象會在系統(tǒng)將要發(fā)生OOM之前,垃圾收集器才會回收掉這些對象。
弱引用
用來描述非必須的對象,比軟引用更弱一些,由 WeakReference 類實(shí)現(xiàn)。被弱引用的對象只能生產(chǎn)到下一次垃圾收集發(fā)生之前,無論當(dāng)前內(nèi)存是否足夠。
虛引用
最弱的引種引用關(guān)系,由 PhantomReference 類實(shí)現(xiàn)。一個對象是否有虛引用的存在,完全不會對其生存時間產(chǎn)生影響,也無法通過虛引用來獲取該對象實(shí)例。為一個對象設(shè)置虛引用關(guān)聯(lián)的唯一目的是能在這個 對象被垃圾收集器回收時收到一個系統(tǒng)通知 。
(3) 線程(非靜態(tài)內(nèi)部類或匿名內(nèi)部類)造成的內(nèi)存泄漏
案例
/** 使用線程造成的內(nèi)存泄漏 * @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(); // 開啟一個異步任務(wù) 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; } }}
分析與優(yōu)化
在之前的分析中可知,Java類中的非靜態(tài)內(nèi)部類和匿名內(nèi)部類默認(rèn)持有外部類的引用。對于上述示例中的 MyRunnable 和 MyAsyncTask 來說,它們是一個 非靜態(tài)內(nèi)部類 ,將默認(rèn)持有 ThreadActivity 對象的引用。假如子線程的任務(wù)在 ThreadActivity 銷毀之前還未完成,就會導(dǎo)致 ThreadActivity 無法被GC正常回收,造成內(nèi)存泄漏。優(yōu)化:將MyRunnable和MyAsyncTask獨(dú)立出來,或使用靜態(tài)內(nèi)部類,因為靜態(tài)內(nèi)部類不持有外部類的引用。
public class ThreadActivity extends AppCompatActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 開啟一個子線程 new Thread(new MyRunnable()).start(); // 開啟一個異步任務(wù) // 優(yōu)化:使用Application的Context new MyAsyncTask(this.getApplicationContext()).execute(); } // 優(yōu)化:使用靜態(tài)內(nèi)部類 static class MyRunnable implements Runnable { @Override public void run() { } } // 優(yōu)化:使用靜態(tài)內(nèi)部類 // 如果需要傳入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) 靜態(tài)實(shí)例造成的內(nèi)存泄漏
案例
/**非靜態(tài)內(nèi)部類創(chuàng)建靜態(tài)實(shí)例造成的內(nèi)存泄漏 * @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; } }}
分析與優(yōu)化
在上述案例中,演示了防止 StaticInstanceActivity 重建,比如橫豎屏切換,導(dǎo)致反復(fù)創(chuàng)建 SomeResources 實(shí)例的問題,這里使用了 static 修飾關(guān)鍵字將 SomeResources 實(shí)例聲明了靜態(tài)實(shí)例,以確保該實(shí)例始終存在的是同一個,且它的生命周期與應(yīng)用相同。然而,由于 SomeResources 是一個 非靜態(tài)內(nèi)部類 ,其對象默認(rèn)持有外部類 StaticInstanceActivity 的引用,就會導(dǎo) SomeResources 的對象一直持有該引用,造成內(nèi)存泄漏。優(yōu)化:使用單例模式實(shí)現(xiàn)SomeResources,或者將其改成靜態(tài)內(nèi)部類。如果需要傳入Context參數(shù),必須使用Application的Context。
public class StaticInstanceActivity extends AppCompatActivity { private static SomeResources mSomeResources; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); if(mSomeResources == null) { // 優(yōu)化,使用Application的Context mSomeResources = new SomeResources(this.getApplicationContext()); } } // 優(yōu)化:使用靜態(tài)內(nèi)部類 static class SomeResources { private Context mCtx; public SomeResources(Context context) { this.mCtx = context; } }}
(5) 資源未關(guān)閉或監(jiān)聽器未移除(注銷)引起的內(nèi)存泄露情況
在開發(fā)中,如果使用了 BraodcastReceiver , ContentObserver , File , Cursor , Stream , Bitmap 、 自定義屬性attributeattr 、傳感器等資源,應(yīng)該在Activity銷毀時及時關(guān)閉或者注銷,否則這些資源將不會被回收,從而造成內(nèi)存泄漏。比如:
// 使用傳感器等資源,需要注銷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種內(nèi)存泄漏外,還有包括 無限循環(huán)動畫 、 使用ListView 、 使用集合容器 以及 使用WebView 也會造成內(nèi)存泄漏,其中,無限循環(huán)動畫造成泄漏的原因是沒有再Activity的onDestory中停止動畫;使用ListView造成泄漏的原因是構(gòu)造Adapter時沒有使用緩存的convertView;使用集合容器造成泄漏的原因是在不使用相關(guān)對象時,沒有清理掉集合中存儲的對象引用。在優(yōu)化時,在退出程序之前將集合中的元素(引用)全部清理掉,再置為null;使用WebView造成泄漏的原因是在不使用WebView時沒有調(diào)用其destory方法來銷毀它,導(dǎo)致其長期占用內(nèi)存且不能被回收。在優(yōu)化時,可以為WebView開啟另外一個進(jìn)程,通過AIDL與主線程進(jìn)行通信,便于WebVIew所在的進(jìn)程可以根據(jù)業(yè)務(wù)需要選擇合適的時機(jī)進(jìn)行銷毀。
-
Android
+關(guān)注
關(guān)注
12文章
3945瀏覽量
127927 -
內(nèi)存
+關(guān)注
關(guān)注
8文章
3055瀏覽量
74327 -
JAVA
+關(guān)注
關(guān)注
19文章
2974瀏覽量
105139
發(fā)布評論請先 登錄
相關(guān)推薦
評論