深入理解JVM - 類文件結構

阿東lazy 2021-08-15 22:34:29 阅读数:500

本文一共[544]字,预计阅读时长:1分钟~
深入 入理 理解 jvm 文件

深入理解JVM - 類文件結構

前言

​ JVM的類文件結構基本都會要記憶的內容,我相信你也記不住,當然我也是記不住的,所以這裏只會列出大致的類文件結構,我們需要大致了解類文件結構是怎麼一回事就行了,具體到那個比特存哪個內容,內容確實太多了,感興趣可以直接去讀書中對應的第6章 類文件結構這一個章節的內容。

​ 類文件結構個人認為需要注意的點就是這幾點:大致的類文件結構,部分Jdk的特性如何通過改動class文件結構實現,比如泛型,自動拆裝箱,動態代理,lambada語法等。

概述:

​ 其實主要內容就是介紹CLASS的文件結構。

  1. 了解JVM的類文件基本結構。
  2. 了解常量池的內容
  3. 了解重點內容屬性錶集合

什麼是Class類文件?

.class文件是由.java通過Javac的命令編譯而來的,也是JVM實現跨平臺的關鍵,同時Class類文件實際上的內容是包含字節碼指令的二進制文件,而字節碼指令簡單理解是jvm對於匯編指令的進一步封裝,甚至有一些書籍拿字節碼的指令來講部分操作系統的底層邏輯實現,注意不要被洗腦了,JVM的字節碼指令只能被JVM識別,放到別的平臺就是一堆亂碼,如果帶歪了建議看CSAPP這本書洗回來。

​ 既然是由外部的.java文件翻譯並且加載到虛擬機上,那麼class文件的結構毫無疑問需要嚴格的規定,防止代碼破壞jvm正常執行,事實上《JVM虛擬機規範》規定了Class文件的整個結構,對於每一比特都有嚴格的要求,現代的JDK雖然對於這個規範有了不少的改動,但是整體來看最基礎的結構還是按照最初始發布的那一套執行,所以基本不需要擔心過時的問題。

任意一個class文件對應一個類或者接口定義,class文件結構也可以看做是一種規範,只要其他語言也能遵守class文件的規範,意味著完全可以通過編寫其他語言的程序轉化為class文件最終翻譯到JVM中。

class文件結構

​ Class文件是一組以8個字節為基礎單比特的二進制流,各個數據項目嚴格按照順序緊凑地排列在文件之中,中間沒有添加任何分隔符,這使得整個Class文件中存儲的內容幾乎全部是程序運行的必要數 據,沒有空隙存在。當遇到需要占用8個字節以上空間的數據項時,則會按照高比特在前 [2] 的方式分割 成若幹個8個字節進行存儲。

​ Class文件格式采用一種類似於C語言結構體的偽結構來存儲數據,這種偽結構中只有兩種數據類型:“無符號數”和“錶”

無符號數:代錶了基本的數據類型,比如u1、u2、u3等,數字代錶了字節,比如1個字節,2個字節,3個字節,無符號數可以描述數字,索引引用或者經過UTF-8的編碼為字符串存儲

*

比如\u304這種字符串

*

:錶是由多個無符號數或者其他錶作為數據項構成的複合數據類型,習慣性以“_info”結尾。

​ 最後可以整個class文件結構看出如下的一張錶。

class文件結構詳解

​ 了解了class文件的大致結構,下面來聊聊class文件的具體組成了。

魔數0xCAFEBABE

​ 在class文件的結構中,每個Class文件的頭4個字節被稱為魔數(Magic Number),它唯一的作用是標記這個文件是一個class文件,除此之外沒有其他作用,至於為什麼叫做咖啡寶貝是因為它象征著著名咖啡品牌Peet’s Coffee並且深受歡迎的Baristas咖啡。

次主版本號

​ 為什麼叫做次主版本號?是因為接著魔數的後面的比特數第5、6個字節被稱為次版本號,第7、8個字節是主版本號。Java的版本號是從45開始的,JDK 1.1之後的每個JDK大版本發布主版本號向上加1(JDK 1.0~1.1使用了45.0~45.3的版本號)。高版本支持向下兼容,但是低版本不支持向上兼容,哪怕代碼一模一樣。《Java虛擬機規範》

*

​ 例如,JDK 1.1能支持版本號為45.0~45.65535的Class文件,無法執行版本號為46.0以上的Class文件,而JDK 1.2則能支持45.0~46.65535的Class文件。目前最新的JDK版本為13,可生成的Class文件主版本號最大值為57.0。

*

​ 下面是書中給出的具體案例:

下面是一個JDK版本號對應參考圖:

*

從JDK 9開始,Javac編譯器不再支持使用-source參數編譯版本號小於1.5的源碼

原因:JDK9的模塊化,以及擴展類加載器的改動。

*

常量池

*

​ 注意:常量池的入口需要放置一個U2(16進制的F)類型的數據,用於記錄常量池的容器計量值

*

​ 主次版本號之後就是常量池的內容了,可以直接看做是class文件的資源倉庫,同時是class文件結構關聯最多的數據部分,也是最大的數據項之一。光靠這一篇文章肯定是無法講完的,同樣即使講完了也記不住,所以這一部分我們只需要掌握存放的內容即可。

存放內容

​ 常量池中主要存放兩大類常量:字面量(Literal)符號引用(Symbolic References)

​ 字面量:比如文本字符串,final常量

​ 符號引用則存放如下內容:

  • 模塊導出或者開放包
  • 類與接口全限定名稱
  • 字段與描述符
  • 方法句柄和類型
  • 動態調用點和動態常量

常量錶

​ 常量池的每一個常量都是一個錶,最初只有11種結構,後來擴展出4種和動態語言相關的常量,為了模塊化:加入CONSTANT_Module_info和CONSTANT_Package_info兩個常量,最終就是11+4+2 = 17種常量,所以到JDK16版本為止有17種常量。同意,記是記不住的,我們也不需要記住:

*

上面的錶重點關注:CONSTANT_Dynamic_infoCONSTANT_InvokeDynamic_info

*

​ 下面我們挑重點看一下這些常量對於JDK的影響。

Constant_class_info 類型

CONSTANT_Class_info類型,此類型的常量代錶一個類或者接口的符號引用,主要結構為一個U1類型的tag(標志比特)和u2類型的name_index(名稱引用)。

​ tag的作用是標志比特區分常量的類型,而name_index錶示常量池的索引值。

*

U1代錶一個字節:1111,U2代錶兩個字節:11111111,也就是65535。

後續以此類推,不再贅述。

*

​ 在講下一個類型之前,我們先提一個問題:**JAVA方法最大長度是多少,為什麼?**我們都知道方法起名是有上限的,但是究竟的上限是多少,這裏我們根據class的文件結構來進行解讀:

CONSTANT_Utf8_info類型

CONSTANT_Utf8_info類型常量,此常量代錶了這個類(或者接口)的全限定名

CONSTANT_Utf8_info類型常量指向name_index,0x00002常量池第二項常量,標記為0x01

。接下來就是重點了,CONSTANT_Utf8_info型常量的最大長度也就是Java中方法、字段名的最大長度。而這裏的最大長度就是length的最大值,既u2類型能錶達的最大值65535。所以Java程序中如果定義了超過64KB英文字符的變量或方法名,即使規則和全部字符都是合法的,也會無法編譯

​ 書中提到的常量也是這兩種,如果我們想要查看字節碼,可以使用javap指令。

*

JDK 7時增加了前三種:CONSTANT_MethodHandle_info、CONSTANT_MethodType_info和 CONSTANT_InvokeDynamic_info。DK 11中又增加了第四種常量CONSTANT_Dynamic_info

*

訪問標志

​ 在常量池結束之後,用兩個字節錶示訪問標志與接口的訪問信息。裏面的具體內容包括:這個Class是類還是接口;是否定義為public類型;是否定義為abstract類型;如果是類的話,是否被聲明為final

​ access_flags(訪問標志)中一共有16個標志比特可以使用,當前只定義了其中9個,如果要計算它的值,可以使用0x0001|0x0020=0x0021

類索引、父類索引與接口索引集合

​ 簡單來說,Class文件中由這三項數據來確定該類型的繼承關系。類索引用於確定這個類的全限定名,而父類索引中記錄了當前類的父類的全限定名,需要注意的是因為JAVA的頂級父類永遠是java.lang.Object,除了java.lang.Object外,所有Java類的父類索引都不為0,而接口索引集合就用來描述這個類實現了哪些接口。

字段錶集合

​ 字段錶(field_info)用於描述接口或者類中聲明的變量,注意字段包含了java當中的類變量或者實例級常量。這裏可能有一個疑問,為什麼方法中的局部變量不屬於字段?**。

​ 首先,我們需要注意的是,方法中的變量都是屬於棧幀範圍內的,所以方法中的變量生命範圍逃不開一個棧幀的高度(或者說容量)。類中的字段可以定義是否公開,是否靜態,引用是否不可修改等,但是局部變量做不到,所以字段錶中無法存放局部變量,也沒有必要存放。

​ 同樣的,上述這些信息中,各個修飾符都是布爾值,要麼有某個修飾符,要麼沒有,很適合使用標志比特來錶示。

​ 接下來書裏面的內容就是講述字段錶集合的細節了,由於我們不需要去研究GC,所以這裏感興趣可以直接去看書裏面的內容。

方法錶集合

​ 和字段錶內容大致相同,Class文件存儲格式中對方法的描述與對字段的描述采用了幾乎完全一致的方式,方法錶集合包括了:訪問標志(access_flags)、名稱索引(name_index)、描述符索引(descriptor_index)、屬性錶集合(attributes)。同時在標志部分增加了和方法相關的標志。

​ 既然是方法錶集合,那代碼到哪去了?答案是在方法屬性裏面有一個code屬性,這屬性就是存放方法中代碼的地方,也是對於程序員來說最有用的地方。這裏需要注意需要注意的是如果父類方法在子類中沒有被重寫(Override),方法錶集合中就不會出現來自父類的方法信息

​ 同樣,需要注意的是雖然java語法不支持返回值重載但是在class特征簽名返回值不同也可以同時存在。

*

需要注意的是volatile關鍵字和transient關鍵字在方法錶集合當中不一樣,

*

屬性錶集合(核心)

​ 屬性錶的內容主要是搭配前面所講的字段和方法進行搭配的,最初的預定義屬性最初只有9種,最新的《Java虛擬機規範》的Java SE 12版本中,現在已經有了29種。

屬性結構的內容如下:
- u2 attrcibute_name_index
- U4 attribute_length
- U1 info

Code屬性

​ Java程序方法體裏面的代碼經過Javac編譯器處理之後,最終變為字節碼指令存儲在Code屬性內。但是需要注意並不是所有的方法都要有這個屬性,因為方法的內容是可以為空的。

​ Code屬性是Class文件中最重要的一個屬性,如果把一個Java程序中的信息分為代碼(Code,方法 體裏面的Java代碼)和元數據(Metadata,包括類、字段、方法定義及其他信息)兩部分,那麼在整 個Class文件裏,Code屬性用於描述代碼,所有的其他數據項目都用於描述元數據

​ 後續的內容是根據的一個javap生產的字節碼指令來進行相關的解讀,這些內容也是不是關鍵的內容,為了减輕記憶負擔,本文也不做過多介紹。

异常錶

​ Code數量裏面還包含一個异常錶,异常錶也是JAVA代碼的一部分,這部分內容雖然可以通過GOTO這種跳轉指令實現,但是在JVM規範中是强制規範JAVA語言使用异常錶而不是GOTO指令實現JAVA的异常以及Finally的處理機制。

*

為什麼不能用GOTO?這就要問問C語言這個老先生了,雖然很多語言都保留了GOTO的語法,但是無一例外沒有人推薦使用。因為它不僅容易出BUG,並且寫出來的源代碼十分難以理解。

*

line_number_table 屬性

  • 用於描述行號和字節碼行號對應關系

  • 用於debug使用

  • -line_number_info 中兩個u2類型的數據項

    • start_pc: 字節碼行號
    • line_number java源代碼行號

由於篇幅問題,其他的屬性這裏就不過多介紹了,思維導圖摘錄了大致內容,在遇到疑問的時候翻一翻記憶會比較深,這裏也不做過多敘述。

總結

​ class文件結構靠著死記硬背是記不住的,需要根據JVM的特性來進行理解,比如動態語言是如何實現的,再比如DEBUG是如何實現的等等,用這些內容幫助理解記憶。

​ 從本文也可以看到,其實重點都在屬性錶集合這一部分。所以如果需要重點理解JAVA的特性,可以從這個屬性錶開始。

寫在最後

​ 不管看幾次,我也記不住,哎......

版权声明:本文为[阿東lazy]所创,转载请带上原文链接,感谢。 https://gsmany.com/2021/08/20210815223358057h.html