您好,登錄后才能下訂單哦!
這篇文章主要介紹“怎么創(chuàng)建Java對象”,在日常操作中,相信很多人在怎么創(chuàng)建Java對象問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”怎么創(chuàng)建Java對象”的疑惑有所幫助!接下來,請跟著小編一起來學(xué)習(xí)吧!
一、創(chuàng)建對象的方式
使用 new 關(guān)鍵字
這是創(chuàng)建一個對象最通用、常規(guī)的方法,同時也是最簡單的方式。通過使用此方法,我們可以調(diào)用任何要調(diào)用的構(gòu)造函數(shù)(默認(rèn)使用無參構(gòu)造函數(shù))
Person p = new Person();
使用 Class 類的 newInstance(),只能調(diào)用空參的構(gòu)造器,權(quán)限必須為 public
//獲取類對象 Class aClass = Class.forName("priv.starfish.Person"); Person p1 = (Person) aClass.newInstance();
Constructor 的 newInstance(xxx),對構(gòu)造器沒有要求
Class aClass = Class.forName("priv.starfish.Person"); //獲取構(gòu)造器 Constructor constructor = aClass.getConstructor(); Person p2 = (Person) constructor.newInstance();
clone()
深拷貝,需要實現(xiàn) Cloneable 接口并實現(xiàn) clone(),不調(diào)用任何的構(gòu)造器
Person p3 = (Person) p.clone();
反序列化
通過序列化和反序列化技術(shù)從文件或者網(wǎng)絡(luò)中獲取對象的二進(jìn)制流。
每當(dāng)我們序列化和反序列化對象時,JVM 會為我們創(chuàng)建了一個獨立的對象。在 deserialization 中,JVM 不使用任何構(gòu)造函數(shù)來創(chuàng)建對象。(序列化的對象需要實現(xiàn) Serializable)
//準(zhǔn)備一個文件用于存儲該對象的信息 File f = new File("person.obj"); FileOutputStream fos = new FileOutputStream(f); ObjectOutputStream oos = new ObjectOutputStream(fos); //序列化對象,寫入到磁盤中 oos.writeObject(p); //反序列化 FileInputStream fis = new FileInputStream(f); ObjectInputStream ois = new ObjectInputStream(fis); //反序列化對象 Person p4 = (Person) ois.readObject();
第三方庫 Objenesls
Java已經(jīng)支持通過 Class.newInstance() 動態(tài)實例化 Java 類,但是這需要Java類有個適當(dāng)?shù)臉?gòu)造器。很多時候一個Java類無法通過這種途徑創(chuàng)建,例如:構(gòu)造器需要參數(shù)、構(gòu)造器有副作用、構(gòu)造器會拋出異常。Objenesis 可以繞過上述限制
二、創(chuàng)建對象的步驟
這里討論的僅僅是普通 Java 對象,不包含數(shù)組和 Class 對象(普通對象和數(shù)組對象的創(chuàng)建指令是不同的。創(chuàng)建類實例的指令:new,創(chuàng)建數(shù)組的指令:newarray,anewarray,multianewarray)
1. new指令
虛擬機遇到一條 new 指令時,首先去檢查這個指令的參數(shù)是否能在 Metaspace 的常量池中定位到一個類的符號引用,并且檢查這個符號引用代表的類是否已被加載、解析和初始化過(即判斷類元信息是否存在)。如果沒有,那么須在雙親委派模式下,先執(zhí)行相應(yīng)的類加載過程。
2. 分配內(nèi)存
接下來虛擬機將為新生代對象分配內(nèi)存。對象所需的內(nèi)存的大小在類加載完成后便可完全確定。如果實例成員變量是引用變量,僅分配引用變量空間即可,即 4 個字節(jié)大小。分配方式有“指針碰撞(Bump the Pointer)”和“空閑列表(Free List)”兩種方式,具體由所采用的垃圾收集器是否帶有壓縮整理功能決定。
如果內(nèi)存是規(guī)整的,就采用“指針碰撞”來為對象分配內(nèi)存。意思是所有用過的內(nèi)存在一邊,空閑的內(nèi)存在另一邊,中間放著一個指針作為分界點的指示器,分配內(nèi)存就僅僅是把指針指向空閑那邊挪動一段與對象大小相等的距離罷了。如果垃圾收集器采用的是 Serial、ParNew 這種基于壓縮算法的,就采用這種方法。(一般使用帶整理功能的垃圾收集器,都采用指針碰撞)
如果內(nèi)存是不規(guī)整的,虛擬機需要維護(hù)一個列表,這個列表會記錄哪些內(nèi)存是可用的,在為對象分配內(nèi)存的時候從列表中找到一塊足夠大的空間劃分給該對象實例,并更新列表內(nèi)容,這種分配方式就是“空閑列表”。使用CMS 這種基于Mark-Sweep 算法的收集器時,通常采用空閑列表。
我們都知道堆內(nèi)存是線程共享的,那在分配內(nèi)存的時候就會存在并發(fā)安全問題,JVM 是如何解決的呢?
一般有兩種解決方案:
對分配內(nèi)存空間的動作做同步處理,采用 CAS 機制,配合失敗重試的方式保證更新操作的原子性
每個線程在 Java 堆中預(yù)先分配一小塊內(nèi)存,然后再給對象分配內(nèi)存的時候,直接在自己這塊"私有"內(nèi)存中分配,當(dāng)這部分區(qū)域用完之后,再分配新的"私有"內(nèi)存。這種方案稱為TLAB(Thread Local Allocation Buffer),這部分 Buffer 是從堆中劃分出來的,但是是本地線程獨享的。
這里值得注意的是,我們說 TLAB 是線程獨享的,只是在“分配”這個動作上是線程獨占的,至于在讀取、垃圾回收等動作上都是線程共享的。而且在使用上也沒有什么區(qū)別。另外,TLAB 僅作用于新生代的 Eden Space,對象被創(chuàng)建的時候首先放到這個區(qū)域,但是新生代分配不了內(nèi)存的大對象會直接進(jìn)入老年代。因此在編寫 Java 程序時,通常多個小的對象比大的對象分配起來更加高效。
虛擬機是否使用 TLAB 是可以選擇的,可以通過設(shè)置 -XX:+/-UseTLAB 參數(shù)來指定,JDK8 默認(rèn)開啟。
3. 初始化
內(nèi)存分配完成后,虛擬機需要將分配到的內(nèi)存空間都初始化為零值(不包括對象頭),這一步操作保證了對象的實例字段在 Java 代碼中可以不賦初始值就直接使用,程序能訪問到這些字段的數(shù)據(jù)類型所對應(yīng)的零值。如:byte、short、long 轉(zhuǎn)化為對象后初始值為 0,Boolean 初始值為 false。
4. 對象的初始設(shè)置(設(shè)置對象的對象頭)
接下來虛擬機要對對象進(jìn)行必要的設(shè)置,例如這個對象是哪個類的實例、如何才能找到類的元數(shù)據(jù)信息、對象的哈希碼、對象的GC分代年齡等信息。這些信息存放在對象的對象頭(Object Header)之中。根據(jù)虛擬機當(dāng)前的運行狀態(tài)的不同,如對否啟用偏向鎖等,對象頭會有不同的設(shè)置方式。
5.<init>方法初始化
在上面的工作都完成了之后,從虛擬機的角度看,一個新的對象已經(jīng)產(chǎn)生了,但是從 Java 程序的角度看,對象創(chuàng)建才剛剛開始,
所以,一般來說,執(zhí)行 new 指令后接著執(zhí)行 init 方法,把對象按照程序員的意愿進(jìn)行初始化(應(yīng)該是將構(gòu)造函數(shù)中的參數(shù)賦值給對象的字段),這樣一個真正可用的對象才算完全產(chǎn)生出來。
三、對象的內(nèi)存布局
在 HotSpot 虛擬機中,對象在內(nèi)存中存儲的布局可以分為 3 塊區(qū)域:對象頭(Header)、實例數(shù)據(jù)(Instance Data)、對其填充(Padding)。
對象頭
HotSpot 虛擬機的對象頭包含兩部分信息。
第一部分用于存儲對象自身的運行時數(shù)據(jù),如哈希碼(HashCode)、GC分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程ID、偏向時間戳等。
對象的另一部分類型指針,即對象指向它的類元數(shù)據(jù)的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例(并不是所有的虛擬機實現(xiàn)都必須在對象數(shù)據(jù)上保留類型指針,也就是說,查找對象的元數(shù)據(jù)信息并不一定要經(jīng)過對象本身)。
如果對象是一個 Java 數(shù)組,那在對象頭中還必須有一塊用于記錄數(shù)組長度的數(shù)據(jù)。
元數(shù)據(jù):描述數(shù)據(jù)的數(shù)據(jù)。對數(shù)據(jù)及信息資源的描述信息。在 Java 中,元數(shù)據(jù)大多表示為注解。
實例數(shù)據(jù)
實例數(shù)據(jù)部分是對象真正存儲的有效信息,也是在程序代碼中定義的各種類型的字段內(nèi)容,無論從父類繼承下來的,還是在子類中定義的,都需要記錄起來。這部分的存儲順序會受虛擬機默認(rèn)的分配策略參數(shù)和字段在 Java 源碼中定義的順序影響(相同寬度的字段總是被分配到一起)。
規(guī)則:
相同寬度的字段總是被分配在一起
父類中定義的變量會出現(xiàn)在子類之前
如果 CompactFields 參數(shù)為 true(默認(rèn)true),子類的窄變量可能插入到父類變量的空隙
對齊填充
對齊填充部分并不是必然存在的,也沒有特別的含義,它僅僅起著占位符的作用。由于 HotSpot VM 的自動內(nèi)存管理系統(tǒng)要求對象的起始地址必須是 8 字節(jié)的整數(shù)倍,也就是說,對象的大小必須是 8 字節(jié)的整數(shù)倍。而對象頭部分正好是 8 字節(jié)的倍數(shù)(1倍或者2倍),因此,當(dāng)對象實例數(shù)據(jù)部分沒有對齊時,就需要通過對齊填充來補全。
我們通過一個簡單的例子加深下理解
public class PersonObject { public static void main(String[] args) { Person person = new Person(); } } public class Person { int id = 1008; String name; Department department; { name = "匿名用戶"; //name賦值為字符串常量 } } public class Department { int id; String name; }
四、對象的訪問定位
我們創(chuàng)建對象的目的,肯定是為了使用它,那 JVM 是如何通過棧幀中的對象引用訪問到其內(nèi)存的對象實例呢?
由于 reference 類型在 Java 虛擬機規(guī)范里只規(guī)定了一個指向?qū)ο蟮囊?,并沒有定義這個引用應(yīng)該通過哪種方式去定位,以及訪問到 Java 堆中的對象的具體位置,因此不同虛擬機實現(xiàn)的對象訪問方式會有所不同,主流的訪問方式有兩種:
句柄訪問
如果使用句柄訪問方式,Java堆中會劃分出一塊內(nèi)存來作為句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例數(shù)據(jù)和類型數(shù)據(jù)各自的具體地址信息。使用句柄方式最大的好處就是reference中存儲的是穩(wěn)定的句柄地址,在對象被移動(垃圾收集時移動對象是非常普遍的行為)時只會改變句柄中的實例數(shù)據(jù)指針,而reference本身不需要被修改。
直接指針(Hotspot 使用該方式)
如果使用該方式,Java堆對象的布局就必須考慮如何放置訪問類型數(shù)據(jù)的相關(guān)信息,reference中直接存儲的就是對象地址。使用直接指針方式最大的好處就是速度更快,他節(jié)省了一次指針定位的時間開銷。
到此,關(guān)于“怎么創(chuàng)建Java對象”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識,請繼續(xù)關(guān)注億速云網(wǎng)站,小編會繼續(xù)努力為大家?guī)砀鄬嵱玫奈恼拢?/p>
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。