java對象詳解
- java對象詳解
- 內存布局
- 普通對象布局
- 數組的內存布局
- 內部類的內存布局
- 對象分解
- 對象頭-mark word(8字節)
- 實例數據
- 對齊填充(可選)
- java鎖分析
- 內存布局
java對象詳解
HotSpot虛擬機中,對象在內存中存儲的布局可以分為
對象頭
,實例數據
,對齊填充
三個區域。本文所說環境均為HotSpot虛擬機。即輸入java -version
返回的虛擬機版本:
java version "1.8.0_111" Java(TM) SE Runtime Environment (build 1.8.0_111-b14) Java HotSpot(TM) 64-Bit Server VM (build 25.111-b14, mixed mode)
內存布局
普通對象布局
在jvm中,任何對象都是8個字節為粒度進行對齊的,這是對象內存布局的第一個規則。
如果調用new Object(),由於Object類並沒有其他沒有其他可存儲的成員,那麽僅僅使用堆中的8個字節來保存兩個字的頭部即可。
除了上面所說的8個字節的頭部(關於對象頭,在下面會有詳細解釋),類屬性緊隨其後。屬性通常根據其大小來排列。例如,整型(int)以4個字節為單位對齊,長整型(long)以8個字節為單位對齊。這裏是出於性能考慮而這麽設計的:通常情況下,如果數據以4字節為單位對齊,那麽從內存中讀4字節的數據並寫入到處理器的4字節寄存器是性價比更高的。
為了節省內存,Sun VM並沒有按照屬性聲明時的順序來進行內存布局。實際上,屬性在內存中按照下面的順序來組織:
-
雙精度型(doubles)和長整型(longs)
-
整型(ints)和浮點型(floats)
-
短整型(shorts)和字符型(chars)
-
布爾型(booleans)和字節型(bytes)
-
引用類型(references)
內存使用率會通過這個機制得到優化。例如,如下聲明一個類:
class MyClass {
byte a;
int c;
boolean d;
long e;
Object f;
}
如果JVM並沒有打亂屬性的聲明順序,其對象內存布局將會是下面這個樣子:
[HEADER: 8 bytes] 8
[a: 1 byte ] 9
[padding: 3 bytes] 12
[c: 4 bytes] 16
[d: 1 byte ] 17
[padding: 7 bytes] 24
[e: 8 bytes] 32
[f: 4 bytes] 36
[padding: 4 bytes] 40
此時,用於占位的14個字節是浪費的,這個對象一共使用了40個字節的內存空間。但是,如果用上面的規則對這些對象重新排序,其內存結果會變成下面這個樣子:
[HEADER: 8 bytes] 8
[e: 8 bytes] 16
[c: 4 bytes] 20
[a: 1 byte ] 21
[d: 1 byte ] 22
[padding: 2 bytes] 24
[f: 4 bytes] 28
[padding: 4 bytes] 32
這次,用於占位的只有6個字節,這個對象使用了32個字節的內存空間。
規則2:類屬性按照如下優先級進行排列:長整型和雙精度類型;整型和浮點型;字符和短整型;字節類型和布爾類型,最後是引用類型。這些屬性都按照各自的單位對齊。
現在我們知道如何計算一個繼承了Object的類的實例的內存大小了。下面這個例子用來做下練習: java.lang.Boolean。這是其內存布局:
[HEADER: 8 bytes] 8
[value: 1 byte ] 9
[padding: 7 bytes] 16
Boolean類的實例占用16個字節的內存!驚訝吧?(別忘了最後用來占位的7個字節)。
規則3:不同類繼承關系中的成員不能混合排列。首先按照規則2處理父類中的成員,接著才是子類的成員。
舉例如下:
class A {
long a;
int b;
int c;
}
class B extends A {
long d;
}
類B的實例在內存中的存儲如下:
[HEADER: 8 bytes] 8
[a: 8 bytes] 16
[b: 4 bytes] 20
[c: 4 bytes] 24
[d: 8 bytes] 32
如果父類中的成員的大小無法滿足4個字節這個基本單位,那麽下一條規則就會起作用:
規則4:當父類中最後一個成員和子類第一個成員的間隔如果不夠4個字節的話,就必須擴展到4個字節的基本單位。
class A {
byte a;
}
class B {
byte b;
}
[HEADER: 8 bytes] 8
[a: 1 byte ] 9
[padding: 3 bytes] 12
[b: 1 byte ] 13
[padding: 3 bytes] 16
註意到成員a被擴充了3個字節以保證和成員b之間的間隔是4個字節。這個空間不能被類B使用,因此被浪費了。
規則5:如果子類第一個成員是一個雙精度或者長整型,並且父類並沒有用完8個字節,JVM會破壞規則2,按照整形(int),短整型(short),字節型(byte),引用類型(reference)的順序,向未填滿的空間填充。
class A {
byte a;
}
class B {
long b;
short c;
byte d;
}
[HEADER: 8 bytes] 8
[a: 1 byte ] 9
[padding: 3 bytes] 12
[c: 2 bytes] 14
[d: 1 byte ] 15
[padding: 1 byte ] 16
[b: 8 bytes] 24
在第12字節處,類A“結束”的地方,JVM沒有遵守規則2,而是在長整型之前插入一個短整型和一個字節型成員,這樣可以避免浪費3個字節。
數組的內存布局
數組有一個額外的頭部成員,用來存放“長度”變量。數組元素以及數組本身,跟其他常規對象同樣,都需要遵守8個字節的邊界規則。
下面是一個有3個元素的字節數組的內存布局:
[HEADER: 12 bytes] 12
[[0]: 1 byte ] 13
[[1]: 1 byte ] 14
[[2]: 1 byte ] 15
[padding: 1 byte ] 16
下面是一個有3個元素的長整型數字的內存布局:
[HEADER: 12 bytes] 12
[padding: 4 bytes] 16
[[0]: 8 bytes] 24
[[1]: 8 bytes] 32
[[2]: 8 bytes] 40
內部類的內存布局
非靜態內部類(Non-static inner classes)有一個額外的“隱藏”成員,這個成員是一個指向外部類的引用變量。這個成員是一個普通引用,因此遵守引用內存布局的規則。內部類因此有4個字節的額外開銷。
以上引用:http://www.importnew.com/1305.html
對象分解
對象頭-mark word(8字節)
對象頭主要包含兩部分信息,第一部分用於存儲對象自身運行時數據,如哈希碼,GC分代年齡(可以查看上一篇關於java內存回收分析的文章),鎖狀態標誌,線程持有鎖,偏向線程ID,偏向時間戳等。
如果對象是數組類型,則虛擬機用3個字寬存儲對象頭,如果是非數組類型,則用2個字寬存儲對象頭。下圖是一個32位虛擬機mark部分占用內存分布情況
j1.jpeg
此圖來源:http://blog.csdn.net/zhoufanyang_china/article/details/54601311
另一部分是klass類型指針,即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例,具體結構參考下圖。
javao.png
在32位系統下,存放Class指針的空間大小是4字節,MarkWord是4字節,對象頭為8字節。
在64位系統下,存放Class指針的空間大小是8字節,MarkWord是8字節,對象頭為16字節。
64位開啟指針壓縮的情況下,存放Class指針的空間大小是4字節,MarkWord是8字節,對象頭為12字節。
數組長度4字節+數組對象頭8字節(對象引用4字節(未開啟指針壓縮的64位為8字節)+數組markword為4字節(64位未開啟指針壓縮的為8字節))+對齊4=16字節。
靜態屬性不算在對象大小內。
實例數據
對象實際數據,大下為實際數據的大小
對齊填充(可選)
按8字節對齊,參照上面內存布局部分
java鎖分析
synchronized到底鎖的是對象還是代碼片段?
例:
package com.startclan.thread;
/**
* Created by wongloong on 17-5-20.
*/
public class TestSync {
public synchronized void test() {
System.out.println("test1 start");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("test1 end");
}
public synchronized void test2() {
System.out.println("test2 start");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("test2 end");
}
}
package com.startclan.thread;
/**
* Created by wongloong on 17-5-20.
*/
public class TestSyncStatic {
public static synchronized void test() {
System.out.println("test1 start");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("test1 end");
}
public static synchronized void test2() {
System.out.println("test2 start");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("test2 end");
}
}
package com.startclan;
import com.startclan.thread.TestSync;
import com.startclan.thread.TestSyncStatic;
import org.junit.Test;
/**
* Created by wongloong on 17-5-18.
*/
public class TestWithThread {
@Test
public void testThread1() throws Exception {
final TestSync t1 = new TestSync();
/**
* 測試synchronized同步非static代碼塊
* 此處會先執行test方法然後執行test2方法,說明synchronized在同步非static方法時,
* 只能同步同一對象的同一實例進行同步
*/
new Thread(new Runnable() {
@Override
public void run() {
t1.test();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
t1.test2();
}
}).start();
Thread.sleep(4000);
}
@Test
public void testThread2() throws Exception {
final TestSync t1 = new TestSync();
final TestSync t2 = new TestSync();
/**
* 測試synchronized同步非static代碼塊
* t1 t2不同對象,
* 不能同步方法
*/
new Thread(new Runnable() {
@Override
public void run() {
t1.test();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
t2.test2();
}
}).start();
Thread.sleep(4000);
}
@Test
public void testThread3() throws Exception {
final TestSyncStatic tss1 = new TestSyncStatic();
final TestSyncStatic tss2 = new TestSyncStatic();
/**
* 測試synchronized 同步 static代碼塊
* 由於method1和method2都屬於靜態同步方法,
* 所以調用的時候需要獲取同一個類上monitor(每個類只對應一個class對象),
* 所以也只能順序的執行。
*/
new Thread(new Runnable() {
@Override
public void run() {
tss1.test();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
tss2.test2();
}
}).start();
Thread.sleep(4000);
}
}
此時輸出結果為:
------------------------1-------------------------
test1 start
test1 end
test2 start
test2 end
-----------------------2-------------------------
test1 start
test2 start
test2 end
test1 end
-----------------------3-------------------------
test1 start
test1 end
test2 start
test2 end
結論:
synchronized(this)以及非static的synchronized方法,只能防止多個線程同時執行同一個實例的同步代碼段(在第一段測試代碼中,分別new了三個Mythread類,所以並不會執行同步)
synchronized(xx.class)及static的synchronized方法,可以防止多個線程同時執行同一個對象的多個實例同步的代碼段
synchronize原理
每一個對象頭信息都包含一個鎖定狀態,可以看上面的mark word的圖解。當線程進入對象中,嘗試獲取鎖的所有權,如果為鎖的值為0,則該線程進入,並設置為1,該線程為鎖的擁有者。如果線程已經占用該鎖,只是重新進入,並且鎖值+1.當線程退出時則-1.如果其他線程訪問這個對象實例,則改線程堵塞。直到鎖值為0的時候,在重新嘗試取得鎖的所有權。
java對象詳解