java中我們隨處可見的都是對象,而對象成爲我們與計算機內核交換的主要載體,使用起來也非常簡單,然而一個對象是如何被JVM創建的卻是極其的複雜,它要經歷類加載機制、分片內存以及設置對象頭的內存佈局。
下面講介紹下Hotspot JVM下新建對象需要基本過程。
前言-字節碼
new語句最會被編譯而成的字節碼,而它將包含用來請求內存指令。
public class ObjectSizeMain{
public static void main(String []args){
ObjectSizeMain obj=new ObjectSizeMain();
}
}
C:\Users\Administrator\Desktop>javap -v ObjectSizeMain.class
Classfile /C:/Users/Administrator/Desktop/ObjectSizeMain.class
Last modified 2019-6-1; size 290 bytes
MD5 checksum 1635999ab99de05314d8568fbaaf6900
Compiled from "ObjectSizeMain.java"
public class ObjectSizeMain
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#13 // java/lang/Object."<init>":()V
#2 = Class #14 // ObjectSizeMain
#3 = Methodref #2.#13 // ObjectSizeMain."<init>":()V
#4 = Class #15 // java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Utf8 LineNumberTable
#9 = Utf8 main
#10 = Utf8 ([Ljava/lang/String;)V
#11 = Utf8 SourceFile
#12 = Utf8 ObjectSizeMain.java
#13 = NameAndType #5:#6 // "<init>":()V
#14 = Utf8 ObjectSizeMain
#15 = Utf8 java/lang/Object
{
public ObjectSizeMain();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class ObjectSizeMain
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: return
LineNumberTable:
line 3: 0
line 4: 8
}
SourceFile: "ObjectSizeMain.java"
重點關注下紅色部分內容,它透露了一些信息:
- new 指令,用來請求內存及用來調用構造器的 invokespecial 指令
- invokespecial指令,用於調用私有實例方法、構造器,以及使用super關鍵字調用父類的實例方法或構造器,和所實現接口的默認方法
關於字節碼可以參閱《實戰JAVA虛擬機 JVM故障診斷與性能優化》中class文件結構的章節
1. 類加載機制
JVM首先檢查一個new指令的參數是否能在常量池中定位到一個符號引用,並且檢查該符號引用代表的類是否已被加載、解析和初始化過(實際上就是在檢查new的對象所屬的類是否已經執行過類加載機制)。如果沒有,先進行類加載機制加載類。可參閱你忽略的ClassLoader。
2. 分配堆內存
簡單理解爲:確定對象內存大小並從Java堆中劃分出來
- 在類加載完成後,一個對象所需的內存大小就可以完全確定了
- 爲對象分配空間,即把一塊兒確定大小的內存從Java堆中劃分出來
內存分配兩種方式
Java堆內存是否規整,取決於GC收集器的算法是"標記-清除",還是"標記-整理"(也稱作"標記-壓縮"),值得注意的是,複製算法內存也是規整的,然後選擇對應的方式。
指針碰撞 |
適用場合:堆內存規整(即沒有內存碎片)的情況下 原理:用過的內存全部整合到一邊,沒有用過的內存放在另一邊,中間有一個分界值指針,只需要向着沒用過的內存方向將該指針移動對象內存大小位置即可 GC收集器:Serial、ParNew |
空閒列表 |
適用場合:堆內存不規整的情況下 原理:虛擬機會維護一個列表,該列表中會記錄哪些內存塊是可用的,在分配的時候,找一塊兒足夠大的內存塊兒來劃分給對象實例(這一塊兒可以類比memcached的slab模型),最後更新列表記錄。 GC收集器:CMS |
內存分配併發問題
堆內存是各個線程的共享區域,所以在操作堆內存的時候,需要處理併發問題。處理的方式有兩種:
CAS+失敗重試 |
做法與AtomicInteger的getAndSet(int newValue)方法的實現方式類似 public final boolean compareAndSet(int expect, int update) { |
TLAB (Thread Local Allocation Buffer) |
原理:爲每一個線程預先在Eden區分配一塊兒內存,JVM在給線程中的對象分配內存時,首先在TLAB分配,當對象大於TLAB中的剩餘內存或TLAB的內存已用盡時,再採用上述的CAS進行內存分配 -XX:+/-UseTLAB:是否使用TLAB -XX:TLABWasteTargetPercent:設置TLAB可佔用的Eden區的比率,默認爲1% -XX:PrintTLAB:查看TLAB的使用情況 JVM會根據以下三個條件來給每個線程分配合適大小的TLAB: 1.-XX:TLABWasteTargetPercent 2.線程數量 3.線程是否頻繁分配對象 |
3. 初始化零值並設置對象頭
- 對象的實例字段不需要賦初始值也可以直接使用其默認零值
- 每一種類型的對象都有不同的默認零值
對象在內存中存儲的佈局分爲三塊
對象頭 |
存儲對象自身的運行時數據:Mark Word(在32bit和64bit虛擬機上長度分別爲32bit和64bit),包含如下信息:
-XX:+UseCompressedOops:JDK 8下默認爲啓用,啓用對象的指針壓縮,節約內存佔用的大小 -XX:+CompactFields:JDK 8下默認爲啓用,分配一個非static的字段在前面字段縫隙中,提高內存的利用率 -XX:FieldsAllocationStyle=1:JDK 8下默認值爲‘1’,實例對象中有效信息的存儲順序
注:Mark Word具有非固定的數據結構,以便在極小的空間內存儲儘量多的信息。如果對象是一個數組,對象頭必須有一塊兒用於記錄數組長度的數據。JVM可以通過Java對象的元數據確定對象長度,但是對於數組不行。
在java中對象的每個成員屬性都有一個offset,通過UnsafeUtils.unsafe().objectFieldOffset可以獲得。但分配內存是有些差別,64位系統中CPU一次讀操作可讀取64bit(8 bytes)的數據,讀取屬性long不能讀取2次(如果從對象頭開始就會發生),因此會打破存儲順序(FieldsAllocationStyle)。字段重排列技術指的是重新分配字段的先後順序,以達到內存對齊的目的。 |
實例數據 | 對象真正存儲的有效信息 |
對齊填充 | HotSpot VM的自動內存管理系統要求對象大小必須是8字節的整數倍,對齊填充沒有特別的含義,它僅僅起着佔位符的作用 |
4. 執行init
爲對象的字段賦值(這裏會根據所寫程序給實例賦值)
小刀牛試
public class ObjectSize_test {
/**
* 啓動參數中添加: -javaagent:D:\Program\repository\classmexer\classmexer\0.03\classmexer-0.03.jar
*/
@Test
public void showObjSize() {
LongInstace v = new LongInstace();
//24=12+8+2+2=對象頭+long+byte+padding(2),不壓縮的話就是16+8+2...
System.out.printf("shallow size: %s.byte\n", MemoryUtil.memoryUsageOf(v));
System.out.printf("retained size: %s.byte\n", MemoryUtil.deepMemoryUsageOf(v));
LongInstace2 v2 = new LongInstace2();
//16=12+4=對象頭+oops
System.out.printf("shallow size: %s.byte\n", MemoryUtil.memoryUsageOf(v2));
//40=16+24=LongInstace2+Long
System.out.printf("retained size: %s.byte\n", MemoryUtil.deepMemoryUsageOf(v2));
IntegerInstace1 v3 = new IntegerInstace1();
//16=12+4=對象頭+oops
System.out.printf("shallow size: %s.byte\n", MemoryUtil.memoryUsageOf(v3));
//32=16+16=LongInstace3+Integer,有壓縮
System.out.printf("retained size: %s.byte\n", MemoryUtil.deepMemoryUsageOf(v3));
LongInstace[]vs=new LongInstace[2];
vs[0]=v;
System.out.printf("shallow size: %s.byte\n", MemoryUtil.memoryUsageOf(vs));
System.out.printf("retained size: %s.byte\n", MemoryUtil.deepMemoryUsageOf(vs));
System.out.printf("first item offset : %s,per item size: %s \n",UnsafeUtils.unsafe().arrayBaseOffset(vs.getClass()),UnsafeUtils.unsafe().arrayIndexScale(LongInstace[].class));
ComplexInstance obj = new ComplexInstance();
//48=12+1+2+2+4+4+8+8+padding(7)
System.out.println("ComplexInstance:" + MemoryUtil.memoryUsageOf(obj));
}
@Test
public void showLayout() throws NoSuchFieldException {
/**
* 無填充
* 內存佈局:對象頭(12) + oops(4)
*/
long offset = UnsafeUtils.unsafe().objectFieldOffset(IntegerInstace1.class.getDeclaredField("value"));
//12=對象頭(12) 開始
System.out.printf("IntegerInstace1.value(Integer): %s \n", offset);
/**
* 有填充
* 規則:64位系統中CPU一次讀操作可讀取64bit(8 bytes)的數據,讀取屬性long不能讀取2次(如果從對象頭開始就會發生)
*/
offset = UnsafeUtils.unsafe().objectFieldOffset(LongInstace1.class.getDeclaredField("value"));
//16=對象頭(12)+padding(4)
System.out.printf("LongInstace1.value(long): %s \n", offset);
offset = UnsafeUtils.unsafe().objectFieldOffset(LongInstace.class.getDeclaredField("value2"));
//12=對象頭(12) -XX:+CompactFields生效
System.out.printf("LongInstace.value2(byte): %s \n", offset);
offset = UnsafeUtils.unsafe().objectFieldOffset(LongInstace.class.getDeclaredField("value3"));
//13=對象頭(12)+value2 -XX:+CompactFields生效
System.out.printf("LongInstace.value3(char): %s \n", offset);
/**
* 內部有多個成員變量時,佈局按-XX:FieldsAllocationStyle=1(longs/doubles、ints/floats、shorts/chars、bytes/booleans)
* double=24=對象頭(12)+int(4)+long(8),double和long是有變量定義的順序決定的,不行你可以試試
* 48=對象頭(12)+int(4)+long(8)+double(8)+float(4)+char(2)+short(2)+boolean(1)+padding(7)
*/
offset = UnsafeUtils.unsafe().objectFieldOffset(ComplexInstance.class.getDeclaredField("bool"));
System.out.printf("ComplexInstance.bool(boolean): %s \n", offset);//40
offset = UnsafeUtils.unsafe().objectFieldOffset(ComplexInstance.class.getDeclaredField("cha"));
System.out.printf("ComplexInstance.cha(char): %s \n", offset);//36
offset = UnsafeUtils.unsafe().objectFieldOffset(ComplexInstance.class.getDeclaredField("sho"));
System.out.printf("ComplexInstance.sho(short): %s \n", offset);//38
offset = UnsafeUtils.unsafe().objectFieldOffset(ComplexInstance.class.getDeclaredField("in"));
System.out.printf("ComplexInstance.in(int): %s \n", offset);//12
offset = UnsafeUtils.unsafe().objectFieldOffset(ComplexInstance.class.getDeclaredField("flo"));
System.out.printf("ComplexInstance.flo(float): %s \n", offset);//32
offset = UnsafeUtils.unsafe().objectFieldOffset(ComplexInstance.class.getDeclaredField("lon"));
System.out.printf("ComplexInstance.lon(long): %s \n", offset);//16
offset = UnsafeUtils.unsafe().objectFieldOffset(ComplexInstance.class.getDeclaredField("dou"));
System.out.printf("ComplexInstance.dou(double): %s \n", offset);//24
}
public final static class LongInstace {
protected long value = 0L;
byte value2;
boolean value3;
}
public final static class LongInstace1 {
protected long value = 0L;
}
public final static class LongInstace2 {
Long value = 0L;
}
public final static class IntegerInstace1 {
Integer value = 0;
}
public final static class ComplexInstance {
boolean bool;
char cha;
short sho;
int in;
float flo;
long lon;
double dou;
}
}
參閱資料
- 《HotSpot實戰》
- 《深入理解Java虛擬機:JVM高級特性與最佳實踐(最新第二版)》
- 《實戰JAVA虛擬機 JVM故障診斷與性能優化》
- 《分佈式Java應用:基礎與實踐》