JVM 進階知識
Java的跨平臺性
Java具有跨平臺性,無疑是JVM底層翻譯出來的彙編指令的不同,Unix和Window系統的彙編指令是不同的,Windows派系採用的是Intel彙編,Unix派系採用的是AT&T彙編。無論在哪個平臺上編寫的Java檔案,編譯後的class檔案,放在哪個平臺上都可以執行,只要下載平臺相對應的JDK,都可以執行這個class檔案,從而翻譯出跟平臺相對應的彙編指令
class檔案、class content、class物件、物件四個基礎概念
class檔案: 這個應該不用多說,就是java檔案編譯後的class檔案
class content: 首先來看一張類載入的圖
我們的class檔案是存在在硬碟上的,需要將檔案記憶體讀取到記憶體中,通過類載入將class檔案內容載入到記憶體中,則會形成一塊class content表示class 檔案內容
class物件:用過反射的網友應該清楚Class<T>這個物件,這就是class物件,類載入器通過解析class content內容則會在方法區(永久代)中形成Class<T>物件,像這個class物件的欄位,方法的描述資訊都會放在方法區中
物件:物件則就是指我們new出來生成的物件
JDK1.8後元空間替代永久代
1. 元空間使用的是直接記憶體,作業系統的記憶體。永久代會產生OOM
2. 硬體的發展,之前的計算機硬體比較落後,沒多少記憶體使用。如果還讓JVM使用直接記憶體的話,會導致沒有多少記憶體供其它程式使用。隨著硬體的發展,現在的機器能夠達到32G 64G這樣的,有足夠的記憶體使用。32位的機器最大的記憶體就是4G,64位的機器最大的記憶體可以達到2的48次方,256T。64bit = 16(保留位) + 48
元空間預設大小
最小20.75M,最大256T,相當於無限大。元空間調優技巧:最小最大設定成一樣大,防止忽大忽小,動態擴充套件。一般設定成實體記憶體的1 / 32。
虛擬機器棧
區域性變量表:存放方法中宣告的區域性變數
運算元棧:變數需要進行運算所要用到的運算元棧
動態連結:指向方法在方法區的記憶體地址,也就是指向下圖中的main方法物件或者add方法物件的記憶體地址
返回地址:恢復現場。如果main方法呼叫了add方法,add方法執行完後,需要返回到main方法前面執行到的指令,則add方法中的返回地址則記錄了main方法中程式計數器所執行到位元組碼指令
一個方法執行完JVM需要做的事情
1. 恢復區域性變量表指標
2. 恢復運算元棧的指標
3. 恢復程式計數器
4. 如果方法有返回地址,則需要返回
5. 清理棧幀(程式計數器做的)
new操作不是原子操作
右邊的圖中我們可以看到new Test()所生成的位元組碼指令,並不是只有一條指令,而是由四條指令組成的,所以它不是一個原子操作。
0 new
3 dup
4 invokespecial #呼叫建構函式,初始化
7 astore_1
這就是為什麼在單例模式的雙檢查鎖DCL中,為什麼要加volatile關鍵字了,防止這四個指令的重排序問題
每個非靜態方法,區域性變量表index=0的位置永遠存放的都是this指標,有點類似於python裡面每個方法的第一個引數是self的意思,this指標在呼叫init構造方法時完成賦值
堆的預設大小
預設大小是實體記憶體的1 / 64,最大是1 / 4
虛擬機器棧 -> 方法區的聯絡
動態連結
堆 -> 方法區
klass Pointer型別指標:物件執行類的Class物件的記憶體地址
方法區 -> 堆
靜態變數:例如public static Test test = new Test(), 靜態屬性在方法區,物件在堆中
指標壓縮
未開啟指標壓縮:記憶體地址佔8位元組。開啟指標壓縮:記憶體地址佔4位
如何計算一個物件的大小
要計算一個物件的大小,首先需要了解一下物件在JVM中的一個記憶體佈局
Mark Word:
32位機:4個位元組
64位機:8個位元組
指標壓縮:
開啟:4個位元組
未開啟:8個位元組
對齊填充:按8個位元組對齊填充
首先看一個簡單的物件,計算它的物件大小(引入jol-core包,可以檢視物件大小)
開啟指標壓縮的情況下,像這種沒有普通屬性的物件被稱為空物件,來看看它的物件大小16B是怎麼算出來的
首先是Mark Word = 8B,因為是64位的機器上
其次是型別指標,預設開啟指標壓縮,則型別指標 = 4B
陣列長度 = 0B
例項資料 = 0B
對齊填充:8B + 4B不是8B對齊,需要填充4B,對齊填充= 4B
最後,Test物件大小 = 8B + 4B + 0B + 0B + 4B = 16B
關閉指標壓縮的情況下:Test物件大小還是16B = 8B + 8B + 0B + 0B + 0B
來看一個具有普通屬性的物件
我們可以看到該Test類開啟了指標壓縮,具有一個普通屬性a,short佔2B,最後得出來的物件大小為16B = 8B(Mark Word) + 4B(型別指標) + 0B(陣列長度) + 2B(例項資料) + 2B(對齊填充)
關閉指標壓縮
發現該Test物件大小為24B = 8B + 8B + 0B + 2B + 6B
來看一下陣列物件所佔的記憶體大小
我們可以看到開啟指標壓縮的情況下,arr陣列物件佔32B
我們可以看到arr陣列物件關閉指標壓縮的情況下,物件大小佔40B,並且陣列物件會有2個padding(對齊填充).
什麼情況下物件的記憶體佈局會產生2個padding
物件是陣列物件並且關閉指標壓縮的情況下,會產生2個padding
開啟指標壓縮的優勢
1. 節省JVM空間
2. 提升JVM執行效率
JVM怎麼做到在指標壓縮的情況下還能執行
假設有三個物件test1 = 16B、test2 = 32B、test3 = 24B。
在記憶體中的地址分別為:test1 = 0x00000、test2 = 0x10000、test3 = 0x30000
因為JVM採用8位元組對齊1000,後面三位一定是000。所以可以將test1, test2, test3後面三位抹掉,那麼在儲存時就會變為test1= 0x00、test2 = 0x10、 test3 = 0x30,而在使用時,則將地址後面的三位000補充回來
開啟指標壓縮的情況下,一個oop(物件指標)能表示最大堆空間是多少
我們知道開啟指標壓縮的情況下,型別指標佔4位元組,也就是32位,上一小節說到JVM儲存時會抹掉後面的3位,也就是可以儲存32 + 3 = 35位,最大記憶體空間也就是2的35次方,32G
oop(物件指標)如何擴容
前面說到JVM採用8位元組對齊,會抹掉後面的3位,如果我們讓它採用16位元組對齊,那麼是不是可以抹掉最後面的4位,oop(物件指標)所能表示的堆空間則為2的32 + 4次方
JVM為什麼不是16位元組對齊
如果採用16位元組對齊的話,在使用時對齊填充浪費記憶體空間
虛擬機器棧預設大小
可以通過命令java -XX:+PrintFlagsFinal -version | grep ThreadStack棧大小為1024KB
虛擬機器棧最小為160KB
JVM對虛擬機器棧做了最小的限制,限制為160KB
我們可以看到通過-Xss引數設定虛擬機器棧大小為100KB,發現控制檯報錯,最小為160KB