咱們編寫一個Java
類,編譯後會生成.class
文件,當類加載器將class
文件加載到jvm
時,會生成一個Klass
類型的對象(c++
),稱爲類描述元數據,存儲在方法區中,即jdk1.8
以後的元數據區。當使用new
建立對象時,就是根據類描述元數據Klass
建立的對象oop
,存儲在堆中。每一個java
對象都有相同的組成部分,稱爲對象頭。java
在學習併發編程知識synchronized
時,咱們老是難以理解其實現原理,由於偏向鎖、輕量級鎖、重量級鎖都涉及到對象頭,因此瞭解java
對象頭是咱們深刻了解synchronized
的前提條件。ios
介紹一款能夠在代碼中計算java
對象的大小以及查看java
對象內存佈局的工具包:jol-core
,jol
爲java object layout
的縮寫,即java對象佈局。使用只須要到maven
倉庫http://mvnrepository.com
搜索java object layout
,選擇想要使用的版本,將依賴添加到項目中便可。c++
使用jol
計算對象的大小(單位爲字節):編程
ClassLayout.parseInstance(obj).instanceSize()
複製代碼
使用jol
查看對象的內存佈局:數組
ClassLayout.parseInstance(obj).toPrintable()
複製代碼
網絡搜索了不少資料,對64
位jvm
的Java
對象頭的佈局講解的都很模糊,不少資料都是講的32
位,並且不少都是從一些書上摘抄下來的,不免會存在錯誤的地方,因此最好的學習方法就是本身去驗證,看jvm
源碼。本篇將詳細介紹64
位jvm
的Java
對象頭。bash
以User
類爲例網絡
public class User {
private String name;
private Integer age;
private boolean sex;
}
複製代碼
經過jol
查看User
對象的內存佈局併發
User user = new User()
System.out.println(ClassLayout.parseInstance(user).toPrintable());
複製代碼
輸出內容以下 jvm
object header
爲對象頭;從圖中能夠看到,對象頭所佔用的內存大小爲16*8bit=128bit
。若是你們本身動手去打印輸出,可能獲得的結果是96bit
,這是由於我關閉了指針壓縮。jdk8
版本是默認開啓指針壓縮的,能夠經過配置vm
參數關閉指針壓縮。maven
-XX:-UseCompressedOops
複製代碼
如今取消關閉指針壓縮的配置,開啓指針壓縮以後,再看User
對象的內存佈局。
開啓指針壓縮能夠減小對象的內存使用。從兩次打印的User
對象佈局信息來看,關閉指針壓縮時,name
字段和age
字段因爲是引用類型,所以分別佔8
個字節,而開啓指針壓縮以後,這兩個字段只分別佔用4
個字節。所以,開啓指針壓縮,理論上來說,大約能節省百分之五十的內存。jdk8
及之後版本已經默認開啓指針壓縮,無需配置。
從兩次打印的User
對象的內存佈局,還能夠看出,bool
類型的age
字段只佔用1
個字節,但後面會跟隨幾個字節的浪費,即內存對齊。開啓指針壓縮狀況下,age
字段的內存對齊須要3
個字節,而關閉指針壓縮狀況下,則須要7
個字節。
以默認開啓指針壓縮狀況下的User
對象的內存佈局來看,對象頭佔用12個字節
,那麼這12
個字節存儲的是什麼信息,咱們不看網上的資料,而是看jdk
的源碼。
我當前使用的jdk
版本是jdk1.8
,可經過命令行java -version
查看,也可經過下面方式查看,這個你們應該都很熟悉了。
System.out.println(System.getProperties());
複製代碼
打開官網後點擊左側菜單欄的Groups
找到HotSpot
,在打開的頁面的Source code
選擇Browsable souce
,以後會跳轉到hg.openjdk.java.net/,選擇jdk8u
,跳轉後的頁面中繼續選擇jdk8u
下面的hotspot
。傳送連接:hg.openjdk.java.net/jdk8u/jdk8u…。
zip
可將源碼打包下載;browse
可在線查看源碼;在開啓指針壓縮的狀況下,User
對象的對象頭佔用12
個字節,本節咱們經過源碼瞭解對象頭都存儲了哪些信息。
在Java
程序運行的過程當中,每建立一個新的對象,JVM
就會相應地建立一個對應類型的oop
對象,存儲在堆中。如new User()
,則會建立一個instanceOopDesc
,基類爲oopDesc
。
[instanceOop.hpp文件:hotspot/src/share/vm/oops/instanceOop.hpp]
class instanceOopDesc : public oopDesc {
}
複製代碼
instanceOopDesc
只提供了幾個靜態方法,如獲取對象頭大小。所以重點看其父類oopDesc
。
[oop.hpp文件:hotspot/src/share/vm/oops/oop.hpp]
class oopDesc {
friend class VMStructs;
private:
volatile markOop _mark;
union _metadata {
Klass* _klass;
narrowKlass _compressed_klass;
} _metadata;
........
}
複製代碼
咱們只關心對象頭,普通對象(如User
對象,本篇不講數組類型)的對象頭由一個markOop
和一個聯合體組成,markOop
就是MarkWord
。這個聯合體是指向類的元數據指針,未開啓指針壓縮時使用_klass
,開啓指針壓縮時使用_compressed_klass
。
markOop
與narrowKlass
的類型定義在/hotspot/src/share/vm/oops/oopsHierarchy.hpp
頭文件中:
[oopsHierarchy.hpp頭文件:/hotspot/src/share/vm/oops/oopsHierarchy.hpp]
typedef juint narrowKlass;
typedef class markOopDesc* markOop;
複製代碼
所以,narrowKlass
是一個juint
,junit
是在globalDefinitions_visCPP.hpp
頭文件中定義的,這是一個無符號整數,即4
個字節。因此開啓指針壓縮以後,指向Klass
對象的指針大小爲4
字節。
[/hotspot/src/share/vm/utilties/globalDefinitions_visCPP.hpp]
typedef unsigned int juint;
複製代碼
而markOop
則是markOopDesc
類型指針,markOopDesc
就是MarkWord
。不知道大家有沒有感受到奇怪,在64
位jvm
中,markOopDesc
指針是8
字節,即64bit
,確實恰好是MarkWord
的大小,可是指針指向的不是一個對象嗎?咱們先看markOopDesc
類。
[markOop.hpp文件:hotspot/src/share/vm/oops/markOop.hpp]
// 64 bits:
// --------
// unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object)
// PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
// size:64 ----------------------------------------------------->| (CMS free block)
class markOopDesc: public oopDesc {
......
}
複製代碼
markOop.hpp
頭文件中給出了64bit
的MarkWord
存儲的信息說明。markOopDesc
類也繼承oopDesc
。若是單純的看markOopDesc
類的源碼,根本找不出來,markOopDesc
是用那個字段存儲MarkWord
的。並且,根據從各類來源的資料中,咱們所知道的是,對象頭的前8
個字節存儲的就是是否偏向鎖、輕量級鎖等等信息(全文都是以64
位爲例),因此不該該是個指針啊。
爲了解答這個疑惑,我是先從markOopDesc
類的源碼中,找一個方法,好比,獲取gc
對象年齡的方法,看下jvm
是從哪裏獲取的數據。
class markOopDesc: public oopDesc {
public:
// 獲取對象年齡
uint age() const {
return mask_bits(value() >> age_shift, age_mask);
}
// 更新對象年齡
markOop set_age(uint v) const {
return markOop((value() & ~age_mask_in_place) | (((uintptr_t)v & age_mask) << age_shift));
}
// 自增對象年齡
markOop incr_age() const {
return age() == max_age ? markOop(this) : set_age(age() + 1);
}
}
複製代碼
那麼,value()
這個方法返回的就是64bit
的MarkWord
了。
class markOopDesc: public oopDesc {
private:
// Conversion
uintptr_t value() const { return (uintptr_t) this; }
}
複製代碼
value
方法返回的是一個指針,就是this
。從set_age
和incr_age
方法中也能夠看出,只要修改MarkWord
,就會返回一個新的markOop
(markOopDesc*
)。難怪會將markOopDesc*
定義爲markOop
,就是將markOopDesc*
當成一個8
字節的整數來使用。想要理解這個,咱們須要先補充點c++
知識,所以我寫了個demo
。
自定義一個類叫oopDesc
,而且除構造函數和析構函數以外,只提供一個Show
方法。
[.hpp文件]
#ifndef oopDesc_hpp
#define oopDesc_hpp
#include <stdio.h>
#include <iostream>
using namespace std;
// 將oopDesc* 定義爲 oop
typedef class oopDesc* oop;
class oopDesc{
public:
void Show();
};
#endif /* oopDesc_hpp */
[.cpp文件]
#include "oopDesc.hpp"
void oopDesc::Show(){
cout << "oopDesc by wujiuye" <<endl;
}
複製代碼
使用oop(指針)
建立一個oopDesc*
,並調用show
方法。
#include <iostream>
#include "oopDesc.hpp"
using namespace std;
int main(int argc, const char * argv[]) {
//
oopDesc* o = oop(0x200);
cout << o << endl;
o->Show();
//
oopDesc* o1;
o1->Show();
return 0;
}
複製代碼
測試輸出
0x200
oopDesc by wujiuye
oopDesc by wujiuye
Program ended with exit code: 0
複製代碼
所以,經過類名(value)
能夠建立一個野指針對象,將指針賦值爲value
,這樣就可使用this
做爲MarkWord
了。若是在oopDesc
中添加一個字段,並提供一個方法訪問,程序運行就會報錯,所以,這樣建立的對象只能調用方法,不能訪問字段。
鎖狀態/gc | Mark Word (64 bits) | - | - | - | 是否偏向鎖 | 鎖標誌位 |
---|---|---|---|---|---|---|
無鎖 | unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 |
偏向鎖 | thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2 |
輕量級鎖 | ptr_to_lock_record:62 | - | - | - | - | lock:2 |
重量級鎖 | ptr_to_heavyweight_monitor:62 | - | - | - | - | lock:2 |
gc標誌 | - | - | - | - | - | lock:2 |
經過倒數三位判斷當前MarkWord
的狀態,就能夠判斷出其他位存儲的是什麼。
[markOop.hpp文件]
enum { locked_value = 0, // 0 00 輕量級鎖
unlocked_value = 1,// 0 01 無鎖
monitor_value = 2,// 0 10 重量級鎖
marked_value = 3,// 0 11 gc標誌
biased_lock_pattern = 5 // 1 01 偏向鎖
};
複製代碼
如今,咱們再看下User
對象打印的內存佈局。
64
位是
MarkWord
,後
32
位是類的元數據指針(開啓指針壓縮)。
從圖中能夠看出,在無鎖狀態下,該User
對象的hashcode
爲0x7a46a697
。因爲MarkWord
實際上是一個指針,在64
位jvm
下佔8
字節。所以MarkWordk
是0x0000007a46a69701
,跟你從圖中看到的正好相反。這裏涉及到一個知識點「大端存儲與小端存儲」。
學過彙編語言的朋友,這個知識點應該都還記得。本篇不詳細介紹,不是很明白的朋友能夠網上找下資料看。
接着,咱們再看一下,使用synchronized
加鎖狀況下的User
對象的內存信息,經過對象頭分析鎖狀態。
public class MarkwordMain {
private static final String SPLITE_STR = "===========================================";
private static User USER = new User();
private static void printf() {
System.out.println(SPLITE_STR);
System.out.println(ClassLayout.parseInstance(USER).toPrintable());
System.out.println(SPLITE_STR);
}
private static Runnable RUNNABLE = () -> {
while (!Thread.interrupted()) {
synchronized (USER) {
printf();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 3; i++) {
new Thread(RUNNABLE).start();
Thread.sleep(1000);
}
Thread.sleep(Integer.MAX_VALUE);
}
}
複製代碼
從該對象頭中分析加鎖信息,MarkWordk
爲0x0000700009b96910
,二進制爲0xb00000000 00000000 01110000 00000000 00001001 10111001 01101001 00010000
。
倒數第三位爲"0"
,說明不是偏向鎖狀態,倒數兩位爲"00"
,所以,是輕量級鎖狀態,那麼前面62
位就是指向棧中鎖記錄的指針。
public class MarkwordMain {
private static final String SPLITE_STR = "===========================================";
private static User USER = new User();
private static void printf() {
System.out.println(SPLITE_STR);
System.out.println(ClassLayout.parseInstance(USER).toPrintable());
System.out.println(SPLITE_STR);
}
private static Runnable RUNNABLE = () -> {
while (!Thread.interrupted()) {
synchronized (USER) {
printf();
}
}
};
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 3; i++) {
new Thread(RUNNABLE).start();
}
Thread.sleep(Integer.MAX_VALUE);
}
}
複製代碼
從該對象頭中分析加鎖信息,MarkWordk
爲0x0000700009b96910
,二進制爲0xb00000000 00000000 01111111 11110000 11001000 00000000 01010011 11101010
。
倒數第三位爲"0"
,說明不是偏向鎖狀態,倒數兩位爲"10"
,所以,是重量級鎖狀態,那麼前面62
位就是指向互斥量的指針。