建立物件
當 JVM
收到一個 new
指令時,會檢查指令中的引數在常量池是否有這個符號的引用,還會檢查該類是否已經被載入過了,如果沒有的話則要進行一次類載入。
接著就是分配記憶體了,通常有兩種方式:
- 指標碰撞
- 空閒列表
使用指標碰撞的前提是堆記憶體是完全工整的,用過的記憶體和沒用的記憶體各在一邊每次分配的時候只需要將指標向空閒記憶體一方移動一段和記憶體大小相等區域即可。
當堆中已經使用的記憶體和未使用的記憶體互相交錯時,指標碰撞的方式就行不通了,這時就需要採用空閒列表的方式。虛擬機器會維護一個空閒的列表,用於記錄哪些記憶體是可以進行分配的,分配時直接從可用記憶體中直接分配即可。
堆中的記憶體是否工整是有垃圾收集器來決定的,如果帶有壓縮功能的垃圾收集器就是採用指標碰撞的方式來進行記憶體分配的。
分配記憶體時也會出現併發問題:
這樣可以在建立物件的時候使用 CAS
這樣的樂觀鎖來保證。
也可以將記憶體分配安排在每個執行緒獨有的空間進行,每個執行緒首先在堆記憶體中分配一小塊記憶體,稱為本地分配快取(TLAB : Thread Local Allocation Buffer
)。
分配記憶體時,只需要在自己的分配快取中分配即可,由於這個記憶體區域是執行緒私有的,所以不會出現併發問題。
可以使用 -XX:+/-UseTLAB
引數來設定 JVM 是否開啟 TLAB
。
記憶體分配之後需要對該物件進行設定,如物件頭。物件頭的一些應用可以檢視 Synchronize 關鍵字原理。
物件訪問
一個物件被建立之後自然是為了使用,在 Java 中是通過棧來引用堆記憶體中的物件來進行操作的。
對於我們常用的 HotSpot
虛擬機器來說,這樣引用關係是通過直接指標來關聯的。
如圖:
這樣的好處就是:在 Java 裡進行頻繁的物件訪問可以提升訪問速度(相對於使用控制程式碼池來說)。
記憶體分配
Eden 區分配
簡單的來說物件都是在堆記憶體中分配的,往細一點看則是優先在 Eden
區分配。
這裡就涉及到堆記憶體的劃分了,為了方便垃圾回收,JVM 將對記憶體分為新生代和老年代。
而新生代中又會劃分為 Eden
區,from Survivor、to Survivor
區。
其中 Eden
和 Survivor
區的比例預設是 8:1:1
,當然也支援引數調整 -XX:SurvivorRatio=8
。
當在 Eden
區分配記憶體不足時,則會發生 minorGC
,由於 Java
物件多數是朝生夕滅的特性,所以 minorGC
通常會比較頻繁,效率也比較高。
當發生 minorGC
時,JVM 會根據複製演算法將存活的物件拷貝到另一個未使用的 Survivor
區,如果 Survivor
區記憶體不足時,則會使用分配擔保策略將物件移動到老年代中。
談到 minorGC
時,就不得不提到 fullGC(majorGC)
,這是指發生在老年代的 GC
,不論是效率還是速度都比 minorGC
慢的多,回收時還會發生 stop the world
使程式發生停頓,所以應當儘量避免發生 fullGC
。
老年代分配
也有一些情況會導致物件直接在老年代分配,比如當分配一個大物件時(大的陣列,很長的字串),由於 Eden
區沒有足夠大的連續空間來分配時,會導致提前觸發一次 GC
,所以儘量別頻繁的建立大物件。
因此 JVM
會根據一個閾值來判斷大於該閾值物件直接分配到老年代,這樣可以避免在新生代頻繁的發生 GC
。
對於一些在新生代的老物件 JVM
也會根據某種機制移動到老年代中。
JVM 是根據記錄物件年齡的方式來判斷該物件是否應該移動到老年代,根據新生代的複製演算法,當一個物件被移動到 Survivor
區之後 JVM 就給該物件的年齡記為1,每當熬過一次 minorGC
後物件的年齡就 +1 ,直到達到閾值(預設為15)就移動到老年代中。
可以使用
-XX:MaxTenuringThreshold=15
來配置這個閾值。
總結
雖說這些內容略顯枯燥,但當應用發生不正常的 GC
時,可以方便更快的定位問題。
號外
最近在總結一些 Java 相關的知識點,感興趣的朋友可以一起維護。