在 Java 程序中,我們擁有多種新建對象的方式曹洽。除了最為常見的 new 語句之外瓤檐,我們還可以通過反射機制、Object.clone 方法排截、反序列化以及 Unsafe.allocateInstance 方法來新建對象
Object.clone 方法和反序列化通過直接復(fù)制已有的數(shù)據(jù)嫌蚤,來初始化新建對象的實例字段。Unsafe.allocateInstance 方法則沒有初始化實例字段断傲,而 new 語句和反射機制脱吱,則是通過調(diào)用構(gòu)造器來初始化實例字段。
new 語句為例,字節(jié)碼將包含用來請求內(nèi)存的 new 指令认罩,以及用來調(diào)用構(gòu)造器的 invokespecial 指令箱蝠。
public class TestFoo {
public static void main(String[] args) {
TestFoo foo = new TestFoo();
}
}
對應(yīng)字節(jié)碼
public static main([Ljava/lang/String;)V
L0
LINENUMBER 5 L0
NEW top/zcwfeng/java/test/TestFoo
DUP
INVOKESPECIAL top/zcwfeng/java/test/TestFoo.<init> ()V
ASTORE 1
如果一個類沒有定義任何構(gòu)造器的話,java編譯器會自動添加一個無參數(shù)的構(gòu)造器
public <init>()V
L0
LINENUMBER 3 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
壓縮指針
在 Java 虛擬機中垦垂,每個 Java 對象都有一個對象頭(object header)宦搬,這個由標記字段(Mark Word)和類型指針(Klass Pointer)所構(gòu)成。其中劫拗,標記字段用以存儲 Java 虛擬機有關(guān)該對象的運行數(shù)據(jù)间校,如哈希碼、GC 信息以及鎖信息页慷,而類型指針則指向該對象的類憔足。
在 64 位的 Java 虛擬機中,對象頭的標記字段占 64 位酒繁,而類型指針又占了 64 位滓彰。也就是說,每一個 Java 對象在內(nèi)存中的額外開銷就是 16 個字節(jié)州袒。以 Integer 類為例揭绑,它僅有一個 int 類型的私有字段,占 4 個字節(jié)郎哭。因此洗做,每一個 Integer 對象的額外內(nèi)存開銷至少是 400%弓叛。這也是為什么 Java 要引入基本類型的原因之一。
為了盡量較少對象的內(nèi)存使用量诚纸,64 位 Java 虛擬機引入了壓縮指針 [1] 的概念(對應(yīng)虛擬機選項 -XX:+UseCompressedOops,默認開啟)陈惰,將堆中原本 64 位的 Java 對象指針壓縮成 32 位的畦徘。
這樣一來,對象頭中的類型指針也會被壓縮成 32 位抬闯,使得對象頭的大小從 16 字節(jié)降至 12 字節(jié)井辆。當然,壓縮指針不僅可以作用于對象頭的類型指針溶握,還可以作用于引用類型的字段杯缺,以及引用類型數(shù)組。
上面是官方的解釋睡榆。我們弄明白幾個問題:
32位操作系統(tǒng)可以尋址到多大內(nèi)存
答:4g 因為 2^32=4 * 1024 * 1024=4g
64位呢萍肆?
2的64次方bai:18446744073709551616
這個數(shù)有點大,計算器一般算不出來胀屿,編程的話用long值才能計算到2的62次方
答:64位過長塘揣,給我們尋址帶寬和對象內(nèi)引用造成了負擔(dān)
一個對象占用的字節(jié)數(shù)
對象頭:
32位系統(tǒng),占用 8 字節(jié)(markWord4字節(jié)+kclass4字節(jié))
64位系統(tǒng)宿崭,開啟 UseCompressedOops(壓縮指針)時亲铡,占用 12 字節(jié),否則是16字節(jié)(markWord8字節(jié)+kclass8字節(jié)葡兑,開啟時markWord8字節(jié)+kclass4字節(jié))
實例數(shù)據(jù)
boolean 1
byte 1
short 2
char 2
int 4
float 4
long 8
double 8
引用類型
32位系統(tǒng)占4字節(jié) (因為此引用類型要去方法區(qū)中找類信息,所以地址為32位即4字節(jié)同理64位是8字節(jié))
64位系統(tǒng)奖蔓,開啟 UseCompressedOops時,占用4字節(jié)讹堤,否則是8字節(jié)
對齊填充
如果對象頭+實例數(shù)據(jù)的值不是8的倍數(shù)吆鹤,那么會補上一些,補夠8的倍數(shù)
32位操作系統(tǒng) 花費的內(nèi)存空間為
對象頭-8字節(jié) + 實例數(shù)據(jù) int類型-4字節(jié) + 引用類型-4字節(jié)+補充0字節(jié)(16是8的倍數(shù)) 16個字節(jié)
64位操作系統(tǒng)(未開啟指針壓縮)
對象頭-16字節(jié) + 實例數(shù)據(jù) int類型-4字節(jié) + 引用類型-8字節(jié)+補充4字節(jié)(28不是8的倍數(shù)補充4字節(jié)到達32字節(jié)) 32個字節(jié)
同樣的對象需要將近兩倍的容量,(實際平均1.5倍)
64位開啟壓縮指針
對象頭-12字節(jié) + 實例數(shù)據(jù) int類型-4字節(jié) + 引用類型-4字節(jié)+補充0字節(jié)=24個字節(jié)---減緩堆空間的壓力(同樣的內(nèi)存更不容易發(fā)生oom)
JVM的實現(xiàn)方式是
不再保存所有引用蜕劝,而是每隔8個字節(jié)保存一個引用檀头。例如,原來保存每個引用0岖沛、1暑始、2…,現(xiàn)在只保存0婴削、8廊镜、16…。因此唉俗,指針壓縮后嗤朴,并不是所有引用都保存在堆中配椭,而是以8個字節(jié)為間隔保存引用。
在實現(xiàn)上雹姊,堆中的引用其實還是按照0x0股缸、0x1、0x2…進行存儲吱雏。只不過當引用被存入64位的寄存器時敦姻,JVM將其左移3位(相當于末尾添加3個0),例如0x0歧杏、0x1镰惦、0x2…分別被轉(zhuǎn)換為0x0、0x8犬绒、0x10旺入。而當從寄存器讀出時,JVM又可以右移3位凯力,丟棄末尾的0茵瘾。(oop在堆中是32位,在寄存器中是35位沮协,2的35次方=32G龄捡。也就是說,使用32位慷暂,來達到35位oop所能引用的堆內(nèi)存空間)
哪些信息會被壓縮聘殖?
1.對象的全局靜態(tài)變量(即類屬性)
2.對象頭信息:64位平臺下,原生對象頭大小為16字節(jié)行瑞,壓縮后為12字節(jié)
3.對象的引用類型:64位平臺下奸腺,引用類型本身大小為8字節(jié),壓縮后為4字節(jié)
4.對象數(shù)組類型:64位平臺下血久,數(shù)組類型本身大小為24字節(jié)突照,壓縮后16字節(jié)
哪些信息不會被壓縮?
1.指向非Heap的對象指針
2.局部變量氧吐、傳參讹蘑、返回值、NULL指針
在JVM中(不管是32位還是64位)筑舅,對象已經(jīng)按8字節(jié)邊界對齊了座慰。對于大部分處理器,這種對齊方案都是最優(yōu)的翠拣。所以版仔,使用壓縮的oop并不會帶來什么損失,反而提升了性能。
看一個實例
class A {
long l;
int i;
}
class B extends A {
long l;
int i;
}
開啟壓縮指針 開啟(-XX:+UseCompressedOops) 默認開啟
> Task :TestFoo.main()
------------B---------------
# WARNING: Unable to get Instrumentation. Dynamic Attach failed. You may add this JAR as -javaagent manually, or supply -Djdk.attach.allowAttachSelf
top.zcwfeng.java.test.B object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 50 04 06 00 (01010000 00000100 00000110 00000000) (394320)
12 4 int A.i 0
16 8 long A.l 0
24 8 long B.l 0
32 4 int B.i 0
36 4 (loss due to the next object alignment)
Instance size: 40 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
================
關(guān)閉壓縮指針 關(guān)閉(-XX:-UseCompressedOops) 可以關(guān)閉壓縮指針
> Task :TestFoo.main()
------------B---------------
# WARNING: Unable to get Instrumentation. Dynamic Attach failed. You may add this JAR as -javaagent manually, or supply -Djdk.attach.allowAttachSelf
top.zcwfeng.java.test.B object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 48 12 e0 a1 (01001000 00010010 11100000 10100001) (-1579150776)
12 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
16 8 long A.l 0
24 4 int A.i 0
28 4 (alignment/padding gap)
32 8 long B.l 0
40 4 int B.i 0
44 4 (loss due to the next object alignment)
Instance size: 48 bytes
Space losses: 4 bytes internal + 4 bytes external = 8 bytes total
字段重排列
Java 虛擬機重新分配字段的先后順序蛮粮,以達到內(nèi)存對齊的目的益缎。Java 虛擬機中有三種排列方法(對應(yīng) Java 虛擬機選項 -XX:FieldsAllocationStyle,默認值為 1)然想,但都會遵循如下兩個規(guī)則莺奔。
其一,如果一個字段占據(jù) C 個字節(jié)又沾,那么該字段的偏移量需要對齊至 NC弊仪。這里偏移量指的是字段地址與對象的起始地址差值。
以 long 類為例杖刷,它僅有一個 long 類型的實例字段。在使用了壓縮指針的 64 位虛擬機中驳癌,盡管對象頭的大小為 12 個字節(jié)滑燃,該 long 類型字段的偏移量也只能是 16,而中間空著的 4 個字節(jié)便會被浪費掉颓鲜。
其二表窘,子類所繼承字段的偏移量,需要與父類對應(yīng)字段的偏移量保持一致甜滨。
在具體實現(xiàn)中乐严,Java 虛擬機還會對齊子類字段的起始位置。對于使用了壓縮指針的 64 位虛擬機衣摩,子類第一個字段需要對齊至 4N昂验;而對于關(guān)閉了壓縮指針的 64 位虛擬機,子類第一個字段則需要對齊至 8N艾扮。
上面的分析既琴,加入了工具JOL的幫助
gradle 配置
implementation 'org.openjdk.jol:jol-core:0.14'
java 環(huán)境
java 11.0.10 2021-01-19 LTS
Java(TM) SE Runtime Environment 18.9 (build 11.0.10+8-LTS-162)
Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11.0.10+8-LTS-162, mixed mode)
當然Java8 版本有的也可以,我的失敗了泡嘴,為了方便所有我選擇存在的環(huán)境11
然后調(diào)用可以分析:
System.out.println("------------B---------------");
B o = new B();
String s = ClassLayout.parseInstance(o).toPrintable();
System.out.println(s);