一文了解JVM(中)
HotSpot 虛擬機對象探秘
對象的創建
| Header | 解釋 |
|---|---|
| 使用 new 關鍵字 | 調用了構造函數 |
| 使用 Class 的 newInstance 方法 | 調用了構造函數 |
| 使用 Constructor 類的newInstance 方法 | 調用了構造函數 |
| 使用 clone 方法 | 沒有調用構造函數 |
| 使用反序列化 | 沒有調用構造函數 |
說到對象的創建,首先讓我們看看 Java 中提供的幾種對象創建方式:
下面是對象創建的主要流程:

虛擬機遇到一條 new 指令時,先檢查常量池是否已經加載相應的類,如果沒有,必須先執行相
應的類加載。類加載通過后,接下來分配內存。若 Java 堆中內存是絕對規整的,使用“指針碰
撞“方式分配內存;如果不是規整的,就從空閑列表中分配,叫做”空閑列表“方式。劃分內存
時還需要考慮一個問題--并發,也有兩種方式: CAS 同步處理,或者本地線程分配緩沖(Thread
LocalAllocation Buffer, TLAB)。然后內存空間初始化操作,接著是做一些必要的對象設置(元信
息、哈希碼…),最后執行 <init> 方法。
為對象分配內存
類加載完成后,接著會在 Java 堆中劃分一塊內存分配給對象。內存分配根據Java 堆是否規整,
有兩種方式:
-
指針碰撞:如果 Java 堆的內存是規整,即所有用過的內存放在一邊,而空閑的的放在另一
邊。分配內存時將位于中間的指針指示器向空閑的內存移動一段與對象大小相等的距離,這
樣便完成分配內存工作。
-
空閑列表:如果 Java 堆的內存不是規整的,則需要由虛擬機維護一個列表來記錄那些內存
是可用的,這樣在分配的時候可以從列表中查詢到足夠大的內存分配給對象,并在分配后更
新列表記錄。
選擇哪種分配方式是由 Java 堆是否規整來決定的,而 Java 堆是否規整又由所采用的垃圾收集器
是否帶有壓縮整理功能決定。

處理并發安全問題
對象的創建在虛擬機中是一個非常頻繁的行為,哪怕只是修改一個指針所指向的位置,在并發情
況下也是不安全的,可能出現正在給對象 A 分配內存,指針還沒來得及修改,對象 B 又同時使
用了原來的指針來分配內存的情況。解決這個問題有兩種方案:
-
對分配內存空間的動作進行同步處理(采用 CAS + 失敗重試來保障更新操作的原子性);
-
把內存分配的動作按照線程劃分在不同的空間之中進行,即每個線程在Java 堆中預先分配
一小塊內存,稱為本地線程分配緩沖(Thread LocalAllocation Buffer, TLAB)。哪個線程
要分配內存,就在哪個線程的 TLAB 上分配。只有 TLAB 用完并分配新的 TLAB 時,才需要
同步鎖。通過-XX:+/-UserTLAB 參數來設定虛擬機是否使用 TLAB。

對象的訪問定位
Java 程序需要通過 JVM 棧上的引用訪問堆中的具體對象。對象的訪問方式取決于 JVM 虛擬機
的實現。目前主流的訪問方式有** 句柄** 和 直接指針 兩種方式。
-
指針: 指向對象,代表一個對象在內存中的起始地址。
-
句柄: 可以理解為指向指針的指針,維護著對象的指針。句柄不直接指向對象,而是指向
對象的指針(句柄不發生變化,指向固定內存地址),再由對象的指針指向對象的真實內存
地址。
句柄訪問
Java 堆中劃分出一塊內存來作為句柄池,引用中存儲對象的句柄地址,而句柄中包含了對象實例數據與對象類型數據各自的具體地址信息,具體構造如下圖所示:

優勢:引用中存儲的是穩定的句柄地址,在對象被移動(垃圾收集時移動對象是非常普遍
的行為)時只會改變句柄中的實例數據指針,而引用本身不需要修改。
直接指針
如果使用直接指針訪問,引用 中存儲的直接就是對象地址,那么 Java 堆對象內部的布局中就必
須考慮如何放置訪問類型數據的相關信息。

優勢:速度更快,節省了一次指針定位的時間開銷。由于對象的訪問在 Java 中非常頻繁,因此
這類開銷積少成多后也是非常可觀的執行成本。 HotSpot 中采用的就是這種方式。
64 位 JVM 中,int 的長度是多數?
Java 中,int 類型變量的長度是一個固定值,與平臺無關,都是 32 位。意思就是說,在 32 位
和 64 位 的 Java 虛擬機中,int 類型的長度是相同的。
32 位和 64 位的 JVM,int 類型變量的長度是多數?
32 位和 64 位的 JVM 中,int 類型變量的長度是相同的,都是 32 位或者4 個字節。
怎樣通過 Java 程序來判斷 JVM 是 32 位 還是 64 位?
你可以檢查某些系統屬性如 sun.arch.data.model 或 os.arch 來獲取該信息。
32 位 JVM 和 64 位 JVM 的最大堆內存分別是多數?
理論上說上 32 位的 JVM 堆內存可以到達 2^32, 即 4GB,但實際上會比這個小很多。不同操
作系統之間不同,如 Windows 系統大約 1.5GB,Solaris大約 3GB。64 位 JVM 允許指定最大
的堆內存,理論上可以達到 2^64,這是一個非常大的數字,實際上你可以指定堆內存大小到
100GB。甚至有的JVM,如 Azul,堆內存到 1000G 都是可能的。
JRE、JDK、JVM 及 JIT 之間有什么不同?
-
JRE 代表 Java 運行時(Java run-time),是運行 Java 引用所必須的。JDK 代表 Java 開
發工具(Java development kit),是 Java 程序的開發工具,如 Java 編譯器,它也包含
JRE。
-
JVM 代表 Java 虛擬機(Java virtual machine),它的責任是運行 Java 應用。
-
JIT 代表即時編譯(Just In Time compilation),當代碼執行的次數超過一定的閾值時,
會將 Java 字節碼轉換為本地代碼,如,主要的熱點代碼會被準換為本地代碼,這樣有利大
幅度提高Java 應用的性能。
內存溢出異常
Java 會存在內存泄漏嗎?
內存泄漏是指不再被使用的對象或者變量一直被占據在內存中。理論上來說,**Java **是有 GC
垃圾回收機制的,也就是說,不再被使用的對象,會被 GC 自動回收掉,自動從內存中清除。
但是,即使這樣,Java 也還是存在著內存泄漏的情況,java 導致內存泄露的原因很明確:長
生命周期的對象持有短生命周期對象的引用就很可能發生內存泄露,盡管短生命周期對象已經不
再需要,但是因為長生命周期對象持有它的引用而導致不能被回收,這就是** java** 中內存泄露
的發生場景。
什么情況下會發生棧內存溢出
-
棧是線程私有的,他的生命周期與線程相同,每個方法在執行的時候都會創建一個棧幀,用
來存儲局部變量表,操作數棧,動態鏈接,方法出口等信息。局部變量表又包含基本數據類
型,對象引用類型.
-
如果線程請求的棧深度大于虛擬機所允許的最大深度,將拋出StackOverflowError 異常,
方法遞歸調用產生這種結果。
-
如果 Java 虛擬機棧可以動態擴展,并且擴展的動作已經嘗試過,但是無法申請到足夠的內
存去完成擴展,或者在新建立線程的時候沒有足夠的內存去創建對應的虛擬機棧,那么
Java 虛擬機將拋出一個 OutOfMemory 異常。(線程啟動過多)
-
參數 -Xss 去調整 JVM 棧的大小

浙公網安備 33010602011771號