一、什么是常量
用final修飾的成員變量表示常量,值一旦給定就無法改變!
final修飾的變量有三種:靜態變量、實例變量和局部變量,分別表示三種類型的常量。
Java中的常量池,實際上分為兩種形態:靜態常量池和運行時常量池。
(1)所謂靜態常量池,即*.class文件中的常量池,class文件中的常量池不僅僅包含字符串(數字)字面量,還包含類、方法的信息,占用class文件絕大部分空間。
(2)而運行時常量池,則是 JVM 虛擬機在完成類裝載操作后,將class文件中的常量池載入到內存中,並保存在方法區中,我們常說的常量池,就是指方法區中的運行時常量池。
二、字符串常量池(String Constant Pool)
1、字符串常量池在Java內存區域的哪個位置?
(1)在JDK6.0及之前版本,字符串常量池是放在Perm Gen區(也就是方法區)中;
(2)在JDK7.0版本,字符串常量池被移到了堆中了。至於為什么移到堆內,大概是由於方法區的內存空間太小了。
2、字符串常量池是什么?
(1)在HotSpot VM里實現的 string pool 功能的是一個StringTable類,它是一個Hash表,默認值大小長度是1009;這個StringTable在每個HotSpot VM的實例只有一份,被所有的類共享。字符串常量由一個一個字符組成,放在了StringTable上。
(2)在JDK6.0中,StringTable的長度是固定的,長度就是1009,因此如果放入String Pool中的String非常多,就會造成hash沖突,導致鏈表過長,當調用String#intern()時會需要到鏈表上一個一個找,從而導致性能大幅度下降;
(3)在JDK7.0中,StringTable的長度可以通過參數指定:
-XX:StringTableSize=66666
3、字符串常量池里放的是什么?
(1)在JDK6.0及之前版本中,String Pool里放的都是字符串常量;
(2)在JDK7.0中,由於String#intern()發生了改變,因此String Pool中也可以存放放於堆內的字符串對象的引用。
需要說明的是:字符串常量池中的字符串只存在一份!
如:
1 String s1 = "hello,world!"; 2 String s2 = "hello,world!";
即執行完第一行代碼后,常量池中已存在 “hello,world!”,那么 s2不會在常量池中申請新的空間,而是直接把已存在的字符串內存地址返回給s2。
4、
5、
6、
字符串常量池是全局的,JVM 中獨此一份,因此也稱為全局字符串常量池。運行時常量池中的字符串字面量若是成員的,則在類的加載初始化階段就使用到了字符串常量池;若是本地的,則在使用到的時候(執行此代碼時)才會使用到字符串常量池。其實,“使用常量池”對應的字節碼是一個 ldc 指令,在給 String 類型的引用賦值的時候會先執行這個指令,看常量池中是否存在這個字符串對象的引用,若有就直接返回這個引用,若沒有,就在堆里創建這個字符串對象並在字符串常量池中記錄下這個引用(jdk1.7)。String 類的 intern() 方法還可在運行期間把字符串放到字符串常量池中。JVM 中除了字符串常量池,8種基本數據類型中除了兩種浮點類型剩余的6種基本數據類型的包裝類,都使用了緩沖池技術,但是 Byte、Short、Integer、Long、Character 這5種整型的包裝類也只是在對應值在 [-128,127] 時才會使用緩沖池,超出此范圍仍然會去創建新的對象。其中:
在 jdk1.6(含)之前也是方法區的一部分,並且其中存放的是字符串的實例;
在 jdk1.7(含)之后是在堆內存之中,存儲的是字符串對象的引用,字符串實例是在堆中;
jdk1.8 已移除永久代,字符串常量池是在本地內存當中,存儲的也只是引用。
三、Class常量池(Class Constant Pool)
1、class常量池簡介
(1)我們寫的每一個Java類被編譯后,就會形成一份class文件;class文件中除了包含類的版本、字段、方法、接口等描述信息外,還有一項信息就是常量池(constant pool table),用於存放編譯器生成的各種字面量(Literal)和符號引用(Symbolic References);
(2)每個class文件都有一個class常量池。
2、什么是字面量和符號引用:
(1)字面量包括:1.文本字符串 2.八種基本類型的值 3.被聲明為final的常量等;
(2)符號引用包括:1.類和方法的全限定名 2.字段的名稱和描述符 3.方法的名稱和描述符。
3、
4、
5、
6、
class文件的@頭4個字節稱為魔數@,它的唯一作用就是確定這個文件時候是一個能被虛擬機接受的class文件。很多圖片格式都用一個魔數來標識文件類型,比如png和jpg等。在java的class文件中,這個數是0xcafebabe。接下來就是class文件的版本號,第5、6個字節是@次版本號@,第7、8個字節是@主版本號@。在這里,次版本號是0,主版本號是52,(十六進制是34),緊接着主版本號的就是@常量池@,常量池可以理解為class文件的資源倉庫,它是class文件結構中與其它項目關聯最多的數據類型,也是占用class文件空間最大的數據項目之一,也是class文件中第一個出現的表類型數據項目。這些符號引用在虛擬機中如果不經過轉換則無法與實際內存相連接,即無法被虛擬機直接使用,在虛擬機運行時,需要從常量池獲得對應的符號引用,再在類創建時或運行時解析並翻譯到具體的內存地址中。每項常量都是一個表,而由於各個常量的類型不一,大小也不相同,所以同樣需要一個u1類型的數據來標記常量的類型,以確定其后的常量表的格式。
常量池(constant_pool)指的是在編譯期被確定,並被保存在已編譯的.class文件中的一些數據。它包括了關於類、方法、接口等中的常量,也包括字符串常量和符號引用。
常量池主要用於存放兩大類常量:字面量(Literal)和符號引用量(SymbolicReferences),字面量相當於Java語言層面常量的概念,如文本字符串,聲明為final的常量值等,符號引用則屬於編譯原理方面的概念,包括了如下三種類型的常量:
類和接口的全限定名; 字段名稱和描述符; 方法名稱和描述符
Java中八種基本類型的包裝類的大部分都實現了常量池技術,它們是Byte、Short、Integer、Long、Character、Boolean,另外兩種浮點數類型的包裝類(Float、Double)則沒有實現。另外Byte,Short,Integer,Long,Character這5種整型的包裝類也只是在對應值在-128到127時才可使用對象池。
在常量池之后,緊接着的2個字節代表@訪問標志@,即在前面說到的,這個Class是類還是接口,是用哪個修飾符來修飾,abstract,public等,還有,如果是類的話,是否被聲明為final,等等。
訪問標志之后,則是@類索引、父索引與接口索引@的集合。類索引和父類索引都是一個u2類型的數據,而接口索引集合是一組u2類型的數據的集合,Class文件中由這三項數據來確定這個類的繼承關系。類索引用來確定這個類的全限定名,父類索引用於確定這個類的父類的全限定名,接口索引集合用來描述這個類實現了哪些接口,這些被實現的接口將按實現或繼承的順序從左到右的順序排列在接口的索引集合中。類索引、父類索引和接口索引都按順序排列在訪問標志之后。
接下來就是@字段表@了,此處字段表存的就是前文說的類成員變量或實例成員變量,但不包括方法內部聲明的變量。如果類存在父類,則除非子類覆蓋了父類的字段定義,否則在子類中不會列出從超類或父接口中繼承而來的字段,但有可能列出原來java代碼中不存在的字段,譬如在內部類為了保持對外部類的訪問性,會自動添加指向外部類實例的字段。另外,java中是不允許出現相同的字段名的,但對於字節碼來說,如果兩個字段的描述符不一致,則字段重名是合法的。
字段表之后就是@方法表@集了。方法表集合與字段表集合的結構形式幾乎完全一致。此處,方法中的代碼的存放位置則是方法表的屬性表中的一項名為"Code"的屬性里面。與字段表集合相對應的,如果父類方法在子類中沒有被重寫(Override),則方法表集合中就不會出現來自父類的方法信息。
Class 文件常量池指的是編譯生成的 class 字節碼文件,其結構中有一項是常量池(Constant Pool Table),用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載后進入方法區的運行時常量池中存放。
這里的字面量是指字符串字面量和聲明為 final 的(基本數據類型)常量值,這些字符串字面量除了類中所有雙引號括起來的字符串(包括方法體內的),還包括所有用到的類名、方法的名字和這些類與方法的字符串描述、字段(成員變量)的名稱和描述符;聲明為final的常量值指的是成員變量,不包含本地變量,本地變量是屬於方法的。這些都在常量池的 UTF-8 表中(邏輯上的划分);
符號引用,就是指指向 UTF-8 表中向這些字面量的引用,包括類和接口的全限定名(包括包路徑的完整名)、字段的名稱和描述符、方法的名稱和描述符。只不過是以一組符號來描述所引用的目標,和內存並無關,所以稱為符號引用,直接指向內存中某一地址的引用稱為直接引用;
四、運行時常量池(Runtime Constant Pool)
1、運行時常量池是什么?
運行時常量池存在於內存中,也就是class常量池被加載到內存之后的版本,不同之處是:它的字面量可以動態的添加(String#intern()),符號引用可以被解析為直接引用
JVM在執行某個類的時候,必須經過加載、連接、初始化,而連接又包括驗證、准備、解析三個階段。而當類加載到內存中后,jvm就會將class常量池中的內容存放到運行時常量池中,由此可知,運行時常量池也是每個類都有一個。在解析階段,會把符號引用替換為直接引用,解析的過程會去查詢字符串常量池,也就是我們上面所說的StringTable,以保證運行時常量池所引用的字符串與字符串常量池中是一致的。
2、運行時常量池在哪里?
(1)在JDK1.7之前運行時常量池邏輯包含字符串常量池存放在方法區, 此時hotspot虛擬機對方法區的實現為永久代
(2)在JDK1.7 字符串常量池被從方法區拿到了堆中, 這里沒有提到運行時常量池,也就是說字符串常量池被單獨拿到堆,運行時常量池剩下的東西還在方法區, 也就是hotspot中的永久代
(3)在JDK1.8 hotspot移除了永久代用元空間(Metaspace)取而代之, 這時候字符串常量池還在堆, 運行時常量池還在方法區, 只不過方法區的實現從永久代變成了元空間(Metaspace)
3、運行時常量池
運行時常量池是方法區的一部分。CLass文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池,用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載后進入方法區的運行時常量池中存放。
運行時常量池相對於CLass文件常量池的另外一個重要特征是具備動態性,Java語言並不要求常量一定只有編譯期才能產生,也就是並非預置入CLass文件中常量池的內容才能進入方法區運行時常量池,運行期間也可能將新的常量放入池中,這種特性被開發人員利用比較多的就是String類的intern()方法。
4、
5、
運行時常量池是方法區的一部分,是一塊內存區域。Class 文件常量池將在類加載后進入方法區的運行時常量池中存放。一個類加載到 JVM 中后對應一個運行時常量池,運行時常量池相對於 Class 文件常量池來說具備動態性,Class 文件常量只是一個靜態存儲結構,里面的引用都是符號引用。而運行時常量池可以在運行期間將符號引用解析為直接引用。可以說運行時常量池就是用來索引和查找字段和方法名稱和描述符的。給定任意一個方法或字段的索引,通過這個索引最終可得到該方法或字段所屬的類型信息和名稱及描述符信息,這涉及到方法的調用和字段獲取。
五、
六、常量池的好處
常量池是為了避免頻繁的創建和銷毀對象而影響系統性能,其實現了對象的共享。例如字符串常量池,在編譯階段就把所有的字符串文字放到一個常量池中。
(1)節省內存空間:常量池中所有相同的字符串常量被合並,只占用一個空間。
(2)節省運行時間:比較字符串時,==比equals()快。對於兩個引用變量,只用==判斷引用是否相等,也就可以判斷實際值是否相等