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