對象的創建與內存分配


創建對象

JVM 收到一個 new 指令時,會檢查指令中的參數在常量池是否有這個符號的引用,還會檢查該類是否已經被加載過了,如果沒有的話則要進行一次類加載。

接着就是分配內存了,通常有兩種方式:

  • 指針碰撞
  • 空閑列表

使用指針碰撞的前提是堆內存是完全工整的,用過的內存和沒用的內存各在一邊每次分配的時候只需要將指針向空閑內存一方移動一段和內存大小相等區域即可。

當堆中已經使用的內存和未使用的內存互相交錯時,指針碰撞的方式就行不通了,這時就需要采用空閑列表的方式。虛擬機會維護一個空閑的列表,用於記錄哪些內存是可以進行分配的,分配時直接從可用內存中直接分配即可。

堆中的內存是否工整是有垃圾收集器來決定的,如果帶有壓縮功能的垃圾收集器就是采用指針碰撞的方式來進行內存分配的。

分配內存時也會出現並發問題:

這樣可以在創建對象的時候使用 CAS 這樣的樂觀鎖來保證。

也可以將內存分配安排在每個線程獨有的空間進行,每個線程首先在堆內存中分配一小塊內存,稱為本地分配緩存(TLAB : Thread Local Allocation Buffer)。

分配內存時,只需要在自己的分配緩存中分配即可,由於這個內存區域是線程私有的,所以不會出現並發問題。

可以使用 -XX:+/-UseTLAB 參數來設定 JVM 是否開啟 TLAB

內存分配之后需要對該對象進行設置,如對象頭。對象頭的一些應用可以查看 Synchronize 關鍵字原理

對象訪問

一個對象被創建之后自然是為了使用,在 Java 中是通過棧來引用堆內存中的對象來進行操作的。

對於我們常用的 HotSpot 虛擬機來說,這樣引用關系是通過直接指針來關聯的。

如圖:

這樣的好處就是:在 Java 里進行頻繁的對象訪問可以提升訪問速度(相對於使用句柄池來說)。

內存分配

Eden 區分配

簡單的來說對象都是在堆內存中分配的,往細一點看則是優先在 Eden 區分配。

這里就涉及到堆內存的划分了,為了方便垃圾回收,JVM 將對內存分為新生代和老年代。

而新生代中又會划分為 Eden 區,from Survivor、to Survivor 區。

其中 EdenSurvivor 區的比例默認是 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 相關的知識點,感興趣的朋友可以一起維護。

地址: https://github.com/crossoverJie/Java-Interview


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM