好好說說Java中的常量池之Class常量池
在Java中,常量池的概念想必很多人都聽說過。這也是面試中比較常考的題目之一。在Java有關(guān)的面試題中,一般習(xí)慣通過String的有關(guān)問題來考察面試者對于常量池的知識的理解,幾道簡單的String面試題難倒了無數(shù)的開發(fā)者。所以說,常量池是Java體系中一個(gè)非常重要的概念。
談到常量池,在Java體系中,共用三種常量池。分別是字符串常量池、Class常量池和運(yùn)行時(shí)常量池。
本文是《好好說說Java中的常量池》系列的***篇,先來介紹一下到底什么是Class常量池。
什么是Class文件
在Java代碼的編譯與反編譯那些事兒中我們介紹過Java的編譯和反編譯的概念。我們知道,計(jì)算機(jī)只認(rèn)識0和1,所以程序員寫的代碼都需要經(jīng)過編譯成0和1構(gòu)成的二進(jìn)制格式才能夠讓計(jì)算機(jī)運(yùn)行。
我們在深入分析Java的編譯原理中提到過,為了讓Java語言具有良好的跨平臺能力,Java獨(dú)具匠心的提供了一種可以在所有平臺上都能使用的一種中間代碼——字節(jié)碼(ByteCode)。
有了字節(jié)碼,無論是哪種平臺(如Windows、Linux等),只要安裝了虛擬機(jī),都可以直接運(yùn)行字節(jié)碼。
同樣,有了字節(jié)碼,也解除了Java虛擬機(jī)和Java語言之間的耦合。這話可能很多人不理解,Java虛擬機(jī)不就是運(yùn)行Java語言的么?這種解耦指的是什么?
其實(shí),目前Java虛擬機(jī)已經(jīng)可以支持很多除Java語言以外的語言了,如Groovy、JRuby、Jython、Scala等。之所以可以支持,就是因?yàn)檫@些語言也可以被編譯成字節(jié)碼。而虛擬機(jī)并不關(guān)心字節(jié)碼是有哪種語言編譯而來的。
Java語言中負(fù)責(zé)編譯出字節(jié)碼的編譯器是一個(gè)命令是javac。
javac是收錄于JDK中的Java語言編譯器。該工具可以將后綴名為.java的源文件編譯為后綴名為.class的可以運(yùn)行于Java虛擬機(jī)的字節(jié)碼。
如,我們有以下簡單的HelloWorld.java代碼:
- public class HelloWorld {
- public static void main(String[] args) {
- String s = "Hollis";
- }
- }
通過javac命令生成class文件:
- javac HelloWorld.java
生成HelloWorld.class文件:
如何使用16進(jìn)制打開class文件:使用 vim test.class ,然后在交互模式下,輸入:%!xxd即可。
可以看到,上面的文件就是Class文件,Class文件中包含了Java虛擬機(jī)指令集和符號表以及若干其他輔助信息。
要想能夠讀懂上面的字節(jié)碼,需要了解Class類文件的結(jié)構(gòu),由于這不是本文的重點(diǎn),這里就不展開說明了。
讀者可以看到,HelloWorld.class文件中的前八個(gè)字母是cafe babe,這就是Class文件的魔數(shù)(Java中的”魔數(shù)”)
我們需要知道的是,在Class文件的4個(gè)字節(jié)的魔數(shù)后面的分別是4個(gè)字節(jié)的Class文件的版本號(第5、6個(gè)字節(jié)是次版本號,第7、8個(gè)字節(jié)是主版本號,我生成的Class文件的版本號是52,這時(shí)Java 8對應(yīng)的版本。也就是說,這個(gè)版本的字節(jié)碼,在JDK 1.8以下的版本中無法運(yùn)行)在版本號后面的,就是Class常量池入口了。
Class常量池
Class常量池可以理解為是Class文件中的資源倉庫。 Class文件中除了包含類的版本、字段、方法、接口等描述信息外,還有一項(xiàng)信息就是常量池(constant pool table),用于存放編譯器生成的各種字面量(Literal)和符號引用(Symbolic References)。
由于不同的Class文件中包含的常量的個(gè)數(shù)是不固定的,所以在Class文件的常量池入口處會設(shè)置兩個(gè)字節(jié)的常量池容量計(jì)數(shù)器,記錄了常量池中常量的個(gè)數(shù)。
當(dāng)然,還有一種比較簡單的查看Class文件中常量池的方法,那就是通過javap命令。對于以上的HelloWorld.class,可以通過
- javap -v HelloWorld.class
查看常量池內(nèi)容如下:
從上圖中可以看到,反編譯后的class文件常量池中共有16個(gè)常量。而Class文件中常量計(jì)數(shù)器的數(shù)值是0011,將該16進(jìn)制數(shù)字轉(zhuǎn)換成10進(jìn)制的結(jié)果是17。
原因是:與Java的語言習(xí)慣不同,常量池計(jì)數(shù)器是從1開始而不是從0開始的,常量池的個(gè)數(shù)是 十進(jìn)制的17,這就代表了其中有16個(gè)常量,索引值范圍為1-16。
常量池中有什么
介紹完了什么是Class常量池以及如何查看常量池,那么接下來我們就要深入分析一下,Class常量池中都有哪些內(nèi)容。
常量池中主要存放兩大類常量:字面量(literal)和符號引用(symbolic references)。
字面量
前面說過,運(yùn)行時(shí)常量池中主要保存的是字面量和符號引用,那么到底什么字面量?
在計(jì)算機(jī)科學(xué)中,字面量(literal)是用于表達(dá)源代碼中一個(gè)固定值的表示法(notation)。幾乎所有計(jì)算機(jī)編程語言都具有對基本值的字面量表示,諸如:整數(shù)、浮點(diǎn)數(shù)以及字符串;而有很多也對布爾類型和字符類型的值也支持字面量表示;還有一些甚至對枚舉類型的元素以及像數(shù)組、記錄和對象等復(fù)合類型的值也支持字面量表示法。
以上是關(guān)于計(jì)算機(jī)科學(xué)中關(guān)于字面量的解釋,并不是很容易理解。說簡單點(diǎn),字面量就是指由字母、數(shù)字等構(gòu)成的字符串或者數(shù)值。
字面量只可以右值出現(xiàn),所謂右值是指等號右邊的值,如:int a=123這里的a為左值,123為右值。在這個(gè)例子中123就是字面量。
- int a = 123;String s = "hollis";
上面的代碼示例中,123和hollis都是字面量。
本文開頭的HelloWorld代碼中,Hollis就是一個(gè)字面量。
符號引用
常量池中,除了字面量以外,還有符號引用,那么到底什么是符號引用呢。
符號引用是編譯原理中的概念,是相對于直接引用來說的。主要包括了以下三類常量:
- 類和接口的全限定名
- 字段的名稱和描述符
- 方法的名稱和描述符
這也就可以印證前面的常量池中還包含一些com/hollis/HelloWorld、main、([Ljava/lang/String;)V等常量的原因了。
Class常量池有什么用
前面介紹了這么多,關(guān)于Class常量池是什么,怎么查看Class常量池以及Class常量池中保存了哪些東西。有一個(gè)關(guān)鍵的問題沒有講,那就是Class常量池到底有什么用。
首先,可以明確的是,Class常量池是Class文件中的資源倉庫,其中保存了各種常量。而這些常量都是開發(fā)者定義出來,需要在程序的運(yùn)行期使用的。
在《深入理解Java虛擬》中有這樣的表述:
Java代碼在進(jìn)行Javac編譯的時(shí)候,并不像C和C++那樣有“連接”這一步驟,而是在虛擬機(jī)加載Class文件的時(shí)候進(jìn)行動態(tài)連接。也就是說,在Class文件中不會保存各個(gè)方法、字段的最終內(nèi)存布局信息,因此這些字段、方法的符號引用不經(jīng)過運(yùn)行期轉(zhuǎn)換的話無法得到真正的內(nèi)存入口地址,也就無法直接被虛擬機(jī)使用。當(dāng)虛擬機(jī)運(yùn)行時(shí),需要從常量池獲得對應(yīng)的符號引用,再在類創(chuàng)建時(shí)或運(yùn)行時(shí)解析、翻譯到具體的內(nèi)存地址之中。關(guān)于類的創(chuàng)建和動態(tài)連接的內(nèi)容,在虛擬機(jī)類加載過程時(shí)再進(jìn)行詳細(xì)講解。
這段話,看起來很繞,不是很容易理解。其實(shí)他的意思就是: Class是用來保存常量的一個(gè)媒介場所,并且是一個(gè)中間場所。在JVM真的運(yùn)行時(shí),需要把常量池中的常量加載到內(nèi)存中。
至于到底哪個(gè)階段會做這件事情,以及Class常量池中的常量會以何種方式被加載到具體什么地方,會在本系列文章的后續(xù)內(nèi)容中繼續(xù)闡述。歡迎關(guān)注我的博客(https://www.hollischuang.com) 和公眾號(Hollis),即可***時(shí)間獲得***內(nèi)容。
另外,關(guān)于常量池中常量的存儲形式,以及數(shù)據(jù)類型的表示方法本文中并未涉及,并不是說這部分知識點(diǎn)不重要,只是Class字節(jié)碼的分析本就枯燥,作者不想在一篇文章中給讀者灌輸太多的理論上的內(nèi)容。感興趣的讀者可以自行Google學(xué)習(xí),如果真的有必要,我也可以單獨(dú)寫一篇文章再深入介紹。
【本文是51CTO專欄作者Hollis的原創(chuàng)文章,作者微信公眾號Hollis(ID:hollischuang)】






























