類的初始化與實例化
一個 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()。html