修改 class_带你探索JVM的Class文件结构
魔數(shù):
大多數(shù)情況下,我們都是通過擴展名來識別一個文件的類型的,比如我們看到一個.txt類型的文件我們就知道他是一個純文本文件。但是,擴展名是可以修改的,那一旦一個文件的擴展名被修改過,那么怎么識別一個文件的類型呢。這就用到了我們提到的“魔數(shù)”。
很多類型的文件,其起始的幾個字節(jié)的內(nèi)容是固定的(或是有意填充,或是本就如此)。因此這幾個字節(jié)的內(nèi)容也被稱為魔數(shù) (magic number),因為根據(jù)這幾個字節(jié)的內(nèi)容就可以確定文件類型。有了這些魔術(shù)數(shù)字,我們就可以很方便的區(qū)別不同的文件。
為了方便虛擬機識別一個文件是否是class類型的文件,SUN公司規(guī)定每個class文件都必須以一個word(四個字節(jié))作為開始,這個數(shù)字就是魔數(shù)。魔數(shù)是由四個字節(jié)的無符號數(shù)組成的,而class文件的名字還挺好聽的的,其魔數(shù)就是0xCAFEBABE
讀者可以隨便編譯一個class文件,然后然后用十六進制編輯器打開編譯后的class文件,基本格式如下:
唯一的作用是用于確定這個文件是否為一個能被虛擬機接受的Class文件,在代碼中使用魔數(shù),不僅使代碼的可讀性大大降低,還可能導(dǎo)致各種問題。
版本號:
- Class文件版本號:次版本號組成u2+主版本號u2。共占4個字節(jié)。
- 高版本的JDK能向下兼容以前的版本的Class文件,但不能運行高版本的Class文件。
- JDK1.1的版本號為45.0-45.65535(10進制),之后每個大版本發(fā)布主版本號加1,如:JDK1.2:46.0~46.65535。
常量池:
字符數(shù)組的存儲方式
public1. 字符串常量池(String Constant Pool):
即String Pool,但是JVM中對應(yīng)的類是StringTable,底層實現(xiàn)是一個hashtable,看代碼
classKey的生成方式
- 通過String的內(nèi)容+長度生成hash值
- 將hash值轉(zhuǎn)為key
Value的生成方式
將Java的String類的實例instanceOopDesc封裝成HashtableEntry
HashtableEntrytemplateString.hashcode()
String類重寫了hashcode方法
public int hashCode() {int h = this.hash;if (h == 0 && this.value.length > 0) {char[] val = this.value;for(int i = 0; i < this.value.length; ++i) {h = 31 * h + val[i];}this.hash = h;}return h; }可以看出String的hashcode與String的內(nèi)容是有關(guān)系的,因此下面的代碼的hashcode是相等的
public不同方式創(chuàng)建字符串在JVM中的存在形式
雙引號
new String
兩個雙引號
兩個new String
拼接字符串底層是如何實現(xiàn)的
雙引號 + 雙引號
public雙引號 + new String
public1.1:字符串常量池在Java內(nèi)存區(qū)域的哪個位置?
- 在JDK6.0及之前版本,字符串常量池是放在Perm Gen區(qū)(也就是方法區(qū))中;
- 在JDK7.0版本,字符串常量池被移到了堆中了。至于為什么移到堆內(nèi),大概是由于方法區(qū)的內(nèi)存空間太小了。
- JDK8以后也還是放在了Heap空間中,并沒有已到元空間。
1.2:字符串常量池是什么?
- 在HotSpot VM里實現(xiàn)的string pool功能的是一個StringTable類,它是一個Hash表,默認值大小長度是1009;這個StringTable在每個HotSpot VM的實例只有一份,被所有的類共享。字符串常量由一個一個字符組成,放在了StringTable上。
- 在JDK6.0中,StringTable的長度是固定的,長度就是1009,因此如果放入String Pool中的String非常多,就會造成hash沖突,導(dǎo)致鏈表過長,當調(diào)用String#intern()時會需要到鏈表上一個一個找,從而導(dǎo)致性能大幅度下降;
- 在JDK7.0中,StringTable的長度可以通過參數(shù)指定:
1.3:字符串常量池里放的是什么?
- 在JDK6.0及之前版本中,String Pool里放的都是字符串常量;
- 在JDK7.0中,由于String#intern()發(fā)生了改變,因此String Pool中也可以存放放于堆內(nèi)的字符串對象的引用。
需要說明的是:字符串常量池中的字符串只存在一份!
如:
即執(zhí)行完第一行代碼后,常量池中已存在 “hello,world!”,那么 s2不會在常量池中申請新的空間,而是直接把已存在的字符串內(nèi)存地址返回給s2。
2. class常量池(Class Constant Pool):
2.1:class常量池簡介:
- 我們寫的每一個Java類被編譯后,就會形成一份class文件;class文件中除了包含類的版本、字段、方法、接口等描述信息外,還有一項信息就是常量池(constant pool table),用于存放編譯器生成的各種字面量(Literal)和符號引用(Symbolic References);
- 每個class文件都有一個class常量池。
2.2:什么是字面量和符號引用:
- 字面量包括:1.文本字符串 2.八種基本類型的值 3.被聲明為final的常量等;
- 符號引用包括:1.類和方法的全限定名 2.字段的名稱和描述符 3.方法的名稱和描述符。
3. 運行時常量池(Runtime Constant Pool):
- 運行時常量池存在于內(nèi)存中,也就是class常量池被加載到內(nèi)存之后的版本,不同之處是:它的字面量可以動態(tài)的添加(String#intern()),符號引用可以被解析為直接引用
- JVM在執(zhí)行某個類的時候,必須經(jīng)過加載、連接、初始化,而連接又包括驗證、準備、解析三個階段。而當類加載到內(nèi)存中后,jvm就會將class常量池中的內(nèi)容存放到運行時常量池中,由此可知,運行時常量池也是每個類都有一個。在解析階段,會把符號引用替換為直接引用,解析的過程會去查詢字符串常量池,也就是我們上面所說的StringTable,以保證運行時常量池所引用的字符串與字符串常量池中是一致的。
訪問標志:
(1)作用:
用于識別一些類或接口層次的訪問信息,主要包括:
- 這個Class是類還是接口;
- 是否定義為public類型;
- 是否定義abstract類型;
- 如果是類的話是否被聲明為final;
- 是否是注解
- 是否是枚舉
- 是否可用invokespecial字節(jié)碼指令
(2)組成:
如上所示,訪問標志中一共有16個標志位可以使用,當前只制定了8個。
類索引,父類索引,接口索引集合:
這三項數(shù)據(jù)主要用于確定這個類的繼承關(guān)系
(1)定義與作用:
類索引(this_class)和父類索引(super_class)都是一個 u2 類型的數(shù)據(jù),而接口索引集合(interfaces)是一組 u2類型的數(shù)據(jù)集合,Class文件中由這三項數(shù)據(jù)來確定類的繼承關(guān)系。
- 類索引:用來確定這個類的全限定名;
- 父類索引:用來確定這個類的父類的全限定名。由于Java語言不允許多重繼承,所以父類索引只有一個,除了 java.lang.Object 之外,所有的Java類都有父類。因此除了java.lang.Object 外,所有Java類的父類索引都不為0。
- 接口索引:用來描述這個類實現(xiàn)了哪些接口。這些被實現(xiàn)的接口將按 implement 語句(若此類本身是一個接口,則應(yīng)當是extend語句)后的接口順序從左到右排列在接口索引集合中。
(2)類索引查找:
類索引、父類索引、接口索引都按照順序排列在訪問標志之后,類索引和父類索引用兩個 u2 類型的索引值表示,它們各自指向一個類型為 CONSTANT_Class_info 的類描述符常量,通過CONSTANT_Class_info 類型常量中的索引值可以找到定義在 CONSTANT_Utf8_info類型常量中的全限定名字符串。下圖所示為類索引查找過程:
(3)接口計數(shù)器:
對于接口索引集合,入口的第一項——u2 類型的數(shù)據(jù)為接口計數(shù)器(interfaces_count),表示索引表的容量。如果該類沒有實現(xiàn)任何接口,則該計數(shù)器為0,后面接口的索引表不再占用任何字節(jié)。類索引、父類索引、接口索引的內(nèi)容如下圖:
查看上圖可知,從偏移地址 0x000000F1 開始的3個 u2 類型的值分別為 0x0001、0x0003、0x0000,代表著類索引為1、父類索引為3、接口索引集合為0。
字段表集合:
(1)字段表結(jié)構(gòu):
可以想一想在Java中描述一個字段可以包含什么信息?可以包括的信息有:
- 字段的作用域(public、private、protected修飾符)
- 是實例變量還是類變量(static修飾符)
- 可變性(final)
- 并發(fā)可見性(volatile修飾符,是否強制從主內(nèi)存讀寫)
- 是否被序列化(transient修飾符)
- 字段數(shù)據(jù)類型(基本類型、對象、數(shù)組)
- 字段名稱
在上述這些信息中,各個修飾符都是布爾值,要么有某個修飾符,要么沒有,很適合使用標志位來表示。而字段叫什么名字、被定義成什么數(shù)據(jù)類型都是無法固定的,只能引用常量池中的常量來描述。下表列出了字段表的最終格式:
- access_flags:是一個 u2的數(shù)據(jù)類型。
- name_index 索引值: 對常量池的引用,代表著字段的簡單名稱。
- descriptor_index 索引值: 對常量池的引用,代表字段和方法的描述符。
(2)定義與作用:
字段表(field_info)用于描述接口或者類中聲明的變量。字段(field)包括類級變量和實例級變量,但是不包括方法內(nèi)部聲明的局部變量。簡單來說,字段表集合存儲的修飾符+名稱
變量修飾符使用標志位表示,字段數(shù)據(jù)類型和字段名稱則引用常量池中常量表示
方法表集合:
(1)結(jié)構(gòu):
Class文件存儲格式中對方法表的描述與字段表是一致的,包括了:
- 訪問標志(access_flags)
- 名稱索引(name_index)
- 描述符索引(descriptor_index)
- 屬性表集合(attributes)
這些數(shù)據(jù)項目的含義也非常類似,僅在訪問標志和屬性表集合的可選項中有區(qū)別:
(2)方法表與字段表的區(qū)別:
區(qū)別在于訪問標志的不同:在方法中不能了用volatile和transient關(guān)鍵字修飾,所以方法表中無ACC_VOLATILE、ACC_TRANSIENT。與之相對的 synchronized、native、strictfp、abstract關(guān)鍵字可修飾方法,所以在方法表中就增加了相應(yīng)的訪問標志。
(3)標志位:
對于方法表,所有的標志位及取值如下表:
行文自此,你會發(fā)現(xiàn)方法的定義可以通過訪問標志、名稱索引、描述符索引表達清楚,但是方法里的代碼去哪里了?
方法里的Java代碼,經(jīng)過編譯器編譯成字節(jié)碼指令后,存放在方法屬性表集合一個名為“Code”屬性里,屬性表作為Class文件格式中最具擴展性的一種數(shù)據(jù)項目。
屬性表集合:
屬性表(attribute_info)在Class文件、字段表、方法表都可以攜帶自己的屬性集合,用于描述某些場景專有的信息。
與Class文件中的其它數(shù)據(jù)項木要求嚴格的順序、長度和內(nèi)存不同,屬性表集合限制稍寬松,不再要求各個屬性表具有嚴格順序,只要不與已有屬性名重復(fù),任何人實現(xiàn)的二便一起都可以想屬性表中寫入自己定義的屬性信息,而Java虛擬機會忽略掉它不認識的屬性。
下表對其中的一些屬性中關(guān)鍵常用部分進行講解:
對于每個屬性,它的名稱需要從常量池中引入一個 CONSTANT_Utf8_info類型的常量來表示,而屬性值的結(jié)構(gòu)則是完全自定義的,只需要通過一個u4 長度屬性去說明屬性值所占用的位置即可。一個符合規(guī)則的屬性表應(yīng)滿足如下結(jié)構(gòu):
總結(jié)
以上是生活随笔為你收集整理的修改 class_带你探索JVM的Class文件结构的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python补课费用_学习python阶
- 下一篇: python画二维数组散点图_Pytho