【JVM】Java 8 中的常量池、字符串池、包裝類對象池


 

1 - 引言

2 - 常量池
  2.1 你真的懂 Java的“字面量”和“常量”嗎?
  2.2 常量和靜態/運行時常量池有什么關系?什么是常量池?
  2.3 字節碼下的常量池以及常量池的加載機制
  2.4 是不是所有的數字字面量都會被存到常量池中?
3 - 包裝類對象池 =JVM 常量池
4 - 字符串池
  4.1 字符串池的實現——StringTable
  4.2 字符串池存的是實例還是引用?
5 - 補充
  5.1 永久代為何被 HotSpot VM 廢棄?
  5.2 為什么 Java 要分常量、簡單類型、引用類型等
6 - 初學者容易混淆的地方

1 - 引言

摘錄一些網上流傳比較廣泛的認識,但如果你認為只懂這些就夠了,這篇文章就沒有必要繼續看下去了!!!

常量池分為靜態常量池、運行時常量池。
靜態常量池在 .class 中,運行時常量池在方法區中,JDK 1.8 中方法區(method area)已經被元空間(metaspace)代替。
字符串池在JDK 1.7 之后被分離到堆區。
String str = new String("Hello world") 創建了 2 個對象,一個駐留在字符串池,一個分配在 Java 堆,str 指向堆上的實例。
String.intern() 能在運行時向字符串池添加常量。
部分包裝類實現了池化技術,-128~127 以內的對象可以重用。
本文的實例講解都是針對 HotSpot 虛擬機的,如下圖,一般 Oracle 官網上安裝的 JDK 都使用該款虛擬機,使用 java -version 就能查看相關信息了。

2 - 常量池

  2.1 你真的懂 Java的“字面量”和“常量”嗎?

  在計算機科學中,字面量(literal)是用於表達源代碼中一個固定值的表示法(notation)。幾乎所有計算機編程語言都具有對基本值的字面量表示,諸如:整數、浮點數以及字符串等 1 。整數是程序中最常用的數字,整數在 Java 中就是一個整數字面量,例如十進制的1、2、16等,16進制的0x01、0x0A等。Java 中的字符串字面量和其他大多數語言相同,將一系列字符用雙引號括起來,如 "Hello world"等。

  那么常量又是什么呢?如果是從 C/C++ 轉過來的程序員,一般認為常量是被 const 修飾的變量或者某些宏定義,而在 Java 中,final 修飾的變量也可以被稱為是常量。

  但 Java 程序員的圈子里,常量不單單指 final 變量,任何具有不變性的東西我們將它稱為常量也不會帶來什么歧義。

  偶爾會在某些論壇中看到“字符串是常量,不可修改”。那么,這種說法是從哪里來的呢?這就要提到到 Java 中 String 類的設計了,打開 String 的源碼,我們看到前面的幾行定義如下:

public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
...
}


  我們看到不僅類定義使用 final 修飾,關鍵的字符數組同樣聲明為 private final。但這就能保證字符串的不可修改性嗎?並不能,final修飾類定義只能使類不被繼承,字符數組被 final 修飾只能保證 value 不能指向其他內存,但我們仍然可以通過 value[0] = 'V' 的方式直接修改 value 的內容。

 

  String 是不可變,關鍵是因為 SUN 公司的工程師,在后面所有 String 的方法里很小心的沒有去動數組里的元素,沒有暴露內部成員字段。private final char value[] 這一句里,private的私有訪問權限的作用都比 final 大。而且設計師還很小心地把整個 String 設成 final 禁止繼承,避免被其他人繼承后破壞。所以 String 是不可變的關鍵都在底層的實現,而不是一個 final。考驗的是工程師構造數據類型,封裝數據的功力 2。

關於 String 的不可修改性更詳細的內容請參考引用 2 。

  2.2 常量和靜態/運行時常量池有什么關系?什么是常量池?

  在Java程序中,有很多的東西是永恆的,不會在運行過程中變化。比如一個類的名字,一個類字段的名字/所屬類型,一個類方法的名字/返回類型/參數名與所屬類型,一個常量,還有在程序中出現的大量的字面值。而這些在JVM解釋執行程序的時候是非常重要的。那么編譯器將源程序編譯成class文件后,會用一部分字節分類存儲這些不變的代碼,而這些字節我們就稱為常量池 3。

 

  Java 中靜態/運行時常量池並非特指保存 final 常量,它還保存諸如字面量、類和接口全限定名、字段、方法名稱、修飾符等永恆不變的東西。

  2.3 字節碼下的常量池以及常量池的加載機制

JDK 1.8 下常量池存儲的常量類型主要是字面量和符號引用。

下面是靜態/運行時常量池的常量表類型:

常量表類型 標志值(占1 byte) 描述

CONSTANT_Utf8	1	UTF-8編碼的Unicode字符串
CONSTANT_Integer	3	int類型的字面值
CONSTANT_Float	4	float類型的字面值
CONSTANT_Long	5	long類型的字面值
CONSTANT_Double	6	double類型的字面值
CONSTANT_Class	7	對一個類或接口的符號引用
CONSTANT_String	8	String類型字面值的符號引用
CONSTANT_Fieldref	9	對一個字段的符號引用
CONSTANT_Methodref	10	對一個類中方法的符號引用
CONSTANT_InterfaceMethodref	11	對一個接口中方法的符號引用
CONSTANT_NameAndType	12	對一個字段或方法的部分符號引用


  下面講一下符號引用:一個 Java 程序啟動時加載了眾多的類,有JDK的,也有我們自己定義的,那么我們怎么在程序運行的時候准確定位到類的位置呢?比如 String str = new String("xxx"),我們怎么在虛擬機內存中找到 String 這個類的定義(或者說類的字節碼)呢?

  答案就在常量池的符號引用中。在未加載到JVM的時候,在 .class 文件的靜態常量池中我們可以找到這么一項 CONSTANT_Class,當然這一項僅僅只是符號引用,我們只知道有 java.lang.String 這么一個類。只有等 JVM 啟動,並判斷程序用到 java.lang.String 的時候才會加載 String 的 .class 文件到內存中(准確地說是方法區),之后,我們就可以在運行時常量池中將原本的符號引用替換為直接引用了。也就是說實際上我們的定位是依靠運行時常量池的,這也就是為什么運行時常量池對於動態加載非常重要的原因。

  詳細的內容可以了解一下 JVM 的類加載過程(加載、連接和初始化),如下圖,將 .class 文件中的靜態常量池轉換為方法區的運行時常量池發生在“Loading”階段,而符號引用替換為直接引用發生在 “Resolution”階段。

 


  我們特別關注 CONSTANT_Utf8、CONSTANT_String 這兩種常量類型。

  CONSTANT_Utf8:用 UTF-8 編碼方式來表示程序中所有的重要常量字符串。這些字符串包括: ①類或接口的全限定名, ②超類的全限定名,③父接口的全限定名, ④類字段名和所屬類型名,⑤類方法名和返回類型名、以及參數名和所屬類型名,⑥字符串字面值。

  每一個 CONSTANT_Utf8 常量項包括三項信息:length of byte array、length of string、string,以 System.out.println("Hello world") 為例,我們可以找到下面這兩個 utf8 常量項(out、println 相關常量項省略了)。

 

 

  CONSTANT_String:字符串字面量都以 utf8 的形式存儲,但是使用CONSTANT_Utf8 存儲的各種類型字符串這么多,哪些是字符串字面量?哪些是全限定名字符串?所以需要一些指向該 utf8 項的符號引用常量來區分。CONSTANT_Class 的作用也是類似的,指向的是類全限定名的 utf8 項。

更加詳細的內容參考《Java虛擬機規范 Java SE 8版》4。

  2.4 是不是所有的數字字面量都會被存到常量池中?


看看下面的代碼:

void main(){
    int i = 1;
}


  是不是能在常量池中找到CONSTANT_Integer 為 1 的項呢?很遺憾,我們並沒有找到這么一項 ,直到 int i = 32768 我們才在表中找到 CONSTANT_Integer 為 32768 的項。

  為什么會出現這種情況呢?對於整數字面量來說,如果值在 -32768~32767 都會直接嵌入指令中,而不會保存在常量區。

  對於 long、double 都有一些類似的情況,比如long l = 1L、double d = 1.0,都找不到對應的常量項。

  但是如果使用 final 修飾變量,將其定義成類常量(注意不是在方法體內定義的局部常量),結果又有所不同,如下:

class main{
  final int i = 1;
}


此時,我們可以在常量池中找到 CONSTANT_Integer 為 1 的項。

 

3 - 包裝類對象池 =JVM 常量池

  包裝類的對象池(也有稱常量池)和JVM的靜態/運行時常量池沒有任何關系。靜態/運行時常量池有點類似於符號表的概念,與對象池相差甚遠。

  包裝類的對象池是池化技術的應用,並非是虛擬機層面的東西,而是 Java 在類封裝里實現的。打開 Integer 的源代碼,找到 cache 相關的內容:

/**
     * Cache to support the object identity semantics of autoboxing for values between
     * -128 and 127 (inclusive) as required by JLS.
     * <p>
     * The cache is initialized on first usage. The size of the cache
     * may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option.
     * During VM initialization, java.lang.Integer.IntegerCache.high property
     * may be set and saved in the private system properties in the
     * sun.misc.VM class.
     */

    private static class IntegerCache {
        static final int low = -128;
        static final int high;
        static final Integer cache[];

        static {
// high value may be configured by property
            int h = 127;
            String integerCacheHighPropValue =
                    sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {
                    int i = parseInt(integerCacheHighPropValue);
                    i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
                    h = Math.min(i, Integer.MAX_VALUE - (-low) - 1);
                } catch (NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
                }
            }
            high = h;

            cache = new Integer[(high - low) + 1];
            int j = low;
            for (int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);

// range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache.high >= 127;
        }

        private IntegerCache() {
        }
    }

    /**
     * Returns an {@code Integer} instance representing the specified
     * {@code int} value. If a new {@code Integer} instance is not
     * required, this method should generally be used in preference to
     * the constructor {@link #Integer(int)}, as this method is likely
     * to yield significantly better space and time performance by
     * caching frequently requested values.
     * <p>
     * This method will always cache values in the range -128 to 127,
     * inclusive, and may cache other values outside of this range.
     *
     * @param i an {@code int} value.
     * @return an {@code Integer} instance representing {@code i}.
     * @since 1.5
     */
    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

  

  IntegerCache 是 Integer 在內部維護的一個靜態內部類,用於對象緩存。通過源碼我們知道,Integer 對象池在底層實際上就是一個變量名為 cache 的數組,里面包含了 -128 ~ 127 的 Integer 對象實例。

  使用對象池的方法就是通過 Integer.valueOf() 返回 cache 中的對象,像 Integer i = 10 這種自動裝箱實際上也是調用 Integer.valueOf() 完成的。

  如果使用的是 new 構造器,則會跳過 valueOf(),所以不會使用對象池中的實例。

Integer i1 = 10;
Integer i2 = 10;
Integer i3 = new Integer(10);
Integer i4 = new Integer(10);
Integer i5 = Integer.valueOf(10);

System.out.println(i1 == i2); // true
System.out.println(i2 == i3); // false
System.out.println(i3 == i4); // false
System.out.println(i1 == i5); // true

  

注意到注釋中的一句話 “The cache is initialized on first usage”,緩存池的初始化在第一次使用的時候已經全部完成,這涉及到設計模式的一些應用。這和常量池中字面量的保存有很大區別,Integer 不需要顯示地出現在代碼中才添加到池中,初始化時它已經包含了所有需要緩存的對象。

4 - 字符串池

字符串池也是類似於對象池的這么一種概念,但它是 JVM 層面的技術。

  在 JDK 1.6 以及以前的版本中,字符串池是放在 Perm 區(Permanent Generation,永久代)。Perm 區是一個類靜態的區域,主要存儲一些加載類的信息,常量池,方法片段等內容,容量是固定的,默認在 32 M 到 96 M 間,我們可以通過 -XX:MaxPermSize = N 來配置永久代的大小,但是在運行過程中它仍然還是固定大小的。也有說 Perm 區實際上就是 HotSpot 下的方法區,HotSpot 的開發人員更願意將方法區稱為 Permanent Generation,這里我們不做過多的探討。

  在 JDK 1.7 的版本中,字符串池移到Java Heap。在 JDK 1.8 中永久代的說法被廢棄,元空間成為方法區的替代品。(本文 5.1 章節補充關於為什么永久代被廢棄)

4.1 字符串池的實現——StringTable

由於字符串池是虛擬機層面的技術,所以在 String 的類定義中並沒有類似 IntegerCache 這樣的對象池,String 類中提及緩存/池的概念只有intern() 這個方法,我將部分注釋做了一些翻譯和刪減:

/**
* 返回一個標准的字符串對象。
* 
* A pool of strings, initially empty, is maintained privately by the
* class {@code String}.
* 
* 當 intern 方法被調用,若池中包含一個被{@link #equals(Object)}方法認定為和該
* String對象相等的String,那么返回池中的String,否則,將該String對象添加到池中
* 並返回它的引用。
* 
* All literal strings and string-valued constant expressions are
* interned. 
*/
public native String intern();


  我們看到,intern() 是一個native 的方法,那么說明它本身並不是由 Java 語言實現的,而是通過 jni (Java Native Interface)調用了其他語言(如C/C++)實現的一些外部方法。

  在 JDK 1.7后,Oracle 接管了 Java 的源碼后就不對外開放了,根據 JDK 的主要開發人員聲明 openJdk7 和 JDK 1.7 使用的是同一分主代碼,只是分支代碼會有些許的變動。所以可以直接跟蹤 openJdk7 的源碼來探究 intern() 的實現。

//// \openjdk7\hotspot\src\share\vm\prims\jvm.cpp

// String support 
JVM_ENTRY(jstring, JVM_InternString(JNIEnv *env, jstring str))
JVMWrapper("JVM_InternString");
JvmtiVMObjectAllocEventCollector oam;
if (str == NULL) return NULL;
oop string = JNIHandles::resolve_non_null(str);
oop result = StringTable::intern(string, CHECK_NULL);
return (jstring) JNIHandles::make_local(env, result);
JVM_END

  

  大體實現:Java 調用 c++ 實現的 StringTable 的 intern() 方法。StringTable 的 intern() 方法跟 Java 中的 HashMap 的實現是差不多的,只是不能自動擴容,默認大小是1009。(不懂HashMap/HashSet/HashTable 實現的趕緊惡補~)

  字符串池(String pool)實際上是一個 HashTable。Java 中 HashMap 和 HashTable 的原理大同小異,將字符串池看作哈希表更便於我們套用學習數據結構時的一些知識。比如解決數據沖突時,HashMap 和 HashTable 使用的是開散列(或者說拉鏈法)。

參考引用 5 :

The string pool is implemented as a fixed capacity hash map 
with each bucket containing a list of strings with the same hash code.
Some implementation details could be obtained from the following
Java bug report: http://bugs.sun.com/view_bug.do?bug_id=6962930.

常量池是一個固定容量的 hash map,每一個 bucket 包含一系列相同 hash 碼的字符串。

 

The default pool size is 1009 (it is present 
in the source code of the above mentioned bug report, increased in Java7u40).
It was a constant in the early versions of Java 6 and became configurable
between Java6u30 and Java6u41. It is configurable in Java 7 from the beginning
(at least it is configurable in Java7u02). You need to specify -XX:StringTableSize = N,
where N is the string pool map size. Ensure it is a prime number for the better performance.

池的默認大小為 1009,在早期的 1.6 版本中是固定的,但是在 Java6u30 后,我們已經可以通過 -XX:StringTableSize = N 參數來配置這個 hash map 的大小。

 

This parameter will not help you a lot in Java 6, 
because you are still limited by a fixed size PermGen size.
The further discussion will exclude Java 6.

但是配置 hash map 的大小在 1.6 版本中意義不大,因為,此時 String pool 還在永久代中,正如我們前面所說,永久代的大小是固定的,hash map 的大小受限於此,我們仍需要小心使用 intern(),否則就有溢出的風險。

 

  4.2 字符串池存的是實例還是引用?

  這個問法其實本身就不太妥當,根據《Java 虛擬機規范》,堆是供對象實例化分配的區域,Java 程序中的對象實例都應該分配在堆上,我們通過引用對這些實例進行訪問。在 HotSpot 下的 reference 類型使用的都是直接指針的訪問形式,也就是直接指向堆上的實例對象。(相信大家也聽過 reference 使用句柄而非直接指針的另一種訪問形式,不過這里討論的是 HotSpot VM)

 

  字符串池這個 HashTable 保存的本質上是 reference,我們實際上想要知道的是字符串池是怎么保存引用,引用的指向,有多少個實例的引用而已?

看一道比較常見的面試題,下面的代碼創建了多少個 String 對象?

String s1 = new String("he") + new String("llo");
String s2 = s1.intern();

System.out.println(s1 == s2);
// 在 JDK 1.6 下輸出是 false,創建了 6 個對象
// 在 JDK 1.7 之后的版本輸出是 true,創建了 5 個對象
// 當然我們這里沒有考慮GC,但這些對象確實存在或存在過

  

  為什么輸出會有這些變化呢?主要還是字符串池從永久代中脫離、移入堆區的原因, intern() 方法也相應發生了變化:

1、在 JDK 1.6 中,調用 intern() 首先會在字符串池中尋找 equal() 相等的字符串,假如字符串存在就返回該字符串在字符串池中的引用;假如字符串不存在,虛擬機會重新在永久代上創建一個實例,將 StringTable 的一個表項指向這個新創建的實例。

 

2、在 JDK 1.7 中,由於字符串池不在永久代了,intern() 做了一些修改,更方便地利用堆中的對象。字符串存在時和 JDK 1.6一樣,但是字符串不存在時不再需要重新創建實例,可以直接指向堆上的實例。

 

  由上面兩個圖,也不難理解為什么 JDK 1.6 字符串池溢出會拋出 OutOfMemoryError: PermGen space ,而在 JDK 1.7 拋出 OutOfMemoryError: Java heap space 。

(題外話:寫的過程中還是會下意識地說“字符串池中的字符串”而不是“字符串池中的引用指向的字符串”,也確實可以體諒為什么許多人被這些文字游戲繞得頭昏腦脹)

第 4 節更加詳細的內容請參考引用 6 。

5 - 補充

  5.1 永久代為何被 HotSpot VM 廢棄?

引用 7 提到原因有兩個:

由於 Permanent Generation 內存經常不夠用或發生內存泄露,引發惱人的java.lang.OutOfMemoryError: PermGen (在Java Web開發中非常常見)。

移除 Permanent Generation 可以促進 HotSpot JVM 與 JRockit VM 的融合,因為 JRockit 沒有永久代 。

This is part of the JRockit and Hotspot convergence effort.
JRockit customers do not need to configure the permanent generation
(since JRockit does not have a permanent generation) and are accustomed
to not configuring the permanent generation. —— JEP 122: Remove the Permanent Generation

 

  早在 JDK 1.7 的版本中 Orcale 已經宣布要廢棄永久代了,事實上,永久代擁有了實例對象,這本身就已經不符合虛擬機規范。大多數虛擬機都沒有設置永久代的概念,字符串池的移出,更是使得永久代名存實亡,HotSpot 讓永久代回歸為方法區, 恐怕也是順應潮流了。

  甚至我們可以理解為 JDK 1.6 下的 永久代 = 字符串池 + 方法區 或者 永久代 = (包含字符串池的)方法區。了解它的真實存在形式,怎么稱呼也無傷大雅。

  5.2 為什么 Java 要分常量、簡單類型、引用類型等

這是偶然看到的一段話,特別摘錄下來:

  為什么 Java 要分常量、簡單類型、引用類型等?
  顯然 Java 並非是為了考試和刁難它的使用者而徒增這些概念的。唯一的動機就是增加復雜性換取性能。那么如果不換取性能,最簡單的方式是什么呢?顯然就是一切變量都是引用類型,這是最簡單的,一個引用類型可以概括 Java 里所有的東西。

  那么簡單類型和常量是什么?它是特例,用特例換取性能。
  對於整數來說,它頻繁參與到計算中,如果用定義一個類,並且使用一個指針的方式來使用它,就會浪費很多性能,所以才有了簡單類型。而常量是怎么回事?它是對大量重復使用的引用類型的一種性能優化,用共享對象的方式,來將大量相同的對象合並存儲唯一的一份 8。

6 - 初學者容易混淆的地方

提到常量池,一般指的是方法區中的靜態/運行時常量池。
字符串池/字符串常量池/字符串對象池/String Pool/String Table 都可以看作一個東西。
包裝類對象池技術和 JVM 的常量池沒有任何關系。
 
轉自:https://blog.csdn.net/Xu_JL1997/article/details/89150026 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM