一、JVM對象創建流程
Ⅰ、類加載檢查——JVM創建對象時先檢查類是否加載
在虛擬機遇到new指令時,比如new關鍵字、對象克隆、對象序列化時,如下字節碼
0: new #2 // class com/example/demo/Calculate
檢查指令的參數(#2)是否能在常量池中定位到一個類的符號引用
常量池:
Constant pool:#1 = Methodref #7.#27 // java/lang/Object."<init>":()V#2 = Class #28 // my/Calculate#3 = Methodref #2.#27 // my/Calculate."<init>":()V#4 = Fieldref #29.#30 // java/lang/System.out:Ljava/io/PrintStream;#5 = Methodref #2.#31 // my/Calculate.compute:()I#6 = Methodref #32.#33 // java/io/PrintStream.println:(I)V#7 = Class #34 // java/lang/Object#8 = Utf8 <init>#9 = Utf8 ()V#10 = Utf8 Code#11 = Utf8 LineNumberTable#12 = Utf8 LocalVariableTable#13 = Utf8 this#14 = Utf8 Lmy/Calculate;#15 = Utf8 compute#16 = Utf8 ()I#17 = Utf8 a#18 = Utf8 I#19 = Utf8 b#20 = Utf8 main#21 = Utf8 ([Ljava/lang/String;)V#22 = Utf8 args#23 = Utf8 [Ljava/lang/String;#24 = Utf8 calculate#25 = Utf8 SourceFile#26 = Utf8 Calculate.java#27 = NameAndType #8:#9 // "<init>":()V#28 = Utf8 my/Calculate#29 = Class #35 // java/lang/System#30 = NameAndType #36:#37 // out:Ljava/io/PrintStream;#31 = NameAndType #15:#16 // compute:()I#32 = Class #38 // java/io/PrintStream#33 = NameAndType #39:#40 // println:(I)V#34 = Utf8 java/lang/Object#35 = Utf8 java/lang/System#36 = Utf8 out#37 = Utf8 Ljava/io/PrintStream;#38 = Utf8 java/io/PrintStream#39 = Utf8 println#40 = Utf8 (I)V
檢查符號引用代表的類是否已經被加載、校驗、準備、解析和初始化,如果沒有加載,通過類加載機制加載類。
Ⅱ、分配內存——創建對象的一大工作就是分配內存
由于類一旦被加載,就可知該類對象所占內存空間(因為對象頭大小、屬性-每個類型占用多少字節是固定的)
為對象分配內存,就是從堆或者棧(一般是堆)中為分配一塊確定大小的空間
劃分內存的方式——通過指針碰撞或者空閑列表的方式分配內存空間:
- 指針碰撞:默認使用的方式,通過一個指針標識當前已經使用到位置,指針一側是已分配的空間、另一次是未使用的空閑內存,通過指針移動對象所需空間大小來分配內存。要求java堆內存絕對規整,已用空間分配在一側。
- 空閑列表:通過維護一張空閑列表維護空閑空間的初始位置和塊大小,通過在空閑列表尋找可用的內存塊(對象所需空間>空閑塊時,該空閑塊不可用),分配并更新空閑列表。
并發分配問題——在分配內存的時必然存在多個線程為對象在堆中分配空間(堆是線程共享的區域),就是存在并發分配內存的問題,解決方法:
-
CAS鎖+失敗重試:CAS-Compare And Swap
-
TLAB:本地線程分配緩存-Thread Local Allocate Buffer,先為每個線程在java堆中分配一塊空間,當為該線程的對象分配內存時,先從預分配內存中進行分配(打破了線程競爭同一塊堆空間的問題)
-XX:+UseTLAB(默認開啟)、-XX:TLABSize設定預分配內存空間大小
Ⅲ、初始化——為分配給對象的內存空間賦0值,不包括對象頭
如果是TLAB(本地線程分配緩存)的分配方式,則初始化提前到為每個線程在java堆中分配一塊空間時進行。
這一過程使java的實例變量和類變量可以在不賦初始值就可使用,只是訪問出的是該類型的0值。
-
對于基本數據類型(如
int
、double
、char
等),如果沒有顯式初始化,它們的默認值如下:-
int
類型的變量默認值為0
。 -
double
類型的變量默認值為0.0
。 -
char
類型的變量默認值為'\u0000'
(即空字符)。 -
public class Person {int age; // 沒有初始化,默認為0String name; // 沒有初始化,默認為null } Person person = new Person(); System.out.println(person.age); // 輸出 0 System.out.println(person.name); // 輸出 null
-
-
對于對象引用類型(如類、接口、數組等),如果沒有顯式初始化,它們的默認值是
null
。 -
局部變量:在Java中,局部變量(在方法內部聲明的變量)如果不初始化就直接使用,編譯器會報錯,因為局部變量在使用前必須顯式初始化。
public void test() {int x; // 編譯錯誤:局部變量x可能尚未初始化System.out.println(x);
}
Ⅳ、設置對象頭
對象
- 對象頭
- 標記字段(Mark Word):占用內存視操作系統,32位的占4字節(32bit),64位的占8字節(64bit),包括鎖標志位、對象的hashcode、分代年齡、偏向線程ID、偏向鎖時間戳(Epoch)、鎖指針。鎖標志位內容不同則保存的對象信息不同。
- 類型指針(Klass Pointer):占用內存視是否開啟指針壓縮,開啟指針壓縮占用4字節,不開啟占用8字節,默認開啟。是指向元空間中類的元數據信息的指針,JVM通過這個指針判斷該對象是哪個類的實例。
- 數組長度(如果對象是數組類型):如果對象是數據類型,存儲數組長度,占用4字節。
- 實例數據
- 對齊填充
以下表格是32位的操作系統下默認開啟指針壓縮的對象頭:
標記字段的結構 | 類型指針 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
25bit | 4bit | 1bit | 2bit | 4字節 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
23bit | 2bit | 是否偏向鎖 | 鎖標志位 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
對象的哈希碼 | 分代年齡 | 0 | 01(無鎖) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
線程ID:持有偏向鎖的線程ID,標識哪個線程偏向該對象 | Epoch:偏向鎖的時間戳,用于批量撤銷偏向鎖 | 分代年齡 | 1 | 01(無鎖) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
指向棧中鎖記錄的指針 | 00(輕量級鎖) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
指向重量級鎖指針(操作系統級互斥鎖) | 10(重量級鎖) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
空 | 11(GC標記,表示對象待回收,由GC算法確定) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Ⅴ、執行方法
執行方法,按照程序員的意愿進行初始化,為屬性賦值(賦程序員給的值)和執行構造方法。
二、查看對象大小和指針壓縮
1、查看對象的內存布局
引入依賴
<dependency><groupId>org.openjdk.jol</groupId><artifactId>jol-core</artifactId><version>0.17</version></dependency>
示例代碼
package com.example.demo;import org.openjdk.jol.info.ClassLayout;/*** 計算對象大小*/
public class JOLSample {public static void main(String[] args) {ClassLayout layout2 = ClassLayout.parseInstance(new A());System.out.println(layout2.toPrintable());}// ‐XX:+UseCompressedOops 默認開啟的壓縮所有指針// ‐XX:+UseCompressedClassPointers 默認開啟的壓縮對象頭里的類型指針Klass Pointer// Oops : Ordinary Object Pointerspublic static class A {//8B mark word//4B Klass Pointer 如果關閉壓縮‐XX:‐UseCompressedClassPointers或‐XX:‐UseCompressedOops,則占用8Bint id; //4BString name; //4B 如果關閉壓縮‐XX:‐UseCompressedOops,則占用8Bbyte b; //1BObject o; //4B 如果關閉壓縮‐XX:‐UseCompressedOops,則占用8B}
}
在64位操作系統上的執行結果(默認開啟指針壓縮):
關閉指針壓縮后,引用類型占用的空間變成8字節:
根據提供的 JOL(Java Object Layout)輸出,以下是 com.example.demo.JOLSample$A
對象的內存布局分析:
-
對象頭(Object Header)
- Mark Word(標記字):
- 偏移量:
0
,大小:8
字節 - 值:
0x0000000000000005
- 含義:表示對象處于 可偏向狀態(
biasable
),分代年齡為0
(age: 0
),存儲鎖、GC 狀態等信息。
- 偏移量:
- Klass Word(類指針):
- 偏移量:
8
,大小:4
字節 - 值:
0xf800cf18
- 含義:指向類元數據的指針(JVM 開啟指針壓縮后為 4 字節)。
- 偏移量:
- Mark Word(標記字):
-
實例字段(Instance Fields)
int id
:- 偏移量:
12
,大小:4
字節 - 值:
0
(默認初始值)。
- 偏移量:
byte b
:- 偏移量:
16
,大小:1
字節 - 值:
0
(默認初始值)。
- 偏移量:
- 對齊填充(Padding Gap):
- 偏移量:
17
,大小:3
字節 - 原因:下一個字段
String name
需對齊到 4 字節邊界(20
是 4 的倍數),因此在byte b
后填充 3 字節。
- 偏移量:
String name
:- 偏移量:
20
,大小:4
字節 - 值:
null
(引用類型,指針壓縮后占 4 字節)。
- 偏移量:
Object o
:- 偏移量:
24
,大小:4
字節 - 值:
null
(引用類型)。
- 偏移量:
-
對象對齊填充(Object Alignment Gap)
- 偏移量:
28
,大小:4
字節 - 原因:對象總大小需對齊至 8 字節(64 位 JVM 的默認對齊)。當前已用 28 字節(
0~27
),需填充至32
字節(28 + 4 = 32
)。
- 偏移量:
關鍵指標
- 實例總大小(Instance Size):
32
字節。 - 空間損失(Space Losses):
- 內部(Internal):
3
字節(字段間填充)。 - 外部(External):
4
字節(對象末尾填充)。 - 總計損失:
7
字節。
- 內部(Internal):
內存布局圖示
偏移量 | 大小(字節) | 內容 | 說明 |
---|---|---|---|
0 | 8 | Mark Word | 鎖、GC 狀態等 |
8 | 4 | Klass Word | 類元數據指針 |
12 | 4 | int id | 整型字段 |
16 | 1 | byte b | 字節字段 |
17 | 3 | 對齊填充 | 補齊至 4 字節邊界 |
20 | 4 | String name | 字符串引用(null ) |
24 | 4 | Object o | 對象引用(null ) |
28 | 4 | 對象對齊填充 | 補齊至 8 字節邊界 |
總結
- 對象頭占 12 字節(
8 + 4
),字段數據占 13 字節(4 + 1 + 4 + 4
),但實際占用 20 字節(含內部填充)。 - JVM 通過填充確保字段對齊和對象對齊,提高內存訪問效率。
- 優化建議:若需減少空間,可調整字段順序(如將
byte b
放在末尾),但 JVM 會自動重排,通常無需手動干預。
2、指針壓縮的JVM配置參數
‐XX:+UseCompressedOops :開啟的壓縮所有指針,默認開啟
‐XX:+UseCompressedClassPointers :開啟的壓縮對象頭里的類型指針Klass Pointer,默認開啟
3、為什么要有指針壓縮
1、在64位的平臺中節約空間和帶寬:在主內存和緩存之間復制較大指針會占用更多帶寬;
2、32位地址最大支持4G內存,通過對對象指針的壓縮編碼、解碼以支持更大的內存配置(不超過32G);
3、堆內存小于4G時不需要開啟指針壓縮,JVM會自動去除高32位地址,使用低虛擬地址空間;
4、堆內存大于32G時,壓縮指針失效,強制使用64位對java對象尋址。(所以堆內存不建議大于32G)
ressedOops :開啟的壓縮所有指針,默認開啟
‐XX:+UseCompressedClassPointers :開啟的壓縮對象頭里的類型指針Klass Pointer,默認開啟
3、為什么要有指針壓縮
1、在64位的平臺中節約空間和帶寬:在主內存和緩存之間復制較大指針會占用更多帶寬;
2、32位地址最大支持4G內存,通過對對象指針的壓縮編碼、解碼以支持更大的內存配置(不超過32G);
3、堆內存小于4G時不需要開啟指針壓縮,JVM會自動去除高32位地址,使用低虛擬地址空間;
4、堆內存大于32G時,壓縮指針失效,強制使用64位對java對象尋址。(所以堆內存不建議大于32G)