在談堆和棧之前,首先我們先要瞭解一下Java對記憶體的分配結構。作為Java程式員大家應該都知道Java的程式都是運行在Java虛擬機器上也就是JVM上,程式中所有的變數、執行個體、方法等都是由JVM在記憶體上分配空間的。
那麼讓我們來初步的瞭解一下Java程式在運行時都會存在哪些記憶體地區:
1.寄存器:JVM內部虛擬寄存器跟CPU有關,程式無法控制。
2.棧:用來存放基礎資料型別 (Elementary Data Type)的變數和引用資料類型的執行個體(執行個體:某一個類模型new出實體的引用變數)。
3.堆:用來存放程式動態產生的資料,eg:根據類模型new出的實體,需要注意的是我們通過new關鍵字建立出來的對象在堆記憶體中只有該對象的成 員變數,而不包括成員方法。因為每一個類的對象都有各自的成員變數儲存在各自的堆記憶體中,但是它們共用該類的成員方法,所以並不是每一次new 都會建立成員方法。
4.常量池:JVM對每個已經載入的類型都會維護一個常量池,常量池就是一個該類型用到的常量的一個有序集合。常量池存在於堆記憶體地區中。
5.程式碼片段:用來存放沖存放裝置中讀取的程式碼片段。
6.資料區段:用來存放靜態成員,包括靜態變數、靜態常量、靜態方法、靜態類。
7.方法區:用來存放所有的函數。
上圖簡單的描述了Java程式在JVM中運行時的記憶體分布。接下來我們將瞭解一下堆和棧的特點。 堆: 1.new出來的對象都有指定的地址值。 2.每個變數都有預設值。eg:int 類型的數組、char類型的數組、Class類型的集合等。 3.當使用完畢後,GC並不會立即對該記憶體進行清理,GC會在閒置時候進行掃描該記憶體是否仍存在引用,若不存在則立即進行回收,若存在則等待下一次的掃描。 棧:通常棧記憶體存放的是類的執行個體和基礎資料類型(int byte char long double 等),當變數處於該變數的作用於外的時候GC會立即對該記憶體進行回收。 對於棧記憶體和堆記憶體的缺點也比較明顯,棧記憶體在分配空間的時候是固定的不可改變的,而堆記憶體在分配空間的時候是動態分配空間的。堆記憶體由於GC並不是立即對廢棄的記憶體進行回收並且GC會採用樹的方式進行引用檢索,這樣就導致了廢棄的記憶體並不會立即回收無端的佔用了記憶體空間。 我想大家應該都看到過這樣一個面試題,問"=="和"equals"的區別,那我們通過這個問題來說明堆和棧之間的聯絡。
String str1 = "abc"; //定義字串變數str1
String str2 = "abc"; //定義字串變數str2
String str3 = new String("abc"); //以new的方式定義字串變數str3
String str4 = new String("abc");//以new的方式定義字串變數str4
/**
* 那麼問題來了
* str1 == str2 ? true:false;
* str2 == str3 ? true:false;
* str3 == str4 ? true:false;
*/
System.out.println("str1 == str2 ? :" + (str1 == str2)); //true
System.out.println("str2 == str3 ? :" + (str2 == str3)); //false
System.out.println("str3 == str4 ? :" + (str3 == str4)); //false
System.out.println("str3.equals(str4) ? :" + str3.equals(str4)); //true
對於str1==str2的值為true和str2 == str3的值為false,我想大家都比較容易,但是str3 == str4 和 str3.equals(str4)的值可能和你的結果就不一定一樣了吧。我來說明一下這個程式碼片段: 1.str1 == str2 這個運算式的比較過程,"=="是一個邏輯運算子,對於基礎資料類型比較的是兩個值是否相等,對於引用資料類型則比較的是兩個引用變數所指向在堆記憶體中的地址值是否一致。String類型是引用資料類型,但是String是一個特殊的引用資料類型,我們在定義一個String類型的變數的時候,在賦值的過程中,Java虛擬機器首先會到字串常量池中檢索所要賦的值是否在字串常量池中存在,如果存在則將該值所在的字串常量池中的地址賦予該引用變數,如果不存在則在字串常量池中分配一個記憶體來儲存所賦的值然後將該值所在的地址賦予引用變數。所以在定義str1變數的時候,JVM在字串常量池中分配了記憶體給"abc"這個值空間然後將這個值的地址給予str1這個引用變數,而str2在定義的時候JVM檢索字串常量池中存在了"abc"這個值同樣將該值的地址給予了str2,所以str1 == str2 的值為true。 2.str2 == str3 這個運算式的比較過程,上面我們說了"=="邏輯運算子,比較String類型的變數比較的是地址,那麼有人會問,不是說"abc"這個值已經在字串常量池中存在了麼,可是為什麼結果會是false呢。那麼我們要說明一下對於String類型變數在定義的時候的方式了,對於String name = "" 這種方式也是最常用的定義方式,該方式的定義過程就是在字串常量池中分配記憶體空間並傳回值的地址,而String name = new String();這種定義方式則完全不同,使用new關鍵字定義的對象所儲存的空間是在堆記憶體中而非在常量池中,它的過程是,首先Java虛擬機器會在字串常量池中檢索是否存在該值得記憶體空間,若不存在則先分配空間給該值,若存在則將該值(不是記憶體位址)複製一份到堆記憶體中,然後將該值在堆記憶體中的地址賦予引用變數str3。這樣就會導致str2 == str3的值為false。 3.str3 == str4 的值為false,通過2的解釋我想大家可以明白str3所引用的對象和str4所引用的對象在堆記憶體中的地址是不一致的,所有是false。 4.str3.equals(str4)的值為true,也許有的人會問了,equals()這個方法比較的不也是地址麼。我的回答很簡單,大家可以看一下Object源碼一切就會明白了, public boolean equals(Object obj) {
return (this == obj);
} 很簡單的程式碼片段,要注意用紅色框注的部分,這個方法返回的運算式用的是"==",那麼上面已經說過了,"=="邏輯運算子在引用變數中比較的是記憶體中的地址,這樣回答可能又會問了,可是答案是true啊。那麼我回答你的是,我們比較的是String類型的變數而非是Object類型的變數。Object是Java中所有類(包括自訂類)的超類,所有的類都繼承了Object類的元素String類也不例外,但是不同的時候String類對equals()方法進行了重寫
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = count;
if (n == anotherString.count) {
char v1[] = value;
char v2[] = anotherString.value;
int i = offset;
int j = anotherString.offset;
while (n-- != 0) {
if (v1[i++] != v2[j++])
return false;
}
return true;
}
}
return false;
} 通過上面的程式碼片段,很容易就能得知str3.equals(str4)的值為什麼是true了,最終比較的是兩個引用變數的值。 這是我對於堆和棧的理解,可能很淺顯所以希望大家可以多多討論互相協助。