java數組內存分配內存結構詳解

引自:https://m.2cto.com/kf/201611/561021.html

Java 數組是靜態的

Java 語言是典型的靜態語言,因此 Java 數組是靜態的,即當數組被初始化之後,該數組 所佔的內存空間、數組長度都是不可變的。Java 程序中的數組必須經過初始化纔可使用。所謂初始化,即創建實際的數組對象,也就是在內存中爲數組對象分配內存空間,併爲每個數組 元素指定初始值。

數組的初始化有以下兩種方式。

  • 靜態初始化:初始化時由程序員顯式指定每個數組元素的初始值,由系統決定數組長度。
  • 動態初始化:初始化時程序員只指定數組長度,由系統爲數組元素分配初始值。

不管採用哪種方式初始化Java 數組,一旦初始化完成,該數組的長度就不可改變,Java 語言允許通過數組的length 屬性來訪問數組的長度。示例如下。


 
  1. public class ArrayTest
  2. {
  3. public static void main(String[] args)
  4. {
  5. // 採用靜態初始化方式初始化第一個數組
  6. String[] books = new String[]
  7. { "1", "2", "3", "4"
  8. };
  9. // 採用靜態初始化的簡化形式初始化第二個數組
  10. String[] names =
  11. {
  12. "孫悟空",
  13. "豬八戒",
  14. "白骨精"
  15. };
  16. // 採用動態初始化的語法初始化第三個數組
  17. String[] strArr = new String[5];
  18. // 訪問三個數組的長度
  19. System.out.println("第一個數組的長度:" + books.length);
  20. System.out.println("第二個數組的長度:" + names.length);
  21. System.out.println("第三個數組的長度:" + strArr.length);
  22. }
  23. }

上面程序中的粗體字代碼聲明並初始化了三個數組。這三個數組的長度將會始終不變,程 序輸出三個數組的長度依次爲4 、3 、5 。

前面已經指出,Java 語言的數組變量是引用類型的變量,books、names 、strArr 這三個變量,以及各自引用的數組在內存中的分配示意圖如圖1.1 所示。

\

從圖1.1可以看出,對於靜態初始化方式而言,程序員無須指定數組長度,指定該數組的 數組元素,由系統來決定該數組的長度即可。例如 books 數組,爲它指定了四個數組元素,它 的長度就是4 ;對於names 數組,爲它指定了三個元素,它的長度就是3 。

執行動態初始化時,程序員只需指定數組的長度,即爲每個數組元素指定所需的內存空間, 系統將負責爲這些數組元素分配初始值。指定初始值時,系統將按如下規則分配初始值。

  • 數組元素的類型是基本類型中的整數類型(byte 、short、int 和long ),則數組元素的值是0 。
  • 數組元素的類型是基本類型中的浮點類型(float 、double ),則數組元素的值是0.0。
  • 數組元素的類型是基本類型中的字符類型(char ),則數組元素的值是'\u0000'。
  • 數組元素的類型是基本類型中的布爾類型(boolean),則數組元素的值是false 。
  • 數組元素的類型是引用類型(類、接口和數組),則數組元素的值是null 。

    Java 數組是靜態的,一旦數組初始化完成,數組元素的內存空間分配即結束,程序只能改變數組元素的值,而無法改變數組的長度。

需要指出的是,Java 的數組變量是一種引用類型的變量,數組變量並不是數組本身,它 只是指向堆內存中的數組對象。因此,可以改變一個數組變量所引用的數組,這樣可以造成數 組長度可變的假象。假設,在上面程序的後面增加如下幾行。


 
  1. // 讓books 數組變量、strArr 數組變量指向names 所引用的數組
  2. books = names;
  3. strArr = names;
  4. System.out.println("--------------");
  5. System.out.println("books 數組的長度:" + books.length);
  6. System.out.println("strArr 數組的長度:" + strArr.length);
  7. // 改變books 數組變量所引用的數組的第二個元素值
  8. books[1] = "唐僧";
  9. System.out.println("names 數組的第二個元素是:" + books[1]);

上面程序中粗體字代碼將讓books 數組變量、strArr 數組變量都指向names 數組變量所引 用的數組,這樣做的結果就是books、strArr、names 這三個變量引用同一個數組對象。此時, 三個引用變量和數組對象在內存中的分配示意圖如圖1.2 所示。

\

從圖1.2可以看出,此時 strArr、names 和books 數組變量實際上引用了同一個數組對象。 因此,當訪問 books 數組、strArr 數組的長度時,將看到輸出 3。這很容易造成一個假象:books 數組的長度從4 變成了3。實際上,數組對象本身的長度並沒有發生改變,只是 books 數組變 量發生了改變。books 數組變量原本指向圖 1.2下面的數組,當執行了books = names;語句之後,books 數組將改爲指向圖1.2 中間的數組,而原來books 變量所引用的數組的長度依然是4 。

從圖1.2 還可以看出,原來 books 變量所引用的數組的長度依然是 4 ,但不再有任何引用 變量引用該數組,因此它將會變成垃圾,等着垃圾回收機制來回收。此時,程序使用books、 names 和strArr 這三個變量時,將會訪問同一個數組對象,因此把 books 數組的第二個元素賦 值爲“唐僧”時,names 數組的第二個元素的值也會隨之改變。

與Java 這種靜態語言不同的是,JavaScript 這種動態語言的數組長度是可以動態改變的,示例如下。


 
  1. <script type="text/javascript">
  2. var arr = [];
  3. document.writeln("arr的長度是:" + arr.length + "
    ");
  4. // 爲arr 數組的兩個數組元素賦值
  5. arr[2] = 6;
  6. arr[4] = "孫悟空";
  7. // 再次訪問arr 數組的長度
  8. document.writeln("arr的長度是:" + arr.length + "
    ");
  9. </script>

上面是一個簡單的JavaScript 程序。它先定義了一個名爲 arr的空數組,因爲它不包含任 何數組元素,所以它的長度是0 。接着,爲 arr數組的第三個、第五個元素賦值,該數組的長 度也自動變爲5 。這就是JavaScript 裏動態數組和Java 裏靜態數組的區別。

基本類型數組的初始化

對於基本類型數組而言,數組元素的值直接存儲在對應的數組元素中,因此基本類型 數組的初始化比較簡單:程序直接先爲數組分配內存空間,再將數組元素的值存入對應內 存裏。

下面程序採用靜態初始化方式初始化了一個基本類型的數組對象。


 
  1. public class PrimitiveArrayTest
  2. {
  3. public static void main(String[] args)
  4. {
  5. // 定義一個int[] 類型的數組變量
  6. int[] iArr;
  7. // 靜態初始化數組,數組長度爲4
  8. iArr = new int[]{2 , 5 , -12 , 20};
  9. }
  10. }

上面代碼的執行過程代表了基本類型數組初始化的典型過程。下面將結合示意圖詳細介紹這段代碼的執行過程。

執行第一行代碼int[] iArr;時,僅定義一個數組變量,此時內存中的存儲示意圖如圖1.4所示。

\

執行了int[] iArr; 代碼後,僅在 main 方法棧中定義了一個 iArr 數組變量,它是一個引用類 型的變量,並未指向任何有效的內存,沒有真正指向實際的數組對象。此時還不能使用該數組 對象。

當執行iArr = new int[]{2,5,-12,20}; 靜態初始化後,系統會根據程序員指定的數組元素來決 定數組的長度。此時指定了四個數組元素,系統將創建一個長度爲4 的數組對象,一旦該數組 對象創建成功,該數組的長度將不可改變,程序只能改變數組元素的值。此時內存中的存儲示 意圖如圖1.5 所示。

靜態初始化完成後,iArr 數組變量引用的數組所佔用的內存空間被固定下來,程序員只能 改變各數組元素內的值。既不能移動該數組所佔用的內存空間,也不能擴大該數組對象所佔用 的內存,或縮減該數組對象所佔用的內存。

\

對於程序運行過程中的變量,可以將它們形容爲具體的瓶子——瓶子可以存儲 水,而變量用於存儲值,也就是數據。對於強類型語言如Java ,它有一個要求: 怎樣的瓶子只能裝怎樣的水,也就是說,指定類型的變量只能存儲指定類型的值。

所有局部變量都是放在棧內存裏保存的,不管其是基本類型的變量,還 是引用類型的變量,都是存儲在各自的方法棧內存中的;但引用類型的變量所引用的對象(包 括數組、普通的Java 對象)則總是存儲在堆內存中。

對於Java 語言而言,堆內存中的對象(不管是數組對象,還是普通的 Java 對象)通常不 允許直接訪問,爲了訪問堆內存中的對象,通常只能通過引用變量。這也是很容易混淆的地方。 例如,iArr 本質上只是main 棧區的引用變量,但使用 iArr.length 、iArr[2] 時,系統將會自動變 爲訪問堆內存中的數組對象。

對於很多Java 程序員而言,他們最容易混淆的是:引用類型的變量何時只是棧內存中的 變量本身,何時又變爲引用實際的Java 對象。其實規則很簡單:引用變量本質上只是一個指 針,只要程序通過引用變量訪問屬性,或者通過引用變量來調用方法,該引用變量就會由它所 引用的對象代替。


 
  1. public class PrimitiveArrayTest2
  2. {
  3. public static void main(String[] args)
  4. {
  5. // 定義一個int[] 類型的數組變量
  6. int[] iArr = null;
  7. // 只要不訪問iArr 的屬性和方法,程序完全可以使用該數組變量
  8. System.out.println(iArr); //①
  9. // 動態初始化數組,數組長度爲5
  10. iArr = new int[5];
  11. // 只有當iArr 指向有效的數組對象後,下面纔可訪問iArr 的屬性
  12. System.out.println(iArr.length); //②
  13. }
  14. }

上面程序中兩行粗體字代碼兩次訪問iArr 變量。對於①行代碼而言,雖然此時的iArr 數 組變量並未引用到有效的數組對象,但程序在①行代碼處並不會出現任何問題,因爲此時並未 通過iArr 訪問屬性或調用方法,因此程序只是訪問iArr 引用變量本身,並不會去訪問iArr 所 引用的數組對象。對於②行代碼而言,此時程序通過iArr 訪問了length 屬性,程序將自動變 爲訪問iArr 所引用的數組對象,這就要求iArr 必須引用一個有效的對象。

有過一些編程經驗,應該經常看到一個Runtime 異常: NullPointerException (空指針異常)。當通過引用變量來訪問實例屬性,或者調 用非靜態方法時,如果該引用變量還未引用一個有效的對象,程序就會引發 NullPointerException 運行時異常。

引用類型數組的初始化

引用類型數組的數組元素依然是引用類型的,因此數組元素裏存儲的還是引用,它指向另一塊內存,這塊內存裏存儲了該引用變量所引用的對象(包括數組和Java 對象)。

爲了說明引用類型數組的運行過程,下面程序先定義一個Person 類,然後定義一個 Person[]數組,並動態初始化該Person[]數組,再顯式地爲數組的不同數組元素指定值。該程序代碼如下。


 
  1. class Person
  2. {
  3. // 年齡
  4. public int age;
  5. // 身高
  6. public double height;
  7. // 定義一個info 方法
  8. public void info()
  9. {
  10. System.out.println("我的年齡是:" + age
  11. + ",我的身高是:" + height);
  12. }
  13. }
  14. public class ReferenceArrayTest
  15. {
  16. public static void main(String[] args)
  17. {
  18. // 定義一個students 數組變量,其類型是Person[]
  19. Person[] students;
  20. // 執行動態初始化
  21. students = new Person[2];
  22. System.out.println("students所引用的數組的長度是:"
  23. + students.length); //①
  24. // 創建一個Person 實例,並將這個Person 實例賦給zhang 變量
  25. Person zhang = new Person();
  26. // 爲zhang 所引用的Person 對象的屬性賦值
  27. zhang.age = 15;
  28. zhang.height = 158;
  29. // 創建一個Person 實例,並將這個Person 實例賦給lee 變量
  30. Person lee = new Person();
  31. // 爲lee 所引用的Person 對象的屬性賦值
  32. lee.age = 16;
  33. lee.height = 161;
  34. // 將zhang 變量的值賦給第一個數組元素
  35. students[0] = zhang;
  36. // 將lee 變量的值賦給第二個數組元素
  37. students[1] = lee;
  38. // 下面兩行代碼的結果完全一樣,
  39. // 因爲lee 和students[1]指向的是同一個Person 實例
  40. lee.info();
  41. students[1].info();
  42. }
  43. }

上面代碼的執行過程代表了引用類型數組的初始化的典型過程。下面將結合示意圖詳細介紹這段代碼的執行過程。

執行Person[] students;代碼時,這行代碼僅僅在棧內存中定義了一個引用變量,也就是一個指針,這個指針並未指向任何有效的內存區。此時內存中的存儲示意圖如圖1.6 所示。

\

在圖1.6中的棧內存中定義了一個 students 變量,它僅僅是一個空引用,並未指向任何有 效的內存,直到執行初始化,本程序對 students 數組執行動態初始化。動態初始化由系統爲數 組元素分配默認的初始值null ,即每個數組元素的值都是 null 。執行動態初始化後的存儲示意 圖如圖1.7 所示。

從圖1.7 中可以看出,students 數組的兩個數組元素都是引用,而且這兩個引用並未指 向任何有效的內存,因此,每個數組元素的值都是 null 。此時,程序可以通過students 來 訪問它所引用的數組的屬性,因此在①行代碼處通過 students 訪問了該數組的長度,此時 將輸出2 。

students 數組是引用類型的數組,因此 students[0] 、students[1] 兩個數組元素相當於兩個引 用類型的變量。如果程序只是直接輸出這兩個引用類型的變量,那麼程序完全正常。但程序依 然不能通過students[0] 、students[1] 來調用屬性或方法,因此它們還未指向任何有效的內存區, 所以這兩個連續的Person 變量(students 數組的數組元素)還不能被使用。

\

接着,程序定義了zhang 和lee 兩個引用變量,並讓它們指向堆內存中的兩個Person 對象,此時的zhang、lee 兩個引用變量存儲在 main 方法棧區中,而兩個 Person 對象則存儲在堆內存中。此時的內存存儲示意圖如圖1.8 所示。

\

對於zhang、lee 兩個引用變量來說,它們可以指向任何有效的Person 對象,而students[0] 、 students[1] 也可以指向任何有效的Person 對象。從本質上來看,zhang、lee、students[0] 、students[1] 能夠存儲的內容完全相同。接着,程序執行students[0] = zhang;和students[1] = lee; 兩行代碼, 也就是讓zhang 和students[0] 指向同一個 Person 對象,讓 lee 和students[1] 指向同一個Person 對象。此時的內存存儲示意圖如圖1.9 所示。

\

從圖1.9 中可以看出,此時 zhang 和students[0] 指向同一個內存區,而且它們都是引用類 型的變量,因此通過 zhang 和students[0] 來訪問Person 實例的屬性和方法的效果完全一樣。不 論修改students[0] 所指向的 Person 實例的屬性,還是修改 zhang 變量所指向的 Person 實例的 屬性,所修改的其實是同一個內存區,所以必然互相影響。同理,lee 和students[1] 也是引用 到同一個Person 對象,也有相同的效果。

前面已經提到,對於引用類型的數組而言,它的數組元素其實就是一個引用類型的變量, 因此可以指向任何有效的內存——此處“有效”的意思是指強類型的約束。比如,對 Person[] 類型的數組而言,它的每個數組元素都相當於Person 類型的變量,因此它的數組元素只能指 向Person 對象。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章