04.18 百度資深架構師深度鑽研JVM的個人理解以及整理

java虛擬機所管理的內存區域劃分為堆,方法區,虛擬機棧,本地方法棧,程序計數器。

每個虛擬機棧中有一個私有的程序計數器,程序計數器佔用很小的一塊內存,在執行一個java方法時,記錄正在執行的虛擬機字節碼的地址。虛擬機棧中有一個棧幀,用於存放局部變量表,操作數棧,動態鏈表,方法出口等。

1. Java內存區域與內存溢出異常

1.1. 運行時數據區域

(1) 程序計數器:程序計數器用來記錄當前線程執行的字節碼行號。程序計數器是線程私有的,因為CPU通過時間輪轉來為線程服務,為了線程切換後能夠恢復的正確的位置,在每一個線程都保存一個程序計數器。如果執行的是本地方法則,程序計數器值為空。

(2) Java虛擬機棧:Java虛擬機棧是Java方法的內存模型,每一個方法被執行的過程都會創建一個棧幀用來存儲局部變量表、操作棧、動態鏈接、方法出口等信息。局部變量表所需的內存空間是在編譯時期分配的。Java虛擬機棧是線程私有的。如果申請的棧深度超過了虛擬機允許的最大棧深度會拋出Stack OverflowError。如果允許擴展時,當擴展時無法申請足夠的內存會拋出OutOfMemoryError。使用-Xss來設置棧大小。

(3) 本地方法棧:與Java虛擬機棧相似,只不過是為本地方法服務的。

(4) 堆:Java堆是內存中線程共享的一塊區域,在Java虛擬機啟動的時候創建的。Java中的所有對象和數組都要在堆上分配。堆內存是GC的主要區域。由於GC是按代回收,所以堆還可以被細分為新生代和老年代。新生代又可以被細分為Eden,FromSurvivor和ToSurvivor區域。使用-Xms和-Xmx來設置堆的下限和上限。如果堆內存中沒有足夠的空間完成實例分配,並且也沒法擴展就會拋出OutOfMemoryError異常。

(5) 方法區:是線程共享的一塊區域。主要用來存儲已被加載的類信息,常量,靜態變量,即時編譯器編譯的代碼。方法區一般被成為永久代,在這個區域也會進行垃圾回收,主要回收的是常量池,和對類型的卸載。方法區會出現OutOfMemoryError異常。使用-XX:PermSize和-XX:MaxPermSize設置方法區的大小。

(6) 運行時常量池:是方法區中的一部分,已被加載的類信息包括,類的版本,字段,方法,接口等描述信息還有常量池。常量池用於存儲編譯時期生成的字面量,符號引用,在類加載後存放到方法區的常量池中。運行時也可以將新的常量加入到常量池中,String類的intern()方法。

2. 垃圾收集器與內存分配策略

2.1. 如何判斷對象已死?

2.1.1. 兩種判斷方法

(1) 引用計數算法:在對象中保存一個引用計數器,如果該對象在一個地方被引用,則引用計數器值加1,如果有一個地方的引用失效則計數器減1。在任意時刻計數器的值為0則表示對象已死。優點是簡單,速度快。缺點是:循環引用問題。

(2) 根搜索算法:當一個對象到GC Roots沒有任何引用鏈,則表示該對象已死。

2.1.2. 哪些對象可以當做GC Roots

(1) Java虛擬機棧中的引用的對象。

(2) 本地方法棧中的引用的對象。

(3) 方法區中常量引用的對象。

(4) 方法區中靜態變量引用的對象。

2.1.3. Java中的四種引用

(1) 強引用:例如Object obj = new Object();只要強引用還在,則對象一定不會被回收

(2) 軟引用:當將要發生內存溢出時,GC則將這些對象列入垃圾回收的範圍。如果回收後仍然內存空間不足,則拋出OutOfMemoryError異常。

(3) 弱引用:弱引用關聯的對象只能活到下一次垃圾回收之前。

(4) 虛引用:虛引用完全不影響對象的生存週期,只是在垃圾回收時收到一個系統通知。

2.1.4. 對象的二次標記

當對象到GC Roots不可達時,並不一定被回收。還回經歷兩次標記的過程。

當對象到GC Roots不可達時,它會被第一次標記,並被篩選。篩選的條件是是否有必要執行finalize(), 如果對象沒有覆finalize()或者已經被JVM執行了finalize(),則認為沒有必要執行(直接被回收)。如果認為有必要執行,則將對象存放到一個F-Queue隊列中,JVM會自動創建一個低級線程Finalizer用來執行finalize()。這裡執行只是觸發該方法,並不會等待該方法執行完成。執行finalize()方法是對象逃脫別回收的最後一次機會。GC 會對F-Queue中的對象進行二次標記,如果在期間被GC Roots引用鏈上的對象重新連接,則不會被回收。

2.2. 方法區的回收

方法區主要回收常量池和無用的類。

2.2.1. 如何判斷一個類是無用的類

要滿足下面三個條件:

(1) 該類的所有對象都已經被回收,Java堆中沒有該類的任何實例。

(2) 該類的類加載器已經被回收。

(3) 該類的java.lang.Class對象沒有在任何地方被使用。沒有在任何地方通過反射訪問該類的方法。

2.3. 垃圾回收算法

(1) 標記清除算法:第一步將所有要回收的對象進行標記,第二步回收掉所有被標記的對象。優點:簡單;缺點:標記和清除效率都較低,並且會使得內存中出現很對碎片。

(2) 複製算法:將內存區域分成一個較大的Eden區域,兩個較小的Survivor區域。分配空間時,每次使用Eden區域和其中一塊Survivor區域。在垃圾回收時,將Eden區域和Survivor區域存活的對象複製到另一塊Survivor中。

(3) 標記-整理算法:第一步對所有要回收對對象進行標記,第二步將存活的對象移到一端,將邊界以外的所有對象回收。

(4) 分代收集算法。按照對象的生存週期將內存分成幾個區域。在每個區域使用不同的算法。一般把Java堆分成新生代和老年代,對新生代使用複製算法,對老年代使用標記清除或者標記整理算法。

2.4. 垃圾收集器

百度資深架構師深度鑽研JVM的個人理解以及整理

(1) Serial收集器:是單線程的,使用的是複製算法。使用一條線程去垃圾回收時,必須要停止其他工作線程。

(2) ParNew收集器:是Serial的多線程版本。

(3) Parallel Scavenge:目標主要是用來控制CPU的吞吐量。使用的是複製算法

(4) Serial Old收集器:Serial的老年版本。使用的是標記整理算法。

(5) Parallel Old收集器:Parallel Scavenge的老年版本。使用的是標記整理算法。

(6) CMS收集器:以獲取最短停頓時間為目標的。使用的是標記清除算法。在標記和清除階段使用的是併發操作。

(7) G1收集器:將Java堆分成若干個大小固定的區域,使用的是標記清除算法。跟蹤沒一個區域的垃圾堆積程度,並維持一個優先級隊列,根據允許的收集時間,選擇垃圾堆積最多的區域進行回收。

2.5. 內存分配與回收策略

最後如果需要了解更多關於JVM的知識點可以私聊回覆我“JVM”即可獲得一份JVM資料大全。

百度資深架構師深度鑽研JVM的個人理解以及整理

替換高清大圖

2.5.1. 對象優先在Eden區域分配

對象優先在Eden區域分配,如果Eden區域沒有足夠的空間分配,則虛擬機發起一次Minor GC。-XX:SurvivorRatio用來設置Eden區域和Survivor區域的大小比值。

Eden區域空間不足,發起一次Minor GC,將Eden區域和Survivor中存活的對象複製到另一個Survivor中,如果Survivor無法容納所有存活的對象,則根據分配擔保機制,將其轉移到老年代。

2.5.2. 大對象直接進入老年代

大對象指需要大量連續內存空間的對象,例如大數組。使用-XX:PretenureSizeThreshold來設置閾值,如果對象答案與這個閾值則直接進入老年代。這樣做的目的避免對象在Eden區域以及兩個Survivor區域發生大量拷貝。

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

Java虛擬機給每個對象定義一個Age計數器,如果對象在Eden區域出生,經過一次GC仍然存活,將其複製到Survivor區域,如果能被容納則將Age加1。當Age的值大於等於MaxTenuringThreshold時進入老年代。閾值設置使用-XX:MaxTenuringThreshold。

2.5.4. 動態對象年齡判定

對應並不總是等到年齡大於maxTenuringThreshold才進入老年代。如果Survivor中相同年齡的對象的總大小大於等於Survivor空間的一半,則將所有大於等於該年齡的對象移入到老年代。

2.5.5. 空間分配擔保

在發生minor GC之前,Java虛擬機會檢測之前每次進入老年代的平均大小是否大於老年代的剩餘大小。如果大於老年代的大小,則將進行一次Full GC。如果不大於,則查看HandlePromotionFailure是否設置為true, 若果是則進行一次minor GC. 如果為False則進行一次Full GC。

3.個人心得總結

常量池用於存放編譯期期間生成的各種字面量和符號,在類加載後進入方法區的運行時常量池。

Java語言並不要求常量一定在編譯期才能產生。並非一定是在class編譯期中預置的才能進去方法區中的運行時常量池,在運行期間也可以將常量放入。運用的最多的就是String類的intern()方法。運行時常量池是方法區的一部分,當無法申請的內存是會拋出OOS異常。

對象的創建,當遇到一個new指令時,會先去檢查這個指令的參數在常量池中是否能定位到一個類的符號引用,在檢查這個符號引用代表的類是否被加載,解析和初始化夠。在類加載通過後,對象所需要的大小可以完全確定。在Java堆中,擁有一個指針座位分界點的指示器,當需要分配內存時,會指向未被分配內存的區域,將其挪動一段與對象大小相等的距離,稱為指針碰撞。如果內存不是規整的,會有一個列表,上面記錄哪些區域是可用的,當需要是劃分列表中一塊足夠大的空間分配給對象,然後更新列表上的記錄,稱為空閒列表。Java堆是否規整由GC收集器是否帶有壓縮整理功能決定。Serial等帶有Compact過程的採用指針碰撞,CMS這種基於Mark-Sweep算法的收集器,採用空閒列表。

對象在內存中存儲的佈局可以分為3部分:對象頭,實例數據,對齊填充。

對象頭包含兩部分,第一部分是自己運行時的數據,包裹哈希嗎,GC分代年齡,鎖狀態的標誌,線程自帶的鎖,偏向線程ID,偏向時間戳等。另一部分是類型指針,指向它的類元數據。虛擬機可以通過這個指針來確定它是哪個類的實例。如果是Java數組的話,在對象頭中還必須有一塊用於用於記錄數組長度的數據,虛擬機可以通過普通java對象的元數據確定Java對象的大小,但是從數組的元數據中無法確定數組的大小。

對齊填充並不是必然存在的,起到一個佔位符的作用,HotSpot VM的自動內存管理系統要求對象的起始地址必須是8字節的倍數,當對象的實例數據沒有對齊的時候,就需要對齊填充來補全。

實例數據是對象真正存儲的有效信息,也是在程序中所定義的各種類型的字段內容。存儲順序受到虛擬機分配策略參數和字段在java源碼中定義順序的影響。相同寬度的字段總是被分配到一起。滿足這個條件下,父類中定義的方法會出現在子類之前。如果CompactFields為true,子類中較窄的變量也可能會插入到父類變量的空隙之中。

在jdk1.6之前,StringBuilder會在java堆中創建一個實例,調用String.intern()會把這個實例複製在方法區,所以他們不是一個相同的引用,在jdk1.7後,不會再實現複製,而是在方法區中記錄首次出現的實例引用。


分享到:


相關文章: