1,對於棧和常量池中的對象能夠共享,對於堆中的對象不能夠共享。棧中的數據大小和生命週期是能夠肯定的,當沒有引用指向數據時,這個數據就會消失。堆中的對象的由垃圾回收器負責回收,所以大小和生命週期不須要肯定,具備很大的靈活性。
對於字符串:其對象的引用都是存儲在棧中的,若是是編譯期已經建立好(直接用雙引號定義的)的就存儲在常量池中,若是是運行期(new出來的)才能肯定的就存儲在堆中。對於equals相等的字符串,在常量池中永遠只有一份,在堆中有多份。
如如下代碼:
Java代碼
String s1 = "china";
String s2 = "china";
String s3 = "china";
String ss1 = new String("china");
String ss2 = new String("china");
String ss3 = new String("china");
對於經過 new 產生一個字符串(假設爲 」china」 )時,會先去常量池中查找是否已經有了 」china」 對象,若是沒有則在常量池中建立一個此字符串對象,而後堆中再建立一個常量池中此 」china」 對象的拷貝對象。這也就是有道面試題: String s = new String(「xyz」); 產生幾個對象?一個或兩個,若是常量池中原來沒有 」xyz」, 就是兩個。
對於基礎類型的變量和常量:變量和引用存儲在棧中,常量存儲在常量池中。
對於成員變量和局部變量:成員變量就是方法外部,類的內部定義的變量;局部變量就是方法或語句塊內部定義的變量。局部變量必須初始化。成員變量虛擬機會自動賦予默認值。
形式參數是局部變量,局部變量的數據存在於棧內存中。棧內存中的局部變量隨着方法的消失而消失。成員變量存儲在堆中的對象裏面,由垃圾回收器負責回收。面試
2. 棧的優點是,存取速度比堆要快,僅次於直接位於CPU中的寄存器。但缺點是,存在棧中的數據大小與生存期必須是肯定的,缺少靈活性。另外,棧數據能夠共享。堆的優點是能夠動態地分配內存大小,生存期也沒必要事先告訴編譯器,Java的垃圾收集器會自動收走這些再也不使用的數據。但缺點是,因爲要在運行時動態分配內存,存取速度較慢。數組
3. Java中的數據類型有兩種。
一種是基本類型(primitive types), 共有8種,即byte, short , int, long, 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()語句來顯示地告訴編譯器,在運行時才根據須要動態建立,所以比較靈活,但缺點是要佔用更多的時間。
舉例以下:
Java代碼
int a1=1;
int b1=1;
int c1=2;
int d1=a1+b1;
Integer a = 1;
Integer b = 2;
Integer c = 3;
Integer d = 3;
Integer e = 321;
Integer f = 321;
Long g = 3L;
System.out.println(a1==b1); //true 結果1
System.out.println(c1==d1); //true 結果2
System.out.println(c==d); //true 結果3
System.out.println(e==f); //false 結果4
分析:結果1:a1==b1如上面所述,會在棧中開闢存儲空間存放數據。
結果2:首先它會在棧中建立一個變量爲c1的引用,而後查找有沒有字面值爲2的地址,沒找到,就開闢一個存放2這個字面值的地址,而後將c1指向2的地址,d1爲兩個字面值相加也爲2,因爲在棧中已經有2這個字面值,便將d1直接指向2的地址。這樣,就出現了c1與d1同時均指向3的狀況。
在分析下面結果之前讓咱們先對Java自動拆箱和裝箱作個瞭解:在自動裝箱時,把int變成Integer的時候,是有規則的,當你的int的值在-128-IntegerCache.high(127) 時,返回的不是一個新new出來的Integer對象,而是一個已經緩存在堆中的Integer對象,(咱們能夠這樣理解,系統已經把-128到127之間的Integer緩存到一個Integer數組中去了,若是你要把一個int變成一個Integer對象,首先去緩存中找,找到的話直接返回引用給你就好了,沒必要再新new一個),若是不在-128-IntegerCache.high(127) 時會返回一個新new出來的Integer對象。
結果3:因爲3是在範圍內因此是從緩存中取數據的,c和d指向同一個對象,結果爲true;
結果4:因爲321不是在範圍內因此不是從緩存中取數據的而是單獨有new對象,e和f並無指向同一個對象,結果爲false;緩存
4. String是一個特殊的包裝類數據。便可以用String str = new String("abc");的形式來建立,也能夠用String str = "abc";的形式來建立(做爲對比,在JDK 5.0以前,你從未見過Integer i = 3;的表達式,由於類與字面值是不能通用的,除了String。而在JDK 5.0中,這種表達式是能夠的!由於編譯器在後臺進行Integer i = new Integer(3)的轉換)。
4(1)String str = "abc"建立對象的過程
1 首先在常量池中查找是否存在內容爲"abc"字符串對象
2 若是不存在則在常量池中建立"abc",並讓str引用該對象
3 若是存在則直接讓str引用該對象app
至於"abc"是怎麼保存,保存在哪?常量池屬於類信息的一部分,而類信息反映到JVM內存模型中是對應存在於JVM內存模型的方法區,也就是說這個類信息中的常量池概念是存在於在方法區中,而方法區是在JVM內存模型中的堆中由JVM來分配的,因此"abc"能夠說存在於堆中(而有些資料,爲了把方法區的堆區別於JVM的堆)。通常這種狀況下,"abc"在編譯時就被寫入字節碼中,因此class被加載時,JVM就爲"abc"在常量池中分配內存,因此和靜態區差很少。
4(2)String str = new String("abc")建立實例的過程
1 首先在堆中(不是常量池)建立一個指定的對象"abc",並讓str引用指向該對象
2 在字符串常量池中查看,是否存在內容爲"abc"字符串對象
3 若存在,則將new出來的字符串對象與字符串常量池中的對象聯繫起來
4 若不存在,則在字符串常量池中建立一個內容爲"abc"的字符串對象,並將堆中的對象與之聯繫起來
intern()方法能夠返回該字符串在常量池中的對象的引用,能夠經過下面代碼簡單的測試
Java代碼
String str1 = "abc";
String str2 = new String("abc").intern();
System.out.println(str1==str2); //true
一個初始爲空的字符串池,它由類String私有地維護。
當調用intern()方法時,若是池已經包含一個等於此String對象的字符串(用 equals(Object) 方法肯定),則返回池中的字符串。不然,將此String對象添加到池中,並返回此String對象的引用。
它遵循如下規則:對於任意兩個字符串s和t,當且僅當s.equals(t)爲true 時,s.intern() == t.intern()才爲true。
因此String str1 = "abc",str1引用的是常量池(方法區)的對象,而String str2 = new String("abc"),str2引用的是堆中的對象,因此內存地址不同,可是內容同樣,因此==爲false,而equals是true.
4(3)String str1 = "abc"; String str2 = "ab" + "c"; str1==str2是ture
是由於String str2 = "ab" + "c"會查找常量池中時候存在內容爲"abc"字符串對象,如存在則直接讓str2引用該對象,顯然String str1 = "abc"的時候,上面說了,會在常量池中建立"abc"對象,因此str1引用該對象,str2也引用該對象,因此str1==str2
4(4)String str1 = "abc"; String str2 = "ab"; String str3 = str2 + "c"; str1==str3是false
是由於String str3 = str2 + "c"涉及到變量(不全是常量)的相加,因此會生成新的對象,其內部實現是先new一個StringBuilder,而後append(str2); append("c");而後讓str3引用toString()返回的對象測試
4(5)String str1 = "abc"; String str2 = "abc"; System.out.println(str1==str2); //true
注意,咱們這裏並不用str1.equals(str2);的方式,由於這將比較兩個字符串的值是否相等。==號,根據JDK的說明,只有在兩個引用都指向了同一個對象時才返回真值。而咱們在這裏要看的是,str1與str2是否都指向了同一個對象。
結果說明,JVM建立了兩個引用str1和str2,但只建立了一個對象,並且兩個引用都指向了這個對象。
4(6)String str1 = "abc"; String str2 = "abc";str1 = "bcd";
String str3 = str1;
System.out.println(str3); //bcd
String str4 = "bcd";
System.out.println(str1 == str4); //true
str3 這個對象的引用直接指向str1所指向的對象(注意,str3並無建立新對象)。當str1改完其值後,再建立一個String的引用str4,並指向因str1修改值而建立的新的對象。能夠發現,這回str4也沒有建立新的對象,從而再次實現棧中數據的共享。
4(7)咱們再接着看如下的代碼。
String str1 = new String("abc");
String str2 = "abc";
System.out.println(str1==str2); //false
建立了兩個引用。建立了兩個對象。兩個引用分別指向不一樣的兩個對象。
String str1 = new String("abc");
String str2 = new String("abc");
System.out.println(str1==str2); //false
建立了兩個引用。建立了兩個對象。兩個引用分別指向不一樣的兩個對象。
以上兩段代碼說明,只要是用new()來新建對象的,都會在堆中建立,並且其字符串是單獨存值的,即便與棧中的數據相同,也不會與棧中的數據共享。
5. 數據類型包裝類的值不可修改。不只僅是String類的值不可修改,全部的數據類型包裝類都不能更改其內部的值。
6. 結論與建議:
(1) 咱們在使用諸如String str = "abc";的格式定義類時,老是想固然地認爲,咱們建立了String類的對象str。擔憂陷阱!對象可能並無被建立!惟一能夠確定的是,指向 String類的引用被建立了。至於這個引用究竟是否指向了一個新的對象,必須根據上下文來考慮,除非你經過new()方法來顯要地建立一個新的對象。所以,更爲準確的說法是,咱們建立了一個指向String類的對象的引用變量str,這個對象引用變量指向了某個值爲"abc"的String類。清醒地認識到這一點對排除程序中難以發現的bug是頗有幫助的。
(2)使用String str = "abc";的方式,能夠在必定程度上提升程序的運行速度,由於JVM會自動根據棧中數據的實際狀況來決定是否有必要建立新對象。而對於String str = new String("abc");的代碼,則一律在堆中建立新對象,而無論其字符串值是否相等,是否有必要建立新對象,從而加劇了程序的負擔。這個思想應該是享元模式的思想,但JDK的內部在這裏實現是否應用了這個模式,不得而知。
(3)當比較包裝類裏面的數值是否相等時,用equals()方法;當測試兩個包裝類的引用是否指向同一個對象時,用==。
(4)因爲String類的immutable性質,當String變量須要常常變換其值時,應該考慮使用StringBuffer類,以提升程序效率ui