Java中一維陣列和二維陣列儲存佔用記憶體大小問題
問題:在java中,一維陣列和二維陣列在資料量一樣的情況下,開闢的記憶體大小是怎樣的?
一、嘗試階段:
1、程式碼一:
public class OneArrayMemory{ public static void main(String[] args){ int num1 = 1024*1024*2; int[] arr1 = new int[num1]; for(int i = 0;i < arr1.length;i++){ arr1[i] = i; } //獲得佔用記憶體總數,並將單位轉換為MB long memory1 = Runtime.getRuntime().totalMemory()/1024/1024; System.out.println("用一維陣列儲存佔用記憶體總量為:"+memory1+"MB"); int nums2 = 1024*1024; int[][] arr2 = new int[nums2][2]; for(int i = 0;i < arr2.length;i++){ arr2[i][0] = i; arr2[i][1] = i; } //獲得佔用記憶體總數,並將單位轉換為MB long memory2 = Runtime.getRuntime().totalMemory()/1024/1024; System.out.println("用二維陣列儲存佔用記憶體總量為:"+memory2+"MB"); } }
2、執行結果:
用一維陣列儲存佔用記憶體總量為:123MB
用二維陣列儲存佔用記憶體總量為:123MB
3、結果有悖於常識,百思不得解。後來查閱了資料,發現了了問題所在。下面補充幾個知識點:
最近在網上看到一些人討論到java.lang.Runtime類中的freeMemory(),totalMemory(),maxMemory ()這幾個方法的一些問題,很多人感到很疑惑,為什麼,在java程式剛剛啟動起來的時候freeMemory()這個方法返回的只有一兩兆位元組,而隨著 java程式往前執行,建立了不少的物件,freeMemory()這個方法的返回有時候不但沒有減少,反而會增加。這些人對freeMemory()這 個方法的意義應該有一些誤解,他們認為這個方法返回的是作業系統的剩餘可用記憶體,其實根本就不是這樣的。這三個方法反映的都是java這個程序的記憶體情 況,跟作業系統的記憶體根本沒有關係。下面結合totalMemory(),maxMemory()一起來解釋。
1)maxMemory()這個方法返回的是java虛擬機器(這個程序)能構從作業系統那裡挖到的最大的記憶體,以位元組為單位,如果在執行java程式的時 候,沒有新增-Xmx引數,那麼就是64兆,也就是說maxMemory()返回的大約是64*1024*1024位元組,這是java虛擬機器預設情況下能 從作業系統那裡挖到的最大的記憶體。如果添加了-Xmx引數,將以這個引數後面的值為準,例如java -cp ClassPath -Xmx512m ClassName,那麼最大記憶體就是512*1024*0124位元組。
2)totalMemory()這個方法返回的是java虛擬機器現在已經從作業系統那裡挖過來的記憶體大小,也就是java虛擬機器這個程序當時所佔用的所有 記憶體。如果在執行java的時候沒有新增-Xms引數,那麼,在java程式執行的過程的,記憶體總是慢慢的從作業系統那裡挖的,基本上是用多少挖多少,直 挖到maxMemory()為止,所以totalMemory()是慢慢增大的。如果用了-Xms引數,程式在啟動的時候就會無條件的從作業系統中挖- Xms後面定義的記憶體數,然後在這些記憶體用的差不多的時候,再去挖。
3)freeMemory()是什麼呢,剛才講到如果在執行java的時候沒有新增-Xms引數,那麼,在java程式執行的過程的,記憶體總是慢慢的從操 作系統那裡挖的,基本上是用多少挖多少,但是java虛擬機器100%的情況下是會稍微多挖一點的,這些挖過來而又沒有用上的記憶體,實際上就是 freeMemory(),所以freeMemory()的值一般情況下都是很小的,但是如果你在執行java程式的時候使用了-Xms,這個時候因為程 序在啟動的時候就會無條件的從作業系統中挖-Xms後面定義的記憶體數,這個時候,挖過來的記憶體可能大部分沒用上,所以這個時候freeMemory()可 能會有些大。
結果異常的根源:totalMemory() 減去freeMemory()才是真正給陣列開闢的記憶體大小!!!
4、修改程式碼:
public class OneArrayMemory{
public static void main(String[] args){
int num1 = 1024*1024*2;
int[] arr1 = new int[num1];
for(int i = 0;i < arr1.length;i++){
arr1[i] = i;
}
//獲得佔用記憶體總數,並將單位轉換為MB
long memory1 = Runtime.getRuntime().totalMemory()/1024/1024 - Runtime.getRuntime().freeMemory() / 1024 / 1024;
//long memory1 = Runtime.getRuntime().totalMemory()/1024/1024;
System.out.println("用一維陣列儲存佔用記憶體總量為:"+memory1+"MB");
int nums2 = 1024*1024;
int[][] arr2 = new int[nums2][2];
for(int i = 0;i < arr2.length;i++){
arr2[i][0] = i;
arr2[i][1] = i;
}
//獲得佔用記憶體總數,並將單位轉換為MB
long memory2 = Runtime.getRuntime().totalMemory()/1024/1024 - Runtime.getRuntime().freeMemory() / 1024 / 1024;
//long memory2 = Runtime.getRuntime().totalMemory()/1024/1024;
System.out.println("用二維陣列儲存佔用記憶體總量為:"+memory2+"MB");
}
}
第二次執行結果:
用一維陣列儲存佔用記憶體總量為:10MB
用二維陣列儲存佔用記憶體總量為:37MB
5、程式碼三:
import java.util.Arrays;
public class OneArrayMemory {
public static void main(String[] args) {
long startTime1 = System.currentTimeMillis(); // 獲取開始時間
int num1 = 1024 * 1024 * 2;
int[] arr1 = new int[num1];
Arrays.fill(arr1, 1);
// 獲得佔用記憶體總數,並將單位轉換成MB
long memory1 = Runtime.getRuntime().totalMemory() / 1024 / 1024 - Runtime.getRuntime().freeMemory() / 1024 / 1024;
System.out.println("用一維陣列儲存佔用記憶體總量為:" + memory1 + "MB");
long endTime1 = System.currentTimeMillis(); // 獲取結束時間
System.out.println("程式執行時間: " + (endTime1 - startTime1) + "ms");
long startTime2 = System.currentTimeMillis(); // 獲取開始時間
int num2 = 1024 * 1024;
int[][] arr2 = new int[num2][2];
for (int[] i : arr2) {
Arrays.fill(i, 1);
}
// 獲得佔用記憶體總數,並將單位轉換成MB
long memory2 = Runtime.getRuntime().totalMemory() / 1024 / 1024 - Runtime.getRuntime().freeMemory() / 1024 / 1024;
System.out.println("用二維陣列儲存佔用記憶體總量為:" + memory2 + "MB");
long endTime2 = System.currentTimeMillis(); // 獲取結束時間
System.out.println("程式執行時間: " + (endTime2 - startTime2) + "ms");
}
}
執行結果:
用一維陣列儲存佔用記憶體總量為:10MB
程式執行時間: 12ms
用二維陣列儲存佔用記憶體總量為:38MB
程式執行時間: 115ms
二、結論:資料量相同的情況下,二維陣列比一維陣列需要開闢更大的記憶體空間。
三、分析
1、一個完整的Java程式執行過程會涉及以下記憶體區域:
暫存器:JVM內部虛擬暫存器,存取速度非常快,程式不可控制。
棧:儲存區域性變數的值,包括:1.用來儲存基本資料型別的值;2.儲存類的例項,即堆區物件的引用(指標)。也可以用來儲存載入方法時的幀。
堆:用來存放動態產生的資料,比如new出來的物件。注意創建出來的物件只包含屬於各自的成員變數,並不包括成員方法。因為同一個類的物件擁有各自的成員變數,儲存在各自的堆中,但是他們共享該類的方法,並不是每建立一個物件就把成員方法複製一次。
常量池:JVM為每個已載入的型別維護一個常量池,常量池就是這個型別用到的常量的一個有序集合。包括直接常量(基本型別,String)和對其他型別、方法、欄位的符號引用(1)。池中的資料和陣列一樣通過索引訪問。由於常量池包含了一個型別所有的對其他型別、方法、欄位的符號引用,所以常量池在Java的動態連結中起了核心作用。常量池存在於堆中。
程式碼段:用來存放從硬碟上讀取的源程式程式碼。
資料段:用來存放static定義的靜態成員。
Java中有兩種型別的陣列:
- 基本資料型別陣列;
- 物件陣列;
2、當一個物件使用關鍵字“new”建立時,會在堆上分配記憶體空間,然後返回物件的引用,這對陣列來說也是一樣的,因為陣列也是一個物件;
1)一維陣列
int[] arr = new int[3];
在以上程式碼中,arr變數存放了陣列物件的引用;如果你建立了空間大小為10的整形陣列,情況是一樣的,一個數組物件所佔的空間在堆上被分配,然後返回其引用;
2)二維陣列
那麼二維陣列是如何儲存的呢?事實上,在Java中只有一維陣列,二維陣列是一個存放了陣列的陣列,如下程式碼及示意圖:
int[ ][ ] arr = new int[3][ ]; arr[0] = new int[3]; arr[1] = new int[5]; arr[2] = new int[4];
對於多維陣列來說,道理是一樣的;
由此可見,資料量相同的情況下,開闢多維陣列會產生更大的開銷。
3)趣事:對於java二維陣列建議 Int[][] arr=new Int[2][100] 而不要使用int[][] arr=new int[100][2],因為後者會產生更多的開銷。