您好,登錄后才能下訂單哦!
類的初始化與實例化
一個 Java 對象的創建過程往往包括類的初始化 和 實例化 兩個階段。
Java 規范規定一個對象在可以被使用之前必須要被正確地初始化。在類初始化過程中或初始化完畢后,根據具體情況才會去對類進行實例化。在實例化一個對象時,JVM 首先會檢查相關類型是否已經加載并初始化,如果沒有,則 JVM 立即進行加載并調用類構造器完成類的初始化。
Java 對象的創建方式
一個對象在可以被使用之前必須要被正確地實例化。在 Java 程序中,有多種方法可以創建對象,最直接的一種就是使用 new 關鍵字來調用一個類的構造函數顯式地創建對象。這種方式是由執行類的實例創建表達式創建對象。除此之外,還可以使用反射機制 (Class 類的 newInstance 方法、Constructor 類的newInstance 方法)、使用 Clone 方法、使用反序列化等方式創建對象。
使用 new 關鍵字創建對象
這是最常見、最簡單的創建對象的方式,通過這種方式可以調用任意的構造函數(無參的和有參的)創建對象。
使用 Class 類的 newInstance 方法 (反射機制) 。事實上 Class 類的 newInstance 方法內部調用的是 Constructor 類的 newInstance 方法,相當于是調用無參的構造器創建對象。
使用 Constructor 類的 newInstance 方法 (反射機制) 。該方法和 Class 類中的 newInstance 方法類似,不同的是 Constructor 類的 newInstance 方法可以調用有參數的和私有的構造函數。
使用Clone方法創建對象
調用一個對象的 clone 方法,JVM 都會創建一個新的、一樣的對象。特別需要說明的是,用 clone 方法創建對象的過程中并不會調用任何構造函數。如何使用 clone 方法以及淺克隆/深克隆機制。簡單而言,要想使用 clone 方法,就必須先實現 Cloneable 接口并實現其定義的 clone 方法,這也是原型模式的應用。
使用 (反) 序列化機制創建對象
當反序列化一個對象時,JVM會創建一個單獨的對象,在此過程中,JVM并不會調用任何構造函數。為了反序列化一個對象,對應的類需要實現 Serializable 接口。
從 Java 虛擬機層面看,除了使用 new 關鍵字創建對象的方式外,其他方式全部都是通過轉變為 invokevirtual 指令直接創建對象的。
Java 對象的創建過程
當一個對象被創建時,虛擬機就會為其分配內存來存放對象自己的實例變量及其繼承父類的實例變量 (即使繼承超類的實例變量有可能被隱藏也會被分配空間) 。在為這些實例變量分配內存的同時,這些實例變量也會被賦予默認值。在內存分配完成之后,Java 虛擬機就會開始對新創建的對象進行初始化。在 Java 對象初始化過程中,主要涉及三種執行對象初始化的結構,分別是實例變量初始化、實例代碼塊初始化以及構造函數初始化。
實例變量初始化與實例代碼塊初始化
在定義(聲明)實例變量的同時,可以直接對實例變量進行賦值或者使用實例代碼塊對其進行賦值。如果以這兩種方式為實例變量進行初始化,那么它們將在構造函數執行之前完成這些初始化操作。實際上,如果對實例變量直接賦值或者使用實例代碼塊賦值,那么編譯器會將其中的代碼放到類的構造函數中去,并且這些代碼會被放在對超類構造函數的調用語句之后 (構造函數的第一條語句必須是超類構造函數的調用語句) ,構造函數本身的代碼之前。
特別需要注意的是,Java 是按照先后順序來執行實例變量初始化和實例初始化器中的代碼,并且不允許順序靠前的實例代碼塊初始化在其后面定義的實例變量。這么做是為了保證一個變量在被使用之前已經被正確地初始化。
構造函數初始化
實例變量初始化與實例代碼塊初始化總是發生在構造函數初始化之前。Java 中的每一個類中都至少會有一個構造函數,如果沒有顯式定義構造函數,那么 JVM 會為它提供一個默認無參的構造函數。在編譯生成的字節碼中,這些構造函數會被命名成 () 方法 (參數列表與 Java 語言中構造函數的參數列表相同) 。Java 要求在實例化類之前,必須先實例化其超類,以保證所創建實例的完整性。
事實上,這一點是在構造函數中保證的:Java 強制要求除 Object 類 (Object 是 Java 的頂層類,沒有超類) 之外所有類的構造函數中的第一條語句必須是超類構造函數的調用語句或者是類中定義的其他的構造函數。如果既沒有調用其他的構造函數,也沒有顯式調用超類的構造函數,那么編譯器會自動生成一個對超類構造函數的調用。
如果顯式調用超類的構造函數,那么該調用必須放在構造函數所有代碼的最前面。正因為如此,Java 才可以使得一個對象在初始化之前其所有的超類都被初始化完成,并保證創建一個完整的對象出來。特別地,如果在一個構造函數中調用另外一個構造函數則不能顯式調用超類的構造函數,而且要另一個構造函數放在構造函數所有代碼的最前面。
Java 通過對構造函數作出上述限制保證一個類的實例能夠在被使用之前正確地初始化。
1.Java普通對象的創建
這里討論的僅僅是普通Java對象,不包含數組和Class對象。
1.1new指令
虛擬機遇到一條new指令時,首先去檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,并且檢查這個符號引用代表的類是否已被加載、解析和初始化過。如果沒有,那么須先執行相應的類加載過程。
1.2分配內存
接下來虛擬機將為新生代對象分配內存。對象所需的內存的大小在類加載完成后便可完全確定。分配方式有“指針碰撞(Bump the Pointer)”和“空閑列表(Free List)”兩種方式,具體由所采用的垃圾收集器是否帶有壓縮整理功能決定。
1.3初始化
內存分配完成后,虛擬機需要將分配到的內存空間都初始化為零值(不包括對象頭),這一步操作保證了對象的實例字段在Java代碼中可以不賦初始值就直接使用,程序能訪問到這些字段的數據類型所對應的零值。
1.4對象的初始設置
接下來虛擬機要對對象進行必要的設置,例如這個對象是哪個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的GC分代年齡等信息。這些信息存放在對象的對象頭(Object Header)之中。根據虛擬機當前的運行狀態的不同,如對否啟用偏向鎖等,對象頭會有不同的設置方式。
1.5<init>方法
在上面的工作都完成了之后,從虛擬機的角度看,一個新的對象已經產生了,但是從Java程序的角度看,對象創建才剛剛開始—<init>方法還沒有執行,所有的字段都還為零。所以,一般來說,執行new指令后悔接著執行init方法,把對象按照程序員的意愿進行初始化(應該是將構造函數中的參數賦值給對象的字段),這樣一個真正可用的對象才算完全產生出來。
2.Java對象內存布局
在HotSpot虛擬機中,對象在內存中存儲的布局可以分為3塊區域:對象頭(Header)、實例數據(Instance Data)、對其填充(Padding)。
2.1對象頭
HotSpot虛擬機的對象頭包含兩部分信息,第一部分用于存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標志、線程持有的鎖、偏向線程ID、偏向時間戳等。
對象的另一部分類型指針,即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例(并不是所有的虛擬機實現都必須在對象數據上保留類型指針,也就是說,查找對象的元數據信息并不一定要經過對象本身)。
如果對象是一個Java數組,那在對象頭中還必須有一塊用于記錄數組長度的數據。
元數據:描述數據的數據。對數據及信息資源的描述信息。在Java中,元數據大多表示為注解。
2.2實例數據
實例數據部分是對象真正存儲的有效信息,也是在程序代碼中定義的各種類型的字段內容,無論從父類繼承下來的,還是在子類中定義的,都需要記錄起來。這部分的存儲順序會虛擬機默認的分配策略參數和字段在Java源碼中定義的順序影響(相同寬度的字段總是被分配到一起)。
2.3對齊填充
對齊填充部分并不是必然存在的,也沒有特別的含義,它僅僅起著占位符的作用。由于HotSpot VM的自動內存管理系統要求對象的起始地址必須是8字節的整數倍,也就是說,對象的大小必須是8字節的整數倍。而對象頭部分正好是8字節的倍數(1倍或者2倍),因此,當對象實例數據部分沒有對齊時,就需要通過對齊填充來補全。
大家都知道,java使用new 關鍵字進行對象的創建,但這只是從語言層次上理解了對象的創建,下邊我們從jvm的角度來看看,對象是怎么被創建出來的,即對象的創建過程。
對象的創建大概分為以下幾步:
1:檢查類是否已經被加載;
2:為對象分配內存空間;
3:為對象字段設置零值;
4:設置對象頭;
5:執行構造方法。
第一步,當程序遇到new 關鍵字時,首先會去運行時常量池中查找該引用所指向的類有沒有被虛擬機加載,如果沒有被加載,那么會進行類的加載過程,如果已經被加載,那么進行下一步,為對象分配內存空間;
第二步,加載完類之后,需要在堆內存中為該對象分配一定的空間,該空間的大小在類加載完成時就已經確定下來了,這里多說一點,為對象分配內存空間有兩種方式:
(1)第一種是jvm將堆區抽象為兩塊區域,一塊是已經被其他對象占用的區域,另一塊是空白區域,中間通過一個指針進行標注,這時只需要將指針向空白區域移動相應大小空間,就完成了內存的分配,當然這種劃分的方式要求虛擬機的對內存是地址連續的,且虛擬機帶有內存壓縮機制,可以在內存分配完成時壓縮內存,形成連續地址空間,這種分配內存方式成為“指針碰撞”,但是很明顯,這種方式也存在一個比較嚴重的問題,那就是多線程創建對象時,會導致指針劃分不一致的問題,例如A線程剛剛將指針移動到新位置,但是B線程之前讀取到的是指針之前的位置,這樣劃分內存時就出現不一致的問題,解決這種問題,虛擬機采用了循環CAS操作來保證內存的正確劃分;
(2)第二種也是為了解決第一種分配方式的不足而創建的方式,多線程分配內存時,虛擬機為每個線程分配了不同的空間,這樣每個線程在分配內存時只是在自己的空間中操作,從而避免了上述問題,不需要同步。當然,當線程自己的空間用完了才需要需申請空間,這時候需要進行同步鎖定。為每個線程分配的空間稱為“本地線程分配緩沖(TLAB)”,是否啟用TLAB需要通過 -XX:+/-UseTLAB參數來設定。
第三步,分配完內存后,需要對對象的字段進行零值初始化,對象頭除外,零值初始化意思就是對對象的字段賦0值,或者null值,這也就解釋了為什么這些字段在不需要進程初始化時候就能直接使用;
第四步,這里,虛擬機需要對這個將要創建出來的對象,進行信息標記,包括是否為新生代/老年代,對象的哈希碼,元數據信息,這些標記存放在對象頭信息中,對象頭非常復雜,這里不作解釋,可以另行百度;
第五步,也就是最后一步,執行對象的構造方法,這里做的操作才是程序員真正想做的操作,例如初始化其他對象啊等等操作,至此,對象創建成功。
java中個,創建一個對象需要經過五步,分別是類加載檢查、分配內存、初始化零值、設置對象頭和執行初始化init()。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。