1. 程式人生 > 實用技巧 >JVM 進階知識

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