本篇內(nèi)容介紹了“Java對(duì)象創(chuàng)建的流程”的有關(guān)知識(shí),在實(shí)際案例的操作過程中,不少人都會(huì)遇到這樣的困境,接下來就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!
網(wǎng)站建設(shè)哪家好,找創(chuàng)新互聯(lián)!專注于網(wǎng)頁設(shè)計(jì)、網(wǎng)站建設(shè)、微信開發(fā)、小程序設(shè)計(jì)、集團(tuán)企業(yè)網(wǎng)站建設(shè)等服務(wù)項(xiàng)目。為回饋新老客戶創(chuàng)新互聯(lián)還提供了康縣免費(fèi)建站歡迎大家使用!
在Java代碼中,有很多行為可以引起對(duì)象的創(chuàng)建,最為直觀的一種就是使用new關(guān)鍵字來調(diào)用一個(gè)類的構(gòu)造函數(shù)顯式地創(chuàng)建對(duì)象,這種方式在Java規(guī)范中被稱為 :由執(zhí)行類實(shí)例創(chuàng)建表達(dá)式而引起的對(duì)象創(chuàng)建。除此之外,我們還可以使用反射機(jī)制(Class類的newInstance方法、使用Constructor類的newInstance方法)、使用Clone方法、使用反序列化等方式創(chuàng)建對(duì)象。
這是我們最常見的也是最簡單的創(chuàng)建對(duì)象的方式,通過這種方式我們可以調(diào)用任意的構(gòu)造函數(shù)(無參的和有參的)去創(chuàng)建對(duì)象。比如:
Student student = new Student();
我們也可以通過Java的反射機(jī)制使用Class類的newInstance方法來創(chuàng)建對(duì)象,事實(shí)上,這個(gè)newInstance方法調(diào)用無參的構(gòu)造器創(chuàng)建對(duì)象,比如:
Student student2 = (Student)Class.forName("Student類全限定名").newInstance(); Student stu = Student.class.newInstance();
java.lang.relect.Constructor類里也有一個(gè)newInstance方法可以創(chuàng)建對(duì)象,該方法和Class類中的newInstance方法很像,但是相比之下,Constructor類的newInstance方法更加強(qiáng)大些,我們可以通過這個(gè)newInstance方法調(diào)用有參數(shù)的和私有的構(gòu)造函數(shù),比如:
public class Student { private int id; public Student(Integer id) { this.id = id; } public static void main(String[] args) throws Exception { Constructorconstructor = Student.class .getConstructor(Integer.class); Student stu3 = constructor.newInstance(123); } }
使用newInstance方法的這兩種方式創(chuàng)建對(duì)象使用的就是Java的反射機(jī)制,事實(shí)上Class的newInstance方法內(nèi)部調(diào)用的也是Constructor的newInstance方法。
無論何時(shí)我們調(diào)用一個(gè)對(duì)象的clone方法,JVM都會(huì)幫我們創(chuàng)建一個(gè)新的、一樣的對(duì)象,特別需要說明的是,用clone方法創(chuàng)建對(duì)象的過程中并不會(huì)調(diào)用任何構(gòu)造函數(shù)。簡單而言,要想使用clone方法,我們就必須先實(shí)現(xiàn)Cloneable接口并實(shí)現(xiàn)其定義的clone方法,這也是原型模式的應(yīng)用。比如:
public class Student implements Cloneable{ private int id; public Student(Integer id) { this.id = id; } @Override protected Object clone() throws CloneNotSupportedException { // TODO Auto-generated method stub return super.clone(); } public static void main(String[] args) throws Exception { Constructorconstructor = Student.class .getConstructor(Integer.class); Student stu3 = constructor.newInstance(123); Student stu4 = (Student) stu3.clone(); } }
當(dāng)我們反序列化一個(gè)對(duì)象時(shí),JVM會(huì)給我們創(chuàng)建一個(gè)單獨(dú)的對(duì)象,在此過程中,JVM并不會(huì)調(diào)用任何構(gòu)造函數(shù)。為了反序列化一個(gè)對(duì)象,我們需要讓我們的類實(shí)現(xiàn)Serializable接口,比如:
public class Student implements Cloneable, Serializable { private int id; public Student(Integer id) { this.id = id; } @Override public String toString() { return "Student [id=" + id + "]"; } public static void main(String[] args) throws Exception { Constructorconstructor = Student.class .getConstructor(Integer.class); Student stu3 = constructor.newInstance(123); // 寫對(duì)象 ObjectOutputStream output = new ObjectOutputStream( new FileOutputStream("student.bin")); output.writeObject(stu3); output.close(); // 讀對(duì)象 ObjectInputStream input = new ObjectInputStream(new FileInputStream( "student.bin")); Student stu5 = (Student) input.readObject(); System.out.println(stu5); } }
Unsafe類使Java擁有了像C語言的指針一樣操作內(nèi)存空間的能力,同時(shí)也帶來了指針的問題。過度的使用Unsafe類會(huì)使得出錯(cuò)的幾率變大,因此Java官方并不建議使用的,官方文檔也幾乎沒有。Oracle正在計(jì)劃從Java 9中去掉Unsafe類,如果真是如此影響就太大了。
我們無法直接創(chuàng)建Unsafe對(duì)象。這里我們使用反射方法得到
private static Unsafe getUnsafe() { try { Field field = Unsafe.class.getDeclaredField("theUnsafe"); field.setAccessible(true); Unsafe unsafe = (Unsafe) field.get(null); return unsafe; } catch (Exception e) { e.printStackTrace(); } return null; }
拿到這個(gè)對(duì)象后,調(diào)用其中的native方法allocateInstance 創(chuàng)建一個(gè)對(duì)象實(shí)例
Object event = unsafe.allocateInstance(Test.class);
從Java虛擬機(jī)層面看,除了使用new關(guān)鍵字創(chuàng)建對(duì)象(
Java對(duì)象的創(chuàng)建過程往往包括類初始化和類實(shí)例化兩個(gè)階段。類的初始化在前、類的實(shí)例化在后。
注意:這與spring的bean正好相反,spring的bean的生命周期,主要是先進(jìn)行實(shí)例化java對(duì)象,然后在進(jìn)行操作屬性、最后進(jìn)行初始化,這里初始化并不是java對(duì)象的初始化,而是spring的參數(shù)的初始化(initMethod、afterPropertiesSet)等。(@PostConstruct是前置攔截初始化方法)。
類的構(gòu)造器調(diào)用(
對(duì)象在可以被使用之前必須要被正確地初始化,這一點(diǎn)是Java規(guī)范規(guī)定的。在實(shí)例化一個(gè)對(duì)象時(shí),JVM首先會(huì)檢查相關(guān)類型是否已經(jīng)加載并初始化,如果沒有,則JVM立即進(jìn)行加載并調(diào)用類構(gòu)造器完成類的初始化。
注意:可以看到類的初始化主要到類構(gòu)造器變量結(jié)束執(zhí)行的時(shí)間點(diǎn)。
主要職責(zé):實(shí)例的構(gòu)造器調(diào)用(
類的實(shí)例化本身意義就是對(duì)象的概念,其實(shí)就是實(shí)例化對(duì)應(yīng)的對(duì)象的過程。
實(shí)例對(duì)象內(nèi)存的分配、實(shí)例對(duì)象參數(shù)的默認(rèn)初始化+實(shí)例對(duì)象參數(shù)的實(shí)例化(就是按開發(fā)要求的實(shí)現(xiàn)調(diào)用,例如調(diào)用構(gòu)造器
等)
此時(shí)一般處于在裝載階段的初始化完成之后,使用之前的階段,接下來就要進(jìn)行類的實(shí)例化操作
類初始化過程中或初始化完畢后,根據(jù)具體情況才會(huì)去對(duì)類進(jìn)行實(shí)例化,首先會(huì)有一下幾個(gè)步驟:
java虛擬機(jī)就會(huì)為其分配內(nèi)存來存放自己及其從父類繼承過來的實(shí)例變量
為這些實(shí)例變量分配內(nèi)存的同時(shí),這些實(shí)例變量先會(huì)被賦予默認(rèn)值(零值)【這個(gè)零值與加載階段中的準(zhǔn)備很相似,就是先賦值語義級(jí)別的默認(rèn)值,而并非參數(shù)真正的初始化】
在內(nèi)存分配完成之后調(diào)用
小結(jié):創(chuàng)建一個(gè)對(duì)象包含下面兩個(gè)過程:
類構(gòu)造器完成類初始化(賦予靜態(tài)變量默認(rèn)值)
類實(shí)例化(分配內(nèi)存、賦予默認(rèn)值、執(zhí)行定制化賦值操作)
Java虛擬機(jī)遇到一條new指令時(shí),首先將去檢查這個(gè)指令的參數(shù)是否能在常量池中定位到一個(gè)類的符號(hào)引用,并且檢查這個(gè)符號(hào)引用代表的類是否已被加載、解析和初始化過**。如果沒有,那必須先執(zhí)行相應(yīng)的類加載過程**。
在類加載并完成類初始化之后,接下來虛擬機(jī)將為新生對(duì)象分配內(nèi)存。對(duì)象所需內(nèi)存的大小在類加載及初始化完成后便可完全確定。
對(duì)象的大小在類加載完成后就已經(jīng)確定,對(duì)象在內(nèi)存中可以分為三塊。
大小確定 與類無關(guān) 與操作系統(tǒng)有關(guān),包括標(biāo)記字段和類型指針
即使父類的實(shí)例字段被子類覆蓋或者被private修飾,都照樣為其分配內(nèi)存,相同寬度的字段會(huì)分配在一起,其次,父類的字段在子類之前賦值和初始化。
滿足虛擬機(jī)對(duì)8的倍數(shù)的要求
對(duì)象分配內(nèi)存的方式。
假設(shè)Java堆中內(nèi)存是絕對(duì)規(guī)整的,所有用過的內(nèi)存都放在一邊,空閑的內(nèi)存放在另一邊,中間放著一個(gè)指針作為分界點(diǎn)的指示器,那么分配內(nèi)存就僅僅是把那個(gè)指針向空閑空間那邊挪動(dòng)一段與對(duì)象大小相等的距離,這種分配方式稱為“指針碰撞”。
如果Java堆中的內(nèi)存并不是規(guī)整的,已使用的內(nèi)存和空閑的內(nèi)存相互交錯(cuò),那就沒有辦法簡單地進(jìn)行指針碰撞了,虛擬機(jī)就必須維護(hù)個(gè)列表,記錄上哪些內(nèi)存塊是可用的,在分配的時(shí)候從列表中找到一塊足夠大的空間劃分給對(duì)象實(shí)例,并更新列表上的記錄,這種分配方式稱為“空閑列表”。
選擇哪種分配方式由Java堆是否規(guī)整決定,而Java堆是否規(guī)整又由所采用的垃圾收集器是否帶有壓縮整理功能決定。
對(duì)象創(chuàng)建在虛擬機(jī)執(zhí)行的過程中是非常頻繁的行為,僅僅修改一個(gè)指針?biāo)赶虻奈恢?,在并發(fā)情況下不是線程安全的。因此也有兩種解決方案:
使用CAS并配上失敗重試的方式保證更新操作的原子性。
(TLAB)給每一個(gè)線程在Java堆中預(yù)先分配線程私有分配緩沖區(qū),哪個(gè)線程需要分配內(nèi)存,只要在線程私有分配緩沖區(qū)中分配即可以。
將分配到的內(nèi)存空間初始化零值,這保證了實(shí)例字段不賦值可以直接使用。如果使用了TLAB,這一步可以提前到TLAB分配的時(shí)候進(jìn)行。
內(nèi)存分配完成后,虛擬機(jī)需要將分配到的內(nèi)存空間都初始化為零值(不包括對(duì)象頭),這一步操作保證了對(duì)象的實(shí)例字段在Java代碼中可以不賦初始值就直接使用,程序能訪問到這些字段的數(shù)據(jù)類型所對(duì)應(yīng)的零值。
虛擬機(jī)要對(duì)對(duì)象進(jìn)行必要的設(shè)置,例如這個(gè)對(duì)象是哪個(gè)類的實(shí)例、如何才能找到類的元數(shù)據(jù)信息、對(duì)象的哈希碼、對(duì)象的GC分代年齡等信息。這些信息存放在對(duì)象的對(duì)象頭之中,主要負(fù)責(zé)的5個(gè)點(diǎn)。
對(duì)象是哪個(gè)類的實(shí)例;
如何找到類的元數(shù)據(jù)信息;
對(duì)象的哈希碼;
對(duì)象的GC分代年齡信息;
鎖的標(biāo)識(shí)等;
執(zhí)行完以上步驟,從虛擬機(jī)角度,一個(gè)對(duì)象已經(jīng)產(chǎn)生了,但是對(duì)于java程序而言,構(gòu)造函數(shù)還沒有開始執(zhí)行。接下來按照構(gòu)造函數(shù)的要求,對(duì)對(duì)象進(jìn)行初始化即可。
對(duì)象頭主要包含兩類信息。第一類是用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù),如哈希碼、GC分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程ID、偏向時(shí)間戳等。對(duì)象頭的另外一部分是類型指針,即對(duì)象指向它的類型元數(shù)據(jù)的指針。
類型數(shù)據(jù)部分是對(duì)象真正存儲(chǔ)的有效信息,即程序代碼中定義的各種類型的字段內(nèi)容。
對(duì)齊填充:任何對(duì)象的大小都必須是8字節(jié)的整數(shù)倍。
使用句柄訪問的話,Java堆中將可能會(huì)劃分出來一塊內(nèi)存來作為句柄池。Reference變量中存放的是句柄池的地址,句柄池中存放有到對(duì)象實(shí)例數(shù)據(jù)的指針以及到對(duì)象類型數(shù)據(jù)的指針。
使用直接訪問的話,reference變量中存放的是對(duì)象的實(shí)例數(shù)據(jù)、對(duì)象的實(shí)例數(shù)據(jù)中包含有到對(duì)象類型數(shù)據(jù)的指針。
在上面工作都完成之后,從虛擬機(jī)的視角來看,一個(gè)新的對(duì)象已經(jīng)產(chǎn)生了,但從Java程序的視角來看,對(duì)象創(chuàng)建才剛開始,
檢測類是否被加載沒有加載的先加載→為新生對(duì)象分配內(nèi)存→將分配到的內(nèi)存空間都初始化為零值→**對(duì)對(duì)象進(jìn)行必要的設(shè)置(對(duì)象頭)**→執(zhí)行
需要類初始化的五種情況(有且僅有這五種) 。
遇到new getstatic putstatic invokestatic這四個(gè)指令時(shí),如果類沒有初始化,則會(huì)觸發(fā)類的初始化。
四個(gè)指令對(duì)應(yīng)的最常見的情況為使用new關(guān)鍵字實(shí)例化對(duì)象,讀取靜態(tài)變量,設(shè)置靜態(tài)變量(編譯期結(jié)果放入常量池的除外,會(huì)觸發(fā)前幾個(gè)階段) ,調(diào)用類的靜態(tài)方法。
使用java.lang.reflect包的方法對(duì)類進(jìn)行反射調(diào)用
初始化一個(gè)類時(shí) 如果父類未初始化 則初始化父類
常見的不會(huì)觸發(fā)初始化的引用方式
通過子類引用父類的靜態(tài)變量 只會(huì)初始化父類 不會(huì)初始化子類
創(chuàng)建類的數(shù)組
引用常量池內(nèi)的變量
通過名字獲取類的二進(jìn)制流并在內(nèi)存中生成class對(duì)象。
驗(yàn)證二進(jìn)制流的正確性和安全性 包括文件格式驗(yàn)證,元數(shù)據(jù)驗(yàn)證,字節(jié)碼驗(yàn)證以及符號(hào)引用驗(yàn)證四個(gè)步驟。
給類變量(static)分配空間并完成初始化 注意:如果不是final變量,值均為零值。
將符號(hào)引用解析為直接引用,包括類或接口的解析,字段解析,類方法解析,接口方法解析。
執(zhí)行類的
()方法,這個(gè)方法由所有對(duì)靜態(tài)變量的賦值操作和所有靜態(tài)代碼塊組成,虛擬機(jī)會(huì)保證父類的 ()方法在子類的方法開始之前結(jié)束,并且提供線程安全的保證(類似于double check,多個(gè)線程同時(shí)初始化時(shí)只有一個(gè)線程進(jìn)入方法,其他線程阻塞,執(zhí)行完成后其他線程不會(huì)再進(jìn)入方法)
Java推薦的類加載器的實(shí)現(xiàn)模型,除了啟動(dòng)類加載器(bootstrap classLoader)以外的所有類加載器都應(yīng)該擁有父加載器,這個(gè)關(guān)系不是通過繼承來實(shí)現(xiàn),而是通過組合的方式。類加載器收到加載請(qǐng)求時(shí),首先請(qǐng)求父加載器進(jìn)行加載,如果父加載器不能加載則調(diào)用自己的加載方法。
遵從雙親委派模型:自定義類加載器時(shí)如果我們希望則重寫findClass()方法。
不想遵循雙親委托型:方案即重寫loadClass()方法
遵循雙親委派從上到下可以分為
啟動(dòng)類加載器 (Bootstrap classLoader) 加載
擴(kuò)展類加載器 (Extension classLoader) 加載
系統(tǒng)類加載器(應(yīng)用類加載器) 加載Classpath內(nèi)的類
自定義類加載器
不遵循雙親委派的常見類加載器:
SPI (Service Provider Interface)加載器 - 線程上下文加載器 實(shí)現(xiàn)父加載器向子加載器請(qǐng)求加載
OSGi模塊化加載器 每個(gè)模塊擁有一個(gè)自定義類加載器 網(wǎng)狀結(jié)構(gòu)的加載過程
內(nèi)存分配與內(nèi)存回收緊密相關(guān),根據(jù)不同的回收策略也有不同的分配策略。
如果采用的是具有壓縮過程的垃圾回收策略,如Serial,ParNew,則Java堆中的內(nèi)存是規(guī)整的,我們只需要將內(nèi)存指針向后移內(nèi)存大小的位置即可,這種方式稱為指針碰撞(Bump the Pointer)。如果采用的回收策略沒有壓縮過程,如CMS,那虛擬機(jī)就需要維護(hù)一個(gè)列表,記錄哪些內(nèi)存是可用的,這種方式稱為空閑列表(Free List)
其次,對(duì)象創(chuàng)建也需要考慮線程安全的問題,一種方案是采用CAS+失敗重試的方法來保證線程安全,另一種方法則是為每一個(gè)線程提前分配一塊內(nèi)存,稱為本地線程分配緩沖(Thread Local Allocation Buffer , TLAB),線程創(chuàng)建對(duì)象時(shí)優(yōu)先在自己的TLAB上分配。
對(duì)象頭包括
MarkWord 32bit/64bit 取決于操作系統(tǒng)
類型指針 指向類元數(shù)據(jù)的指針
數(shù)組長度 如果是數(shù)組的話
我們主要介紹MarkWord
根據(jù)鎖狀態(tài)的不同,markword會(huì)復(fù)用自己的空間,分別記錄一些不同的信息。
我們注意到 輕量級(jí)鎖和重量級(jí)鎖狀態(tài)時(shí),會(huì)將分代年齡覆蓋掉,那當(dāng)鎖狀態(tài)解除時(shí),要怎么恢復(fù)呢?
答案是上鎖時(shí),鎖的數(shù)據(jù)中會(huì)保存一份原markword的備份
“Java對(duì)象創(chuàng)建的流程”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識(shí)可以關(guān)注創(chuàng)新互聯(lián)網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實(shí)用文章!