1. 程式人生 > >淺談JAVA中字符串常量的儲存位置

淺談JAVA中字符串常量的儲存位置

數據 每一個 [] jit 返回 inf post 符號 boolean

在講述這些之前我們需要一些預備知識:

Java的內存結構我們可以通過兩個方面去看待它。

一、從抽象的JVM的角度去看。相關定義請參考JVM規範:Chapter 2. The Structure of the Java Virtual Machine

從該角度看的話Java內存結構包含以下部分:該部分內容可以結合:JVM簡介(更加詳細深入的介紹)

1、棧區:由編譯器自動分配釋放,具體方法執行結束後,系統自動釋放JVM內存資源。

其作用有保存局部變量的值,包括:1.用來保存基本數據類型的值;2.保存類的實例對象的引用。也可以用來保存加載方法時的幀。

2、堆區:一般由程序員分配釋放,JVM不定時查看這個對象,如果沒有引用指向這個對象就回收。

其作用為用來存放動態產生的數據,包括new出來的實例,數組等。註意創建出來的對象只包含屬於各自的成員變量,並不包括成員方法。

因為同一個類的對象擁有各自的成員變量,存儲在各自的堆中,但是他們共享該類的方法,並不是每創建一個對象就把成員方法復制一次。

3、代碼區存放程序中方法的二進制代碼,而且是多個對象共享一個代碼空間區域。

4、數據區:用來存放static定義的靜態成員。

5、常量池:JVM為每個已加載的類型維護一個常量池,常量池就是這個類型用到的常量的一個有序集合。包括直接常量(基本類型,String)和對其他類型、方法、字段的符號引用。池中的數據和數組一樣通過索引訪問。由於常量池包含了一個類型所有的對其他類型、方法、字段的符號引用,所以常量池在Java的動態鏈接中起了核心作用。常量池存在於堆中。

下圖大致描述了JAVA的內存分配

技術分享圖片

二、從操作系統上的進程的角度。相關定義請參考各種操作系統的資料,例如Linux的話可以參考這個簡單的介紹:Linux Processes explained (此方面一般被較少地談論到,本文對此僅僅做一個稍微的介紹)

這裏切記一點:JVM規範所描述的抽象JVM概念與實際實現並不總一一對應。

接來下我們來看一段代碼實例與註釋:

public class TestStringConstant {
    public static void main(String args[]) {
        // 字符串常量,分配在常量池中,編譯器會對其進行優化,  Interned table
        
// 即當一個字符串已經存在時,不再重復創建一個相同的對象,而是直接將s2也指向"hello". String s1 = "hello"; String s2 = "hello"; // new出來的對象,分配在heap中.s3與s4雖然它們指向的字符串內容是相同的,但是是兩個不同的對象. // 因此==進行比較時,其所存的引用是不同的,故不會相等 String s3 = new String("world"); String s4 = new String("world"); System.out.println(s1 == s2); // true System.out.println(s3 == s4); // false System.out.println(s3.equals(s4)); // true // String中equals方法已經被重寫過,比較的是內容是否相等. } }

那麽對於上例代碼中提到的編譯器的優化,下面將進行更進一步的詳細介紹。請看下例代碼:

class A {
    private String a = "aa";
    public boolean methodB() {
        String b = "bb";
        final String c = "cc";
        return false;
    }
}

 "aa"、"bb"的String對象按JVM規範在Java heap上,在JDK8之前的HotSpot VM實現裏在PermGen,在JDK7開始的HotSpot VM裏在普通Java heap裏(而不在PermGen裏);"cc"如果存在的話也一樣,但是可能會不存在。

這些String對象屬於“interned String”。String是Java對象,根據JVM規範的定義它必須存在於Java heap中,interned String也不例外。Interned String特別的地方在於JVM會有個StringTable存著interned String的引用,保證內容相同的String對象不被重復intern。(這裏便是編譯器的優化)

這個StringTable怎樣實現JVM規範裏並沒有規定,不過通常它並不保存String對象的內容,而只是保存String對象的引用而已。

  • 從JVM規範看a、b、c變量:

a變量作為A類的對象實例字段,會跟隨A的實例在Java heap上。

b變量作為局部變量會在Java線程棧上。

c變量雖然也是局部變量,但因為有final修飾並且有初始化為一個常量值,所以c是一個常量。它可能會被優化掉(就沒有c這個變量了),也可能跟b一樣作為局部變量在Java線程棧上。

通過以上相信大家對於字符串常量的分配區域以及java的內存分配有了一個較為形象的了解。

下面是一些相關知識點的補充與註意事項:

1.分清什麽是實例什麽是對象。Class a= new Class();此時a叫實例,而不能說a是對象。實例在棧中,對象在堆中,操作實例實際上是通過實例的指針間接操作對象。多個實例可以指向同一個對象。

2.棧中的數據和堆中的數據銷毀並不是同步的。方法一旦結束,棧中的局部變量立即銷毀,但是堆中對象不一定銷毀。因為可能有其他變量也指向了這個對象,直到棧中沒有變量指向堆中的對象時,它才銷毀,而且還不是馬上銷毀,要等垃圾回收掃描時才可以被銷毀。

3.以上的棧、堆、代碼段、數據段等等都是相對於應用程序而言的。每一個應用程序都對應唯一的一個JVM實例,每一個JVM實例都有自己的內存區域,互不影響。並且這些內存區域是所有線程共享的。這裏提到的棧和堆都是整體上的概念,這些堆棧還可以細分。

4.類的成員變量在不同對象中各不相同,都有自己的存儲空間(成員變量在堆中的對象中)。而類的方法卻是該類的所有對象共享的,只有一套,對象使用方法的時候方法才被壓入棧,方法不使用則不占用內存。

  • 從HotSpot VM的實現看:

當methodB()被解釋執行時,輸入的字節碼是怎樣的就會怎樣執行,而由於javac的實現不會優化掉變量b,所以調用methodB()時它一定會在Java線程棧上的局部變量區裏;當字節碼裏變量c存在時,它也跟b一樣在Java線程棧的局部變量區。

當methodB()被JIT編譯執行時,由於局部變量b、c都沒有被使用,所以它們經過JIT編譯後就消失了,調用methodB()不會在棧上給b或c變量分配任何空間。

通過以上相信大家對於字符串常量的分配區域以及java的內存分配有了一個較為形象的了解。

下面是一些相關知識點的補充與註意事項:

1.分清什麽是實例什麽是對象。Class a= new Class();此時a叫實例,而不能說a是對象。實例在棧中,對象在堆中,操作實例實際上是通過實例的指針間接操作對象。多個實例可以指向同一個對象。

2.棧中的數據和堆中的數據銷毀並不是同步的。方法一旦結束,棧中的局部變量立即銷毀,但是堆中對象不一定銷毀。因為可能有其他變量也指向了這個對象,直到棧中沒有變量指向堆中的對象時,它才銷毀,而且還不是馬上銷毀,要等垃圾回收掃描時才可以被銷毀。

3.以上的棧、堆、代碼段、數據段等等都是相對於應用程序而言的。每一個應用程序都對應唯一的一個JVM實例,每一個JVM實例都有自己的內存區域,互不影響。並且這些內存區域是所有線程共享的。這裏提到的棧和堆都是整體上的概念,這些堆棧還可以細分。

4.類的成員變量在不同對象中各不相同,都有自己的存儲空間(成員變量在堆中的對象中)。而類的方法卻是該類的所有對象共享的,只有一套,對象使用方法的時候方法才被壓入棧,方法不使用則不占用內存。

對於String的相關補充:

對於String的修改其實是new了一個StringBuilder並調用append方法,然後調用toString返回一個新的String.

(註意:append方法並不會new一個新的對象.)

但是JVM是會對String進行優化的,比如:

String str = "I" + "love" + "java"

其中的字符串在編譯的時候就能夠確認,所以編譯器會直接將其拼接成一個字符串放在常量池:"I love java"

但是若代碼為下面這樣:

String a = "I";
String b = "love";
String c = "java";
String str = a + b + c;

那麽只有等到運行的時候才能夠確定str最終是什麽,編譯器並不會對其進行優化,而是通過StringBuilder對字符串改變來實現的。

但是註意,要是此處給 a, b, c添加上 final 關鍵字,則編譯器就能夠對其進行優化。我們可以做下面這樣一個測試:

public class foo{
    public static void main(String[] args) {
        String a = "I ";
        String b = "love ";
        String c = "java";
        
        final String a1 = "I ";
        final String b1 = "love ";
        final String c1 = "java";
        
        String str = a + b + c;
        String str1 = a1 + b1 + c1;     // equals to str1 = "I " + "love " + "java"
        String str2 = "I " + "love " + "java";
        
        System.out.println(str == "I love java");   // output false
        System.out.println(str1 == "I love java");  // output true
        System.out.println(str2 == "I love java");  // output true
        System.out.println(a + "love " + "java" == "I love java");  // output false
        System.out.println(a1 + "love " + "java" == "I love java");  // output true
    }
}

Note:

  StringBuilder 和 StringBuffer 的區別:

StringBuffer 是線程安全的;StringBuilder 是非線程安全的。

因為StingBuffer是在StringBuilder的基礎上加鎖,而加鎖是一個重量級的操作,需要調用操作系統內核來實現。

比較耗時。

故在效率上: StringBuilder > StringBuffer

淺談JAVA中字符串常量的儲存位置