引言 :棧、堆、常量池雖同屬 Java 內存分配時操作的區域,但其適用范圍和功用卻大不相同。本文將深入
Java 核心,詳細講解 Java 內存分配方面的知識。
Java 內存分配與管理是 Java 的核心技術之一,之前我們曾介紹過 Java 的內存管理與內存泄露以及 Java 垃圾回收方面的知識,今天我們再次深入 Java 核心,詳細介紹一下 Java
在內存分配方面的知識。一般 Java 在內存分配時會涉及到以下區域:
◆寄存器:我們在程序中無法控制
◆棧:存放基本類型的數據和對象的引用,但對象本身不存放在棧中,而是存放在堆中
◆堆:存放用 new 產生的數據
◆靜態域:存放在對象中用 static 定義的靜態成員
◆常量池:存放常量
◆非 RAM 存儲:硬盤等永久存儲空間
Java 內存分配中的棧
在函數中定義的一些基本類型的變量數據和對象的引用變量都在函數的棧內存中分配。
當在一段代碼塊定義一個變量時,Java 就在棧中 為這個變量分配內存空間,當該變量退出
該作用域后,Java 會自動釋放掉為該變量所分配的內存空間,該內存空間可以立即被另作
他用。
Java 內存分配中的堆
堆內存用來存放由 new 創建的對象和數組。 在堆中分配的內存,由 Java 虛擬機的自動垃圾
回收器來管理。
在堆中產生了一個數組或對象后,還可以 在棧中定義一個特殊的變量,讓棧中這個變量的
取值等于數組或對象在堆內存中的首地址,棧中的這個變量就成了數組或對象的引用變量。
引用變量就相當于是 為數組或對象起的一個名稱,以后就可以在程序中使用棧中的引用變
量來訪問堆中的數組或對象。引用變量就相當于是為數組或者對象起的一個名稱。
引用變量是普通的變量,定義時在棧中分配,引用變量在程序運行到其作用域之外后被釋放。
而數組和對象本身在堆中分配,即使程序 運行到使用 new 產生數組或者對象的語句所在的
代碼塊之外,數組和對象本身占據的內存不會被釋放,數組和對象在沒有引用變量指向它的
時候,才變為垃圾,不能在被使用,但仍 然占據內存空間不放,在隨后的一個不確定的時
間被垃圾回收器收走(釋放掉)。這也是 Java 比較占內存的原因。
實際上,棧中的變量指向堆內存中的變量,這就是 Java 中的指針!
常量池 (constant pool)
常量池指的是在編譯期被確定,并被保存在已編譯的.class 文件中的一些數據。除了包含
代碼中所定義的各種基本類型(如 int、long 等等)和對象型(如 String 及數組)的常量
值(final)還包含一些以文本形式出現的符號引用,比如:
◆類和接口的全限定名;
◆字段的名稱和描述符;
◆方法和名稱和描述符。
虛擬機必須為每個被裝載的類型維護一個常量池。常量池就是該類型所用到常量的一個有序
集和,包括直接常量(string,integer 和 floating point 常量)和對其他類型,字段和方
法的符號引用。
對于 String 常量,它的值是在常量池中的。而 JVM 中的常量池在內存當中是以表的形式存
在的, 對于 String 類型,有一張固定長度的 CONSTANT_String_info 表用來存儲文字字符
串值,注意:該表只存儲文字字符串值,不存儲符號引 用。說到這里,對常量池中的字符
串值的存儲位置應該有一個比較明了的理解了。
在程序執行的時候,常量池 會儲存在 Method Area,而不是堆中。
堆與棧
Java 的堆是一個運行時數據區,類的(對象從中分配空間。這些對象通過 new、newarray、
anewarray 和 multianewarray 等指令建立,它們不需要程序代碼來顯式的釋放。堆是由垃
圾回收來負責的,堆的優勢是可以動態地分配內存 大小,生存期也不必事先告訴編譯器,
因為它是在運行時動態分配內存的,Java 的垃圾收集器會自動收走這些不再使用的數據。
但缺點是,由于要在運行時動態 分配內存,存取速度較慢。
棧的優勢是,存取速度比堆要快,僅次于寄存器,棧數據可以共享。但缺點是,存在棧中的
數據大小與生存期必須是 確定的,缺乏靈活性。棧中主要存放一些基本類型的變量數據(int,
short, long, byte, float, double, boolean, char)和對象句柄(引用)。
棧有一個很重要的特殊性,就是存在棧中的數據可以共享。假設我們同時定義:
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=4;那么編譯器會重新搜索棧中是否有 4 值,如果沒有,則將 4 存放進來,
并令 a 指向 4;如果已經有了,則直接將 a 指向這個地址。因此 a 值的改變不會影響 到 b
的值。
要注意這種數據的共享與兩個對象的引用同時指向一個對象的這種共享是不同的,因為這種
情況 a 的修改并不會影響到 b, 它是由編譯器完成的,它有利于節省空間。而一個對象引用
變量修改了這個對象的內部狀態,會影響到另一個對象引用變量。
String 是一個特殊的包裝類數據。可以用:
String str = new String("abc");
String str = "abc";
兩種的形式來創建,第一種是用 new()來新建對象的,它會在存放于堆中。每調用一次就會
創建一個新的對象。而第二種是先在棧中創建一個對 String 類的對象引用變量 str,然后
通過符號引用去字符串常量池 里找有沒有"abc",如果沒有,則將"abc"存放進字符串常量
池 ,并令 str 指向”abc”,如果已經有”abc” 則直接令 str 指向“abc”。
比較類里面的數值是否相等時,用 equals()方法;當測試兩個包裝類的引用是否指向同一
個對象時,用==,下面用例子說明上面的理論。
String str1 = "abc";
String str2 = "abc";
System.out.println(str1==str2); //true
可以看出 str1 和 str2 是指向同一個對象的。
String str1 =new String ("abc");
String str2 =new String ("abc");
System.out.println(str1==str2); // false
用 new 的方式是生成不同的對象。每一次生成一個。
因此用第二種方式創建多個”abc”字符串,在內存中 其實只存在一個對象而已. 這種寫法
有利與節省內存空間. 同時它可以在一定程度上提高程序的運行速度,因為 JVM 會自動根據
棧中數據的實際情況來決定是否有必要創建新對象。而對于 String str = new
String("abc");的代碼,則一概在堆中創建新對象,而不管其字符串值是否相等,是否有
必要創建新對象,從而加重了程序的負擔。
另 一方面, 要注意: 我們在使用諸如 String str = "abc";的格式定義類時,總是想當然
地認為,創建了 String 類的對象 str。擔心陷阱!對象可能并沒有被創建!而可能只是指
向一個先前已經創建的 對象。只有通過 new()方法才能保證每次都創建一個新的對象。
由于 String 類的 immutable 性質,當 String 變量需要經常變換 其值時,應該考慮使用
StringBuffer 類,以提高程序效率。
首先 String 不屬于 8 種基本數據類型,String 是一個對象。因為對象的默認值是 null,
所以 String 的默認值也是 null;但它又是一種特殊的對象,有其它對象沒有的一些特性。
new String()和 new String(”")都是申明一個新的空字符串,是空串不是 null;
String str=”kvill”;String str=new String (”kvill”)的區別
示例:
1. String s0="kvill";
2. String s1="kvill";
3. String s2="kv" + "ill";
4. System.out.println( s0==s1 );
5. System.out.println( s0==s2 );
結果為:
true
true
首先,我們要知結果為道 Java 會確保一個字符串常量只有一個拷貝。
因為例子中的 s0和s1中的”kvill”都是字符串常量,它們在編譯期就被確定了,所以s0==s1
為 true;而”kv”和”ill”也都是字符串常量,當一個字 符串由多個字符串常量連接而
成時,它自己肯定也是字符串常量,所以 s2 也同樣在編譯期就被解析為一個字符串常量,
所以 s2 也是常量池中” kvill”的一個引用。所以我們得出 s0==s1==s2;用 new String()
創建的字符串不是常量,不能在編譯期就確定,所以 new String() 創建的字符串不放入常
量池中,它們有自己的地址空間。
示例:
1. String s0="kvill";
2. String s1=new String("kvill");
3. String s2="kv" + new String("ill");
4. System.out.println( s0==s1 );
5. System.out.println( s0==s2 );
6. System.out.println( s1==s2 );
結果為:
false
false
false
例中 s0 還是常量池 中"kvill”的應用,s1 因為無法在編譯期確定,所以是運行時創建
的新對象”kvill”的引用,s2 因為有后半部分 new String(”ill”)所以也無法在編譯期
確定,所以也是一個新創建對象”kvill”的應用;明白了這些也就知道為何得出此結果了。
String.intern():
再補充介紹一點:存在于.class 文件中的常量池,在運行期被 JVM 裝載,并且可以擴充。
String的 intern()方法就是擴充常量池的 一個方法;當一個String實例str調用intern()
方法時,Java 查找常量池中 是否有相同 Unicode 的字符串常量,如果有,則返回其的引用,
如果沒有,則在常 量池中增加一個 Unicode 等于 str 的字符串并返回它的引用;看示例就
清楚了
示例:
1. String s0= "kvill";
2. String s1=new String("kvill");
3. String s2=new String("kvill");
4. System.out.println( s0==s1 );
5. System.out.println( "**********" );
6. s1.intern();
7. s2=s2.intern(); //把常量池中"kvill"的引用賦給 s2
8. System.out.println( s0==s1);
9. System.out.println( s0==s1.intern() );
10. System.out.println( s0==s2 );
結果為:
false
false //雖然執行了 s1.intern(),但它的返回值沒有賦給 s1
true //說明 s1.intern()返回的是常量池中"kvill"的引用
true
最后我再破除一個錯誤的理解:有人說, “使用 String.intern() 方法則可以將一個 String
類的保存到一個全局 String 表中 ,如果具有相同值的 Unicode 字符串已經在這個表中,
那么該方法返回表中已有字符串的地址,如果在表中沒有相同值的字符串,則將自己的地址
注冊到表中”如果我把他說的這個全局的 String 表理解為常量池的話,他的最后一句話,”
如果在表中沒有相同值的字符串,則將自己的地址注冊到表中”是錯的:
示例:
1. String s1=new String("kvill");
2. String s2=s1.intern();
3. System.out.println( s1==s1.intern() );
4. System.out.println( s1+" "+s2 );
5. System.out.println( s2==s1.intern() );
結果:
false
kvill kvill
true
在這個類中我們沒有聲名一個”kvill”常量,所以常量池中一開始是沒有”kvill”的,當
我們調用 s1.intern()后就在常量池中新添加了一 個”kvill”常量,原來的不在常量池中
的”kvill”仍然存在,也就不是“將自己的地址注冊到常量池中”了。
s1==s1.intern() 為 false 說明原來的”kvill”仍然存在;s2 現在為常量池中”kvill”
的地址,所以有 s2==s1.intern()為 true。
關于 equals()和==:
這個對于 String 簡單來說就是比較兩字符串的 Unicode 序列是否相當,如果相等返回 true;
而==是 比較兩字符串的地址是否相同,也就是是否是同一個字符串的引用。
關于 String 是不可變的
這一說又要說很多,大家只 要知道 String 的實例一旦生成就不會再改變了,比如說:String
str=”kv”+”ill”+” “+”ans”; 就是有 4 個字符串常量,首先”kv”和”ill”生成
了”kvill”存在內存中,然后”kvill”又和” ” 生成 “kvill “存在內存中,最后又
和生成了”kvill ans”;并把這個字符串的地址賦給了 str,就是因為 String 的”不可變”
產生了很多臨時變量,這也就是為什么建議用 StringBuffer 的原 因了,因為 StringBuffer
是可改變的。
下面是一些 String 相關的常見問題:
String 中的 final 用法和理解
final StringBuffer a = new StringBuffer("111");
final StringBuffer b = new StringBuffer("222");
a=b;//此句編譯不通過
final StringBuffer a = new StringBuffer("111");
a.append("222");// 編譯通過
可見,final 只對引用的"值"(即內存地址)有效,它迫使引用只能指向初始指向的那個對象,
改變它的指向會導致編譯期錯誤。至于它所指向的對象 的變化,final 是不負責的。
String 常量池問題的幾個例子
下面是幾個常見例子的比較分析和理解:
String a = "a1";
String b = "a" + 1;
System.out.println((a == b)); //result = true
String a = "atrue";
String b = "a" + "true";
System.out.println((a == b)); //result = true
String a = "a3.4";
String b = "a" + 3.4;
System.out.println((a == b)); //result = true
分析:JVM 對于字符串常量的"+"號連接,將程序編譯期,JVM 就將常量字符串的"+"連接優
化為連接后的值,拿"a" + 1 來說,經編譯器優化后在 class 中就已經是 a1。在編譯期其字
符串常量的值就確定下來,故上面程序最終的結果都為 true。
String a = "ab";
String bb = "b";
String b = "a" + bb;
System.out.println((a == b)); //result = false
分析:JVM 對于字符串引用,由于在字符串的"+"連接中,有字符串引用存在,而引用的值
在程序編譯期是無法確定的,即"a" + bb 無法被編譯器優化,只有在程序運行期來動態分
配并將連接后的新地址賦給 b。所以上面程序的結果也就為 false。
String a = "ab";
final String bb = "b";
String b = "a" + bb;
System.out.println((a == b)); //result = true
分析:和[3]中唯一不同的是 bb 字符串加了 final 修飾,對于 final 修飾的變量,它在編譯
時被解析為常量值的一個本地拷貝存儲到自己的常量 池中或嵌入到它的字節碼流中。所以
此時的"a" + bb 和"a" + "b"效果是一樣的。故上面程序的結果為 true。
String a = "ab";
final String bb = getBB();
String b = "a" + bb;
System.out.println((a == b)); //result = false
private static String getBB() {
return "b";
}
分析:JVM 對于字符串引用 bb,它的值在編譯期無法確定,只有在程序運行期調用方法后,
將方法的返回值和"a"來動態連接并分配地址為 b,故上面 程序的結果為 false。
通過上面 4 個例子可以得出得知:
String s = "a" + "b" + "c";
就等價于 String s = "abc";
String a = "a";
String b = "b";
String c = "c";
String s = a + b + c;
這個就不一樣了,最終結果等于:
StringBuffer temp = new StringBuffer();
temp.append(a).append(b).append(c);
String s = temp.toString();
由上面的分析結果,可就不難推斷出 String 采用連接運算符(+)效率低下原因分析,形
如這樣的代碼:
public class Test {
public static void main(String args[]) {
String s = null;
for(int i = 0; i < 100; i++) {
s += "a";
}
}
}
每做一次 + 就產生個 StringBuilder 對象,然后 append 后就扔掉。下次循環再到達時重新
產生個 StringBuilder 對象,然后 append 字符串,如此循環直至結束。如果我們直接采用
StringBuilder 對象進行 append 的話,我們可以節省 N - 1 次創建和銷毀對象的時間。
所以對于在循環中要進行字符串連接的應用,一般都是用 StringBuffer 或 StringBulider
對象來進行 append 操作。
String 對象的 intern 方法理解和分析:
public class Test4 {
private static String a = "ab";
public static void main(String[] args){
String s1 = "a";
String s2 = "b";
String s = s1 + s2;
System.out.println(s == a);//false
System.out.println(s.intern() == a);//true
}
}
這里用到 Java 里面是一個常量池的問題。對于 s1+s2 操作,其實是在堆里面重新創建了一
個新的對象,s 保存的是這個新對象在堆空間的的內容,所 以 s 與 a 的值是不相等的。而當
調用 s.intern()方法,卻可以返回 s 在常量池中的地址值,因為 a 的值存儲在常量池中,
故 s.intern 和 a 的值相等。
總結
棧中用來存放一些原始數據類型的局部變量數據和對象的引用(String,數組.對象等等)但
不存放對象內容
堆中存放使用 new 關鍵字創建的對象.
字符串是一個特殊包裝類,其引用是存放在棧里的,而對象內容必須根據創建方式不同定(常
量池和堆).有的是編譯期就已經創建好,存放在字符串常 量池中,而有的是運行時才被創
建.使用 new 關鍵字,存放在堆中。