深入理解Java虛擬機:對象實例化及直接內(nèi)存詳解
前言
在Java世界中,萬物皆對象。當我們談論一個對象時,其實質(zhì)是指代一段具有特定屬性和行為的內(nèi)存區(qū)域。在JVM的視角下,對象的存儲并非簡單的字節(jié)序列堆積,而是遵循著嚴謹?shù)慕Y(jié)構(gòu)設計與管理規(guī)則。從創(chuàng)建到消亡,一個Java對象在JVM中的生命歷程主要圍繞堆內(nèi)存展開,而堆正是JVM內(nèi)存模型中用于存儲對象實例的主要區(qū)域。
本文將圍繞對象的實例化、對象內(nèi)存布局、對象的訪問定位和直接內(nèi)存展開介紹說明。
對象實例化
圖片
創(chuàng)建對象的方式
最常見的方式new、Xxx的靜態(tài)方法,XxxBuilder/XxxFactory的靜態(tài)方法:
Student student = new Student();
Class的newInstance方法:反射的方式,只能調(diào)用空參的構(gòu)造器,權限必須是public:
Class clazz = Class.forName("org.yian.Student");
Student student = (Student) clazz.newInstance();
Constructor的newInstance(XXX):反射的方式,可以調(diào)用空參、帶參的構(gòu)造器,權限沒有要求:
Student.class.getConstructor().newInstance();
使用clone():不調(diào)用任何的構(gòu)造器,要求當前的類需要實現(xiàn)Cloneable接口,實現(xiàn)clone():
Student clone = student.clone();
使用序列化:從文件中、從網(wǎng)絡中獲取一個對象的二進制流:
public class Student implements Serializable {
private String name;
private Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public void say() {
System.out.println("hello i'm yian!"+this.age);
}
public static void main(String[] args) throws Exception {
Student student = new Student();
student.setAge(10);
String filePath = "com";
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filePath));
oos.writeObject(student);
oos.close();
System.out.println("序列化完成!");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filePath));
Student student1 = (Student) ois.readObject();
ois.close();
student1.say();
System.out.println("反序列化完成!");
}
}
第三方庫 Objenesis:
Objenesis objenesis = new ObjenesisStd();
ObjectInstantiator<Student> instantiator = objenesis.getInstantiatorOf(Student.class);
Student st = instantiator.newInstance();
創(chuàng)建對象的步驟
字節(jié)碼分析對象創(chuàng)建
javap -v -p Student.class:
圖片
- new:會首先檢查這個Class有沒有加載,即加載、鏈接、初始化?并按照編譯中的大小信息分配空間,進行創(chuàng)建對象 ,并對其臨時初始化
- dup:第一句new會在操作數(shù)棧中生成一個指向該對象的引用,dup指令會將這個引用再復制一遍,放到操作數(shù)棧的棧頂,上面那個引用是作為一個句柄指向方法區(qū)的對應方法。
- invokespecial :進行真實初始化,為其賦實際的初始值【即調(diào)用構(gòu)造器方法<init>】
注意:
<init>與<clinit>的區(qū)別:前者是一個類的構(gòu)造器在字節(jié)碼中對應的方法,后者習慣被稱為類構(gòu)造器方法,他會在類加載的初始化階段對類的靜態(tài)部分進行初始化【如靜態(tài)代碼塊,靜態(tài)成員變量等】
JVM創(chuàng)建對象的步驟
1.判斷對象對應的類是否加載、鏈接、初始化
虛擬機遇到一條new指令,首先去檢查這個指令的參數(shù)能否在Metaspace的常量池中定位到一個類的符號引用,并且檢查這個符號引用代表的類是否已經(jīng)被加載,解析和初始化(即判斷類元信息是否存在)
如果沒有,那么在雙親委派模式下,使用當前類加載器以ClassLoader + 包名 + 類名為key進行查找對應的.class文件;
- 如果沒有找到文件,則拋出ClassNotFoundException異常
- 如果找到,則進行類加載,并生成對應的Class對象
2.為對象分配內(nèi)存
首先計算對象占用空間的大小,接著在堆中劃分一塊內(nèi)存給新對象。如果實例成員變量是引用變量,僅分配引用變量空間即可,即4個字節(jié)大小
如果內(nèi)存規(guī)整:虛擬機將采用的是指針碰撞法(Bump The Point)來為對象分配內(nèi)存。
- 所有用過的內(nèi)存在一邊,空閑的內(nèi)存放另外一邊,中間放著一個指針作為分界點的指示器,分配內(nèi)存就僅僅是把指針指向空閑那邊挪動一段與對象大小相等的距離罷了。如果垃圾收集器選擇的是Serial、ParNew這種基于壓縮算法的,虛擬機采用這種分配方式。一般使用帶Compact(整理)過程的收集器時,使用指針碰撞
如果內(nèi)存不規(guī)整:虛擬機需要維護一個空閑列表(Free List)來為對象分配內(nèi)存
- 已使用的內(nèi)存和未使用的內(nèi)存相互交錯,那么虛擬機將采用的是空閑列表來為對象分配內(nèi)存。意思是虛擬機維護了一個列表,記錄上那些內(nèi)存塊是可用的,再分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,并更新列表上的內(nèi)容
選擇哪種分配方式由Java堆是否規(guī)整所決定,而Java堆是否規(guī)整又由所采用的垃圾收集器是否帶有壓縮整理功能決定。
3.處理并發(fā)問題
- 采用CAS失敗重試、區(qū)域加鎖保證更新的原子性
- 每個線程預先分配一塊TLAB:通過設置-XX:+UseTLAB參數(shù)來設定
4.初始化分配到的內(nèi)存
所有屬性設置默認值,保證對象實例字段在不賦值時可以直接使用
5.設置對象的對象頭
- Mark Word:存儲對象自身的運行時元數(shù)據(jù),如哈希碼(HashCode)、GC分代年齡、鎖狀態(tài)標志、偏向線程ID等。這些信息直接影響對象的內(nèi)存管理、線程同步以及方法調(diào)用等操作
- 類型指針:指向方法區(qū)中的類型信息,如類的元數(shù)據(jù)、方法表、常量池等。通過類型指針,JVM能夠快速定位到對象所屬類的詳細定義,實現(xiàn)方法調(diào)度、字段訪問等操作
6.執(zhí)行init方法進行初始化
在Java程序的視角看來,初始化才正式開始。初始化成員變量,執(zhí)行實例化代碼塊,調(diào)用類的構(gòu)造方法,并把堆內(nèi)對象的首地址賦值給引用變量
對象初始化初始化順序為:默認 -> 顯式或靜態(tài)代碼塊 -> 構(gòu)造方法 -> setter
對象內(nèi)存布局
圖片
對象頭
對象頭包含了兩部分,分別是運行時元數(shù)據(jù)和類型指針。如果是數(shù)組,還需要記錄數(shù)組的長度。
運行時元數(shù)據(jù):
- 哈希值(HashCode)
- GC分代年齡
- 鎖狀態(tài)標志
- 線程持有的鎖
- 偏向線程ID
- 偏向時間戳
類型指針:
- 指向類元數(shù)據(jù)InstanceKlass,確定該對象所屬的類型
實例數(shù)據(jù)
它是對象真正存儲的有效信息,包括程序代碼中定義的各種類型的字段(包括從父類繼承下來的和本身擁有的字段)
- 相同寬度的字段總是被分配在一起
- 父類中定義的變量會出現(xiàn)在子類之前
- 如果CompactFields參數(shù)為true(默認為true):子類的窄變量可能插入到父類變量的空隙
對齊填充
不是必須的,也沒有特別的含義,僅僅起到占位符的作用
小結(jié):
public class Customer{
int id = 1001;
String name;
Account acct;
{
name = "匿名客戶";
}
public Customer() {
acct = new Account();
}
}
public class CustomerTest{
public static void main(string[] args){
Customer cust = new Customer();
}
}
圖片
對象的訪問定位
圖片
JVM是如何通過棧幀中的對象引用訪問到其內(nèi)部的對象實例?
句柄訪問
圖片
reference中存儲穩(wěn)定句柄地址,對象被移動(垃圾收集時移動對象很普遍)時只會改變句柄中實例數(shù)據(jù)指針即可,reference本身不需要被修改
直接指針(HotSpot采用)
圖片
直接指針是局部變量表中的引用,直接指向堆中的實例,在對象實例中有類型指針,指向的是方法區(qū)中的對象類型數(shù)據(jù)
直接內(nèi)存
不是虛擬機運行時數(shù)據(jù)區(qū)的一部分,也不是《Java虛擬機規(guī)范》中定義的內(nèi)存區(qū)域。直接內(nèi)存是在Java堆外的、直接向系統(tǒng)申請的內(nèi)存區(qū)間。來源于NIO,通過存在堆中的DirectByteBuffer操作Native內(nèi)存。通常,訪問直接內(nèi)存的速度會優(yōu)于Java堆,即讀寫性能高
非直接緩存區(qū)
圖片
使用IO讀寫文件,需要與磁盤交互,需要由用戶態(tài)切換到內(nèi)核態(tài)。在內(nèi)核態(tài)時,需要兩份內(nèi)存存儲重復數(shù)據(jù),效率低。
直接緩存區(qū)
圖片
使用NIO時,操作系統(tǒng)劃出的直接緩存區(qū)可以被java代碼直接訪問,只有一份。NIO適合對大文件的讀寫操作。