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