Java 對象的建立過程

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

  1. 類加載檢查
    在java中,new一個對象的時候,java虛擬機會首先去檢查這個指令的參數是否能在常量池中找到這個對象對應的類的符號引用,檢查這個符號引用表明的類是否被類加載器加載、解析和初始化;若是沒有,則必需要進行類加載。
  2. 分配內存
    在類加載以後,虛擬機會爲將要的ThinkMarkets代理申請www.kaifx.cn/broker/thinkmarkets.html建立的對象分配內存,對象所需內存的大小在類加載完成即可徹底肯定,給對象分配內存是要在java堆中劃分出一塊肯定的內存。在java堆內存分配通常有兩種方式,指針碰撞和空閒列表。
    (1)指針碰撞
    在java堆規整的狀況下,適合採用指針碰撞方式。用過的內存所有整合到以便,沒有用過的內存放在另一邊,中間有一個分界值指針,用來將用過的內存與空閒內存分隔開來,當給新生對象分配內存時,指針便會向空閒內存區域移動。
    (2)空閒列表
    在java堆不規整的狀況下,適合採用空閒列表方式。這種方式中,java虛擬機會維護一個列表,該列表是記錄內存的塊是不是可用的,當爲新生對象分配內存的時候,會找一塊足夠大的內存分配給新生對象,以後更新這個列表。
    java堆是否規整由java虛擬機採用的垃圾收集器是否有壓縮整理的功能決定。
  3. 初始化零值
    在給新生對象分配完內存完以後,虛擬機須要將分配到的內存空間都初始化爲零值,這步操做保證了對象的實例字段在java代碼中能夠不賦初值就能夠直接使用。
  4. 設置對象頭
    初始化零值以後,要對新生對象設置對象頭。對象頭中包含類的元數據信息、對象的哈希碼、對象的GC分代年齡等信息。根據虛擬機當前運行狀態的不一樣,對象頭也會有不一樣的設置方式。
  5. 初始化(執行init方法)在給對象設置完對象頭以後,虛擬機已經將一個對象產生了,此時,方法沒有執行,對象的全部字段都爲零值,零值的對象在程序中沒有使用意義,只有初始化以後,對象才能真正體現出做用。此外,虛擬機建立java對象的時候,要保障線程的安全,虛擬機採用兩種方式來保證線程安全。Java建立對象的過程簡單記錄一下Java建立對象的過程,就是new一個對象的時候發生了哪些事情。Java程序執行的過程在此不做說明,對象的建立過程只是程序執行過程的一部分。有關整個程序執行的過程,等熟悉了虛擬機以後在做說明。對象建立過程簡述Java中對象的建立就是在堆上分配內存空間的過程,此處說的對象建立僅限於new關鍵字建立的普通Java對象,不包括數組對象的建立。大體過程以下:檢測類是否被加載爲對象分配內存爲分配的內存空間初始化零值對對象進行其餘設置執行init方法檢測類是否被加載當虛擬機執行到new時,會先去常量池中查找這個類的符號引用。若是能找到符號引用,說明此類已經被加載到方法區(方法區存儲虛擬機已經加載的類的信息),能夠繼續執行;若是找不到符號引用,就會使用類加載器執行類的加載過程,類加載完成後繼續執行。爲對象分配內存類加載完成之後,虛擬機就開始爲對象分配內存,此時所需內存的大小就已經肯定了。只須要在堆上分配所須要的內存便可。具體的分配內存有兩種狀況:第一種狀況是內存空間絕對規整,第二種狀況是內存空間是不連續的。對於內存絕對規整的狀況相對簡單一些,虛擬機只須要在被佔用的內存和可用空間之間移動指針便可,這種方式被稱爲指針碰撞。對於內存不規整的狀況稍微複雜一點,這時候虛擬機須要維護一個列表,來記錄哪些內存是可用的。分配內存的時候須要找到一個可用的內存空間,而後在列表上記錄下已被分配,這種方式成爲空閒列表。分配內存的時候也須要考慮線程安全問題,有兩種解決方案:第一種是採用同步的辦法,使用CAS來保證操做的原子性。另外一種是每一個線程分配內存都在本身的空間內進行,便是每一個線程都在堆中預先分配一小塊內存,稱爲本地線程分配緩衝(TLAB),分配內存的時候再TLAB上分配,互不干擾。爲分配的內存空間初始化零值對象的內存分配完成後,還須要將對象的內存空間都初始化爲零值,這樣能保證對象即便沒有賦初值,也能夠直接使用。對對象進行其餘設置分配完內存空間,初始化零值以後,虛擬機還須要對對象進行其餘必要的設置,設置的地方都在對象頭中,包括這個對象所屬的類,類的元數據信息,對象的hashcode,GC分代年齡等信息。執行init方法執行完上面的步驟以後,在虛擬機裏這個對象就算建立成功了,可是對於Java程序來講還須要執行init方法纔算真正的建立完成,由於這個時候對象只是被初始化零值了,尚未真正的去根據程序中的代碼分配初始值,調用了init方法以後,這個對象才真正能使用。到此爲止一個對象就產生了,這就是new關鍵字建立對象的過程。過程以下:檢測類是否被加載–>爲對象分配內存空間–>初始零值–>進行必要的設置–>調用init方法進行初始化。對象的建立過程:類加載檢查-->分配內存-->初始化零值-->設置對象頭-->執行init方法一、類加載檢查:虛擬機遇到一條new指令時,先檢查這個指令的參數可否在常量池中定位到一個類的符號引用,並檢查這個符號引用表明的類是否已被ji加載、解析和初始化過。若是沒有,則先進行類的加載過程。二、分配內存:有兩種方式指針碰撞:假設Java堆中的內存是規整的,用過的內存在一邊,空閒的在另外一邊,中間有一個指針做爲分界點的指示器,所分配的內存就把那個指針向空閒那邊挪動一段與對象大小相等的距離。空閒列表:若是Java堆中的內存不是規整的,虛擬機必須維護一個列表,記錄哪些內存塊可用的,分配時從列表中找到一塊足夠大的空間劃分給對象,並更新列表的記錄。在劃分可用空間時,會遇到線程安全的問題。解決這個問題有兩種方案。第一種:對分配內存空間的動做進行同步處理--虛擬機採用CAS配上失敗重試的方式保證更新操做的原子性。另外一種是把內存分配的動做安裝現場劃分在不一樣的空間之中進行,即每一個線程在Java堆中預先分配一小塊內存,稱爲本地線程分配緩衝(TLAB)。那個線程須要分配內存,就在哪一個線程的TLAB上分配,只有TLAB用完並分配新的TLAB時,才須要同步鎖定。是否使用TLAB,-XX:+UseTLAB參數來設定。三、初始化零值 將分配到的內存空間都初始化爲零值,若是用TLAB,則在TLAB分配時初始化爲零值。四、設置對象頭:主要設置類的元數據信息、對象的哈希碼、對象的GC分代年齡等信息。五、執行init方法初始化。類加載過程當JVM第一次要使用一個類的時候,須要加載這個類;首先根據classpath的配到硬盤上找這個類的class文件(若是沒有配置classpath,就到當前位置找);若是找到這個class,就加載到方法區; a) 分別將這個類的靜態成員加載到靜態區域,非靜態成員加載到非靜態區域; b) 在靜態區域爲全部靜態成員變量分配空間,賦默認值; c) 爲全部靜態成員變量顯示賦值 d) 執行全部靜態代碼塊 (c和d具體順序是按照代碼書寫的前後順序) 等到靜態代碼塊都執行完畢,類加載完成;對象的建立過程JVM遇到new關鍵字,首先回去堆內存中開闢空間;爲全部非靜態的成員變量分配空間,賦默認值;調用相應的構造函數進棧執行;構造函數執行時首先要執行隱式三步: a) super():調用父類構造函數 // 若是第一行是this就調用this b) 給對象中全部非靜態成員變量顯示賦值; c) 執行構造代碼塊; (b和c具體執行順序,是按照代碼書寫的前後順序)隱式三步執行結束後,開始執行構造函數中的代碼; 等到構造函數結束出棧,對象纔算建立完成
相關文章
相關標籤/搜索