jvm-記憶體裡的java物件
在java中,建立物件的方式有很多種。最常見的就是new關鍵字了。除此之外,還有反射,clone(),反序列化以及Unsafe.allocateInstance。其中,反序列化和clone()是直接複製已有的資料來初始化物件的欄位。Unsafe.allocateInstance 沒有初始化物件的欄位。new和反射則是呼叫構造方法來初始化例項欄位的。
下面是new關鍵字的位元組碼
test test = new test(); -------------------------------- 0: new #13 // class com/lv/ddpay/test/test 3: dup 4: invokespecial #14 // Method "<init>":()V 7: astore_1
可以看到,編譯來的位元組碼包含new指令,和用來呼叫構造方法的invokespecial指令。
當然,對於構造方法,java有很多約束
- 如果一個類沒有定義構造方法,那java編譯器就會自動新增一個構造方法
- 子類的構造方法需要呼叫父類的構造方法。如果父類的構造方法時無參構造方法,那麼可以隱式呼叫,就是說java編譯器會自動增加對父類構造方法呼叫的指令。如果父類沒有無參構造方法,那子類就只能顯示的呼叫。顯示呼叫有兩種
- 使用super關鍵字直接呼叫父類構造方法
- 使用this關鍵字呼叫同類裡的別的構造方法
無論是super還是this,都需要作為構造方法裡的第一條語句
總結來講,就是說當呼叫構造方法的時候,會優先呼叫父類構造方法,一直到Object類。
從上面可以看出,使用new來建立物件,記憶體裡其實擁有自己和父類的所有例項欄位。即雖然子類不能訪問父類的私有欄位,但那些欄位還是被分配了記憶體。下面就來看看java物件是怎麼在記憶體裡的。
壓縮指標
在jvm裡,每個物件都有一個由型別指標和標記欄位組成的物件頭。其中,標記欄位時儲存該物件執行時的資料,譬如hash,gc資訊。在64位的jvm裡,標記欄位佔64位,型別指標也佔64位,就是說,每個物件在java記憶體裡的額外開銷有16個位元組。所以,位了減少物件的額外記憶體,64位虛擬機器就有了壓縮指標這一東西。jvm引數是 -XX:-UseConpressedOops ,預設是開啟的。開啟之後,物件指標就能壓縮到32位。壓縮指標有個對應的東西就記憶體對齊。jvm引數是 -XX:ObjectAlignmentInBytes 預設值是8.就是說,在jvm堆中,物件的起始地址都要對齊到8的倍數。如果一個物件所需的記憶體用不到8的倍數,那空出來的部分就直接浪費了。預設情況下,jvm中的32位壓縮指標可以定址到2的35次方個位元組,也就是32GB的空間。
記憶體對齊不僅僅在物件和物件之間,欄位之間也是存在的。欄位對齊的一個重要原因,是讓欄位只出現在一個CPU的快取行裡。如果欄位不對齊,就很有可能出現跨快取行的欄位,這對於程式執行時非常不利的。
jvm為了讓欄位對齊,還有一個重要的東西就是欄位重排序。就是jvm重新分配欄位的先後順序以達到記憶體對齊的效果。jvm裡有三種方法,讓欄位重排序。jvm欄位 -XX:FieldAllocationStyle 預設值為1.這三種方法都會遵循拉你兩個規則
- 子類繼承的欄位的偏移量要和父類對於的欄位偏移量儲存一致
- 如果一個欄位佔據A個位元組,那麼該欄位的偏移量需要對齊值AX。即A的整數倍數
當然,在java裡面還有個東西叫虛共享。java8裡有個註釋叫@Contended,該註解也會影響欄位的排序。譬如兩個執行緒訪問不同的volatile修飾的欄位,從邏輯層面上看,並沒有共享,所以不需要同步。但如果中這兩個欄位恰好在同一個快取行裡,那麼寫操作也就造成了實際記憶體上的共享。因此jvm就會讓有@Contended註解的欄位單獨的處於快取行裡,因此記憶體也就會大量浪費。
總結
物件的構造有多種方法。常見的new關鍵字會被編譯成new指令,然後呼叫對應的構造方法。構造方法的呼叫會先父類在子類依次呼叫。64位jvm為了節省空間引入了壓縮指標的概念,將64位型別指標壓縮成32位,使每個物件頭都能節省4位元組的空間。但,堆大小超過32GB的話,壓縮指標就失去了效用。壓縮指標要求jvm堆中的物件的起始地址要是-XX:ObjectAlignmentInBytes 設定的值的倍數。jvm還會對欄位進行重排序,是欄位也能記憶體對齊。