標籤:min 記憶體回收演算法 對象 分析法 位元組碼 之間 字串 引用計數 方法
1.JVM管理的記憶體包含所示的幾個運行時資料區域,其中方法區和堆為線程共用的資料區域,程式計數器,虛擬機器棧以及本地方法棧為線程私人的資料區域。
程式計數器:可以看做是當前線程所執行的位元組碼的行號指標,告訴位元組碼解譯器該讀取哪條指令
虛擬機器棧:生命週期和線程相同,每個方法在執行的同時都會建立一個棧幀,用於儲存局部變數表,運算元棧,動態連結,方法出口等資訊,每一個方法從調用到完成的過程就對應了一個棧幀在虛擬機器中入棧到出棧的過程。棧中存放了編譯器可知的各種基礎資料型別 (Elementary Data Type)和對象引用。
本地方法棧:與虛擬機器棧十分類似,區別在於本地方法棧是為Native方法服務。
堆:堆中存放對象執行個體。幾乎 所有的對象執行個體都在這裡分配。堆是記憶體回收行程管理的主要區域。從記憶體回收的角度來看,由於現有記憶體回收行程基本都採用分代收集演算法,所以堆可以細分為新生代和老年代,再細緻一點可以分為:Eden空間,From Survivor 空間,To Survivor空間。
方法區:用於儲存被虛擬機器載入的類資訊,常量,靜態變數,即時編譯器編譯後的代碼等資料。(HotSpot虛擬機器的將GC分代收集擴充至方法區,使用永久代來實現方法區。因此存在把方法區看成永久代)。
運行時常量池:方法區的一部分,用於存放編譯器產生的各種字面量和符號引用。
2.對於字串,其對象的引用儲存在棧中,如果是編譯期已經建立好(直接用雙引號定義的)的就儲存在常量池中,如果是運行期(new出來的)才能確定的就儲存在堆中 。對於equals相等的字串,在常量池中永遠只有一份,在堆中有多份。對於通過new產生一個字串時,會先去常量池中尋找是否已經有了此對象,如果沒有則在常量池中建立一個此字串對象,然後堆中再建立一個常量池中此對象的拷貝對象。例如:String s = new String(“xyz”);如果常量池中原來沒有”xyz”,就會產生兩個對象。
3.如何判斷對象已死:
(1)引用計數法:給每個對象添加一個引用計數器,有引用計數器+1,引用失效計數器-1,計數器為0則對象不可用。缺點是無法解決循環參考的問題。
(2)可達性分析法:當一個對象到GC Roots沒有任何引用鏈相連(類似樹的葉子節點到根節點之間中斷連線),則此對象不可用。
4.即使在可達性分析演算法中不可達的對象也不是非死不可的,這時候他們暫時處於‘緩刑‘階段,要真正宣告一個對象的死亡至少要經曆兩次標記過程:
如果對象在可達性分析時發現不可達,將會被第一次標記並且進行一次篩選,篩選的條件是此對象是否有必要執行finalize方法(當對象沒有重寫父類的 finalize 方法或者 finalize 方法已經被調用過,虛擬機器就認為沒必要執行finalize方法,值得注意的是,對象的finalize方法只會被調用一次),如果這個對象被判定為有必要執行 finalize 方法,那麼這個對象將會被放置到一個叫做F-Queue 的隊列中,並在稍後由一個虛擬機器自動建立的,低優先順序的finalizer線程去執行它(虛擬機器出發finalize方法,但不一定會等待它執行完成)。稍後GC將對 F-Queue 中的對象進行第二次小規模的標記,如果對象仍被標記為回收(如果對象在第二次標記之前與其他可達對象建立了參考關聯性,則可以‘逃過一劫‘),則該對象基本上就真的被回收了。
5.記憶體回收一般發生在堆上,然而方法區也可能發生記憶體回收。在堆中,尤其是新生代中,常規應用進行一次垃圾收集一般可以回收 70%~ 95%的空間,而永久代垃圾收集效率遠低於此。永久代的垃圾收集主要收集主要回收兩部分內容:廢棄常量或者無用的類。判斷常量不可用較簡單(沒有任何對象引用常量池中的這個常量),而判定一個類是無用的類則條件嚴苛的多:
(1).該類的所有執行個體均被回收
(2).載入該類的 ClassLoader 已經被回收
(1).該類對應的 java.lang.Class 對象沒有在任何地方被使用,無法再任何地方通過反射訪問該類的方法。
6.記憶體回收演算法:
標記-清除演算法:分為標記,清除兩個階段,首先標記出所有需要回收的對象,標記完成後統一回收所有被標記的對象。此演算法有兩個不足:一是兩個過程的效率都不高,二是標記清除後會產生大量不連續的記憶體片段(當需要分配大對象時無法找到足夠的連續記憶體而不得不提前觸發另一次記憶體回收動作)。
複製演算法:將可用記憶體分為大小相等的兩塊,每次只使用其中的一塊,當一塊的記憶體用完了就把存活的對象複製到另一塊,然後將已使用的記憶體空間一次清理掉。這種演算法簡單高效,但是不足在於將記憶體縮小了一般。
標記-整理演算法:標記過程和標記-清除演算法一樣,但是後續不是直接清除記憶體,而是讓存活的對象都向一邊移動,然後清除掉邊界意外的記憶體。
分代收集演算法:根據對象存活周期的不同將記憶體劃分為幾塊,一般是把 Java 堆分為新生代和老年代。現在的商業虛擬機器普遍使用複製演算法回收新生代,由於新生代的對象絕大部分都是“朝生夕死”的,所以並不需要按1:1的比例劃分記憶體空間,而是分為較大的Eden區和兩個較小的Survivor區,每次使用Eden和一塊Survivor,GC時將存活的對象複製到另外一塊Survivor(From Survivor),最後清理掉剛才使用的Eden和Survivor(To Survivor).HotSpot虛擬機器預設Eden和Survivor比例為8:1,也就是每次新生代中可用的記憶體空間為整個新生代的90%(80%+10%)。當Survivor(To Survivor)記憶體不夠用時,需要依賴其他記憶體進行分配擔保(老年代)。複製演算法在對象存活率較高時效率低下而且如果不想浪費50%記憶體還需要額外的空間驚醒分配擔保,因此不適合老年代這種存在大量存活對象的地區,所以一般使用標記-整理演算法回收老年代。
7.記憶體配置與回收策略:
1.Java 體系中所提倡的自動記憶體管理最終可以歸結為兩個問題:給對象分配記憶體以及回收分配給對象的記憶體。對象主要分配在新生代的Eden區上,少數情況下也可能直接分配在老年代中。
(1)大對數情況下,對象在新生代Eden區中分配,當Eden區沒有足夠的空間進行分配時,虛擬機器將會發起一次 Minor GC.
(2).大對象直接進入老年代,所謂的大對象是指需要大量連續記憶體空間的對象,最典型的大對象就是那種很長的字串以及數組
(3)長期存活的對象將進入老年代。如果對象在Eden出生經過第一次 Minor GC 後仍然存活,並且能被Survivor容納的話,將被移動到Survivor空間中,並且對象年齡設為1,對象每熬過一次 Minor GC,年齡就增加1歲,當他的年齡增加到一定程度(預設15歲)就會晉陞到老年代中。虛擬機器並不是永遠要求對象的年齡達到 Max Tenuring Threshhold 才能晉陞老年代,如果在Survivor空間中相同年齡所有對象的大小的總和等於Survivor空間的一般,年齡大於等於該年齡的對象就可以直接進入老年代,無需等到Max Tenuring Threshhold 中要求的年齡。
(4)在發生 Minor GC 之前,虛擬機器會先檢查老年代最帶可用的連續空間是是否大於新生代所有對象總空間,如果這個條件成立,那麼 Minor GC 可以確保是安全的。
Java記憶體配置與記憶體回收