Java中的成員初始化順序和內存分配過程

原帖是這樣描述的:

http://java.dzone.com/articles/java-object-initialization?utm_source=feedburner&utm_medium=feed&utm_campaign=Feed%3A+javalobby%2Ffrontpage+%28Javalobby+%2F+Java+Zone%29&utm_content=

我們這裏把問題簡化方便分析。

屬性、方法、構造方法和自由塊都是類中的成員,在創建類的對象時,類中各成員的執行順序:

1.父類靜態成員和靜態初始化快,按在代碼中出現的順序依次執行。2.子類靜態成員和靜態初始化塊,按在代碼中出現的順序依次執行。

3. 父類的實例成員和實例初始化塊,按在代碼中出現的順序依次執行。4.執行父類的構造方法。

5.子類實例成員和實例初始化塊,按在代碼中出現的順序依次執行。6.執行子類的構造方法。

最後,生成對象由main線程調用

再強調一遍:父靜態->子靜態->父變量->父初始化區->父構造–>子變量->子初始化區->子構造

知道了1+1=2,讓我們來舉個簡單的栗子:

package com.harryz.newclass;

public class A extends B {
    public int a = 100;
 
    public A() {
        super();
        System.out.println(a);
        a = 200;
    }
 
    public static void main(String[] args) {
        System.out.println(new A().a);
    }
}
 
class B {
    public B() {
        System.out.println(((A) this).a);
    }
}

這裏其實只有兩個trick:

第一,super()必須是A()構造方法中的第一行,也就是說Java必須保證構造方法的調用是符合上面順序的;

第二,在B()構造方法中調用((A) this).a時,A類被分配到內存空間,但是它的成員a還沒有被初始化,所以a的值是默認值0;(詭異的用法。。。)

//B()
0
//A()
100
//main
200


所以,別忘了內存的分配過程:


new的時候
        棧                                                                       堆
rectangle   ------------------------------------------> new Rectangle(3,2);


new ClassName();//創建 ClassName類的一個實例時:

解釋器截取new這個關鍵字時,就會爲ClassName量身定做一個內存空間,這個時候也就是爲該類中的所有成員變量分配內存空間之時,然後按照前面的順序進行初始化,所有引用類型將其製成null 基本數據類型爲0;

之後解釋器會繼續解釋執行到 ClassName();這句話,也就是該類的構造器,調用指定的類的構造方法(根據用戶的需求初始化對象)。

然而這裏面有一種成員變量並不完全在此過程中被初始化,此成員變量爲靜態成員變量,它是在當類靜態屬性或方法第一次被調用或者該類第一次被創建對象時被初始化。

接下來是繼承關係下父子類的創建和初始化過程:
當我們試圖去創建一個子類時,java解釋器發現該類繼承了其他類,所以就會先去創建其父類,切記這個時候並沒有爲子類分配任何的內存空間
而是直接越過自己的創建過程去創建父類,如果檢查到父類也繼承了其他類,java解釋器就會依此類推繼續創建父類的父類。直到最後一個根父類
被分配內存後纔會創建子類。而構造方法的調用則是從子類開始的,但是在子類的構造方法中必須去調用父類的構造方法,經常性的我們沒有看到
在子類的構造方法中顯示的調用父類的構造方法,那是因爲解釋器隱式的調用了父類默認的無參構造方法,然而當我們通過重載機制將父類的默認
構造方法覆蓋時,那麼在子類中就必須顯示的調用父類的構造方法


補充Java內存管理知識:原博客地址

1. 內存分配策略

按照編譯原理的觀點,程序運行時的內存分配有三種策略,分別是靜態的,棧式的,和堆式的。

靜態存儲分配是指在編譯時就能確定每個數據目標在運行時刻的存儲空間需求,因而在編譯時就可以給他們分配固定的內存空間。這種分配策略要求程序代碼中不允許有可變數據結構(比如可變數組)的存在,也不允許有嵌套或者遞歸的結構出現,因爲它們都會導致編譯程序無法計算準確的存儲空間需求。

棧式存儲分配也可稱爲動態存儲分配,是由一個類似於堆棧的運行棧來實現的。和靜態存儲分配相反,在棧式存儲方案中,程序對數據區的需求在編譯時是完全未知的,只有到運行的時候才能夠知道,但是規定在運行中進入一個程序模塊時,必須知道該程序模塊所需的數據區大小才能夠爲其分配內存。和我們在數據結構所熟知的棧一樣,棧式存儲分配按照先進後出的原則進行分配。

靜態存儲分配要求在編譯時能知道所有變量的存儲要求,棧式存儲分配要求在過程的入口處必須知道所有的存儲要求,而堆式存儲分配則專門負責在編譯時或運行時模塊入口處都無法確定存儲要求的數據結構的內存分配,比如可變長度串和對象實例。堆由大片的可利用塊或空閒塊組成,堆中的內存可以按照任意順序分配和釋放。

2. JVM中的堆和棧

JVM是基於堆棧的虛擬機。JVM爲每個新創建的線程都分配一個堆棧,也就是說,對於一個Java程序來說,它的運行就是通過對堆棧的操作來完成的。堆棧以幀爲單位保存線程的狀態。JVM對堆棧只進行兩種操作:以幀爲單位的壓棧和出棧操作。

java把內存分兩種:一種是棧內存,另一種是堆內存

棧(stack)與堆(heap)都是Java用來在Ram中存放數據的地方。與C++不同,Java自動管理棧和堆,程序員不能直接地設置棧或堆。

棧(stack):是一個先進後出的數據結構,通常用於保存方法(函數)中的參數,局部變量。

堆(heap):是一個可動態申請的內存空間(其記錄空閒內存空間的鏈表由操作系統維護),是一個運行時數據區,C中的malloc語句所產生的內存空間就在堆中。

3. 堆和棧優缺點比較

棧的優勢是,存取速度比堆要快,僅次於直接位於CPU中的寄存器。但缺點是,存在棧中的數據大小與生存期必須是確定的,缺乏靈活性。另外,棧數據可以共享,詳見第3點。

堆的優勢是可以動態地分配內存大小,生存期也不必事先告訴編譯器,Java的垃圾收集器會自動收走這些不再使用的數據。但缺點是,由於要在運行時動態分配內存,存取速度較慢。

4. Java中的數據類型有兩種

一種是基本類型

共有8種,即int, short, long, byte, float, double, boolean, char(注意,並沒有string的基本類型)。

這種類型的定義是通過諸如int a = 3; long b = 255L;的形式來定義的,稱爲自動變量。值得注意的是,自動變量存的是字面值,不是類的實例,即不是類的引用,這裏並沒有類的存在。如int a = 3; 這裏的a是一個指向int類型的引用,指向3這個字面值。這些字面值的數據,由於大小可知,生存期可知(這些字面值固定定義在某個程序塊裏面,程序塊退出後,字段值就消失了),出於追求速度的原因,就存在於棧中。

另外,棧有一個很重要的特殊性,就是存在棧中的數據可以共享。假設我們同時定義:

       int a = 3; 
       int b = 3; 
  編譯器先處理int a = 3;首先它會在棧中創建一個變量爲a的引用,然後查找有沒有字面值爲3的地址,沒找到,就開闢一個存放3這個字面值的地址,然後將a指向3的地址。接着處理int b = 3;在創建完b的引用變量後,由於在棧中已經有3這個字面值,便將b直接指向3的地址。這樣,就出現了a與b同時均指向3的情況。

特別注意的是,這種字面值的引用與類對象的引用不同。假定兩個類對象的引用同時指向一個對象,如果一個對象引用變量修改了這個對象的內部狀態,那麼另一個對象引用變量也即刻反映出這個變化。相反,通過字面值的引用來修改其值,不會導致另一個指向此字面值的引用的值也跟着改變的情況。如上例,我們定義完a與b的值後,再令a=4;那麼,b不會等於4,還是等於3。在編譯器內部,遇到a=4;時,它就會重新搜索棧中是否有4的字面值,如果沒有,重新開闢地址存放4的值;如果已經有了,則直接將a指向這個地址。因此a值的改變不會影響到b的值。

另一種是包裝類數據

如Integer, String, Double等將相應的基本數據類型包裝起來的類。這些類數據全部存在於堆中,Java用new()語句來顯示地告訴編譯器,在運行時才根據需要動態創建,因此比較靈活,但缺點是要佔用更多的時間。

String是一個特殊的包裝類數據。即可以用String str = new String("abc");的形式來創建,也可以用String str = "abc";的形式來創建(作爲對比,在JDK 5.0之前,你從未見過Integer i = 3;的表達式,因爲類與字面值是不能通用的,除了String。而在JDK 5.0中,這種表達式是可以的!因爲編譯器在後臺進行Integer i = new Integer(3)的轉換)。前者是規範的類的創建過程,即在Java中,一切都是對象,而對象是類的實例,全部通過new()的形式來創建。Java中的有些類,如DateFormat類,可以通過該類的getInstance()方法來返回一個新創建的類,似乎違反了此原則。其實不然。該類運用了單例模式來返回類的實例,只不過這個實例是在該類內部通過new()來創建的,而getInstance()向外部隱藏了此細節。那爲什麼在String str = "abc";中,並沒有通過new()來創建實例,是不是違反了上述原則?其實沒有。

 5.String在內存中的存放

String是一個特殊的包裝類數據,可以用用以下兩種方式創建:

String str = new String("abc");第一種創建方式是用new()來新建對象的,它會存放於堆中。每調用一次就會創建一個新的對象。

String str = "abc";  第二種創建方式先在棧中創建一個對String類的對象引用變量str,然後在棧中查找有沒有存放值爲"abc"的地址,如果沒有,則開闢一個存放字面值爲"abc"的地址,接着創建一個新的String類的對象o,並將o的字符串值指向這個地址,而且在棧中這個地址旁邊記下這個引用的對象o。如果已經有了值爲"abc"的地址,則查找對象o,並返回o的地址,最後將str指向對象o的地址。

值得注意的是,一般String類中字符串值都是直接存值的。但像String str = "abc";這種場合下,其字符串值卻是保存了一個指向存在棧中數據的引用!

6.數組在內存中的存放

int x[] 或者int []x 時,在內存棧空間中創建一個數組引用,通過該數組名來引用數組。

x = new int[5] 將在堆內存中分配5個保存int型數據的空間,堆內存的首地址放到棧內存中,每個數組元素被初始化爲0。

7.static變量在內存中的存放

用 static的修飾的變量和方法,實際上是指定了這些變量和方法在內存中的“固定位置”-static storage。既然要有“固定位置”那麼他們的 “大小”似乎就是固定的了,有了固定位置和固定大小的特徵了,在棧中或堆中開闢空間那就是非常的方便了。如果靜態的變量或方法在不出其作用域的情況下,其引用句柄是不會發生改變的。

8. java中變量在內存中的分配

1、類變量(static修飾的變量)

在程序加載時系統就爲它在堆中開闢了內存,堆中的內存地址存放於棧以便於高速訪問。靜態變量的生命週期一直持續到整個"系統"關閉

2、實例變量

當你使用java關鍵字new的時候,系統在堆中開闢並不一定是連續的空間分配給變量(比如說類實例),然後根據零散的堆內存地址,通過哈希算法換算爲一長串數字以表徵這個變量在堆中的"物理位置"。 實例變量的生命週期--當實例變量的引用丟失後,將被GC(垃圾回收器)列入可回收“名單”中,但並不是馬上就釋放堆中內存

3、局部變量

局部變量,由聲明在某方法,或某代碼段裏(比如for循環),執行到它的時候在棧中開闢內存,當局部變量一但脫離作用域,內存立即釋放


發佈了27 篇原創文章 · 獲贊 11 · 訪問量 16萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章