JVM深入學習-Java解析Class文件過程的範例程式碼
前言:
身為一個java程式設計師,怎麼能不了解JVM呢,倘若想學習JVM,那就必須了解Class文件,Class之於虛擬機,就如魚之於水,虛擬機因為Class有了生命。 《深入理解java虛擬機》中花了一整章來講解Class文件,可是看完後,一直都還是迷迷糊糊,似懂非懂。正好前段時間看見一本書很不錯:《自己動手手寫Java虛擬機》,作者利用go語言實現了一個簡單的JVM,雖然沒有完整實現JVM的所有功能,但是對於一些對JVM稍感興趣的人來說,可讀性還是很高的。作者講解的很詳細,每個過程都分成了一章,其中一部分就是講解如何解析Class檔案。
這本書不太厚,很快就讀完了,讀完後,收穫豐富。但紙上得來終覺淺,絕知此事要躬行,我便嘗試著自己解析Class文件。 go語言雖然很優秀,但是終究不熟練,尤其是不太習慣其把型別放在變數之後的語法,還是老實實用java吧。
Class檔案
什麼是Class檔案?
java之所以能夠實現跨平台,便在於其編譯階段不是將程式碼直接編譯為平台相關的機器語言,而是先編譯成二進位形式的java字節碼,放在Class檔之中,虛擬機器再載入Class文件,解析出程式運作所需的內容。每個類別都會被編譯成一個單獨的class文件,內部類別也會作為一個獨立的類,產生自己的class。
基本結構
隨便找到一個class文件,用Sublime Text打開是這樣的:
是不是一臉懵逼,不過java虛擬機器規格中給了class檔案的基本格式,只要按照這個格式去解析就可以了:
ClassFile { u4 magic; u2 minor_version; u2 major_version; u2 constant_pool_count; cp_info constant_pool[constant_pool_count-1]; u2 access_flags; u2 this_class; u2 super_class; u2 interfaces_count; u2 interfaces[interfaces_count]; u2 fields_count; field_info fields[fields_count]; u2 methods_count; method_info methods[methods_count]; u2 attributes_count; attribute_info attributes[attributes_count]; }
ClassFile中的欄位類型有u1、u2、u4,這是什麼型別呢?其實很簡單,就是分別表示1個位元組,2個位元組和4個位元組。
開頭四個位元組為:Magic,是用來唯一識別檔案格式的,一般被稱為magic number(魔數),這樣虛擬機器才能辨識出所載入的文件是否為class格式,class檔案的魔數為cafebabe。不只是class文件,基本上大部分文件都有魔數,用來標識自己的格式。
接下來的部分主要是class檔案的一些訊息,如常數池、類別存取標誌、父類別、介面資訊、欄位、方法等,具體的資訊可參考《Java虛擬機器規格》。
解析
欄位類型
上面說到ClassFile的欄位型別有u1、u2、u4,分別表示1個位元組,2個位元組和4個位元組的無符號整數。 java中short、int、long分別為2、4、8個位元組的有符號整數,去掉符號位,剛好可以用來表示u1、u2、u4。
public class U1 { public static short read(InputStream inputStream) { byte[] bytes = new byte[1]; try { inputStream.read(bytes); } catch (IOException e) { e.printStackTrace(); } short value = (short) (bytes[0] & 0xFF); return value; } } public class U2 { public static int read(InputStream inputStream) { byte[] bytes = new byte[2]; try { inputStream.read(bytes); } catch (IOException e) { e.printStackTrace(); } int num = 0; for (int i= 0; i < bytes.length; i++) { num <<= 8; num |= (bytes[i] & 0xff); } return num; } } public class U4 { public static long read(InputStream inputStream) { byte[] bytes = new byte[4]; try { inputStream.read(bytes); } catch (IOException e) { e.printStackTrace(); } long num = 0; for (int i= 0; i < bytes.length; i++) { num <<= 8; num |= (bytes[i] & 0xff); } return num; } }
常數池
定義好字段類型後,我們就可以讀取class檔案了,首先是讀取魔數之類的基本信息,這部分很簡單:
FileInputStream inputStream = new FileInputStream(file); ClassFile classFile = new ClassFile(); classFile.magic = U4.read(inputStream); classFile.minorVersion = U2.read(inputStream); classFile.majorVersion = U2.read(inputStream);
這部分只是熱身,接下來的大頭在於常量池。在解析常數池之前,我們先來解釋一下常數池是什麼。
常數池,顧名思義,存放常數的資源池,這裡的常數指的是字面量和符號引用。字面量指的是一些字串資源,而符號引用分為三類:類別符號引用、方法符號引用和欄位符號引用。透過將資源放在常數池中,其他項目就可以直接定義成常數池中的索引了,避免了空間的浪費,不只是class文件,Android可執行文件dex也是同樣如此,將字串資源等放在DexData中,其他項目透過索引定位資源。 java虛擬機器規格給了常數池中每一項的格式:
cp_info { u1 tag; u1 info[]; }
上面的這個格式只是一個通用格式,常數池中真正包含的資料有14種格式,每種格式的tag值不同,具體如下所示:
由於格式太多,文章中只挑選一部分講解:
這裡先讀取常數池的大小,初始化常數池:
//解析常量池 int constant_pool_count = U2.read(inputStream); ConstantPool constantPool = new ConstantPool(constant_pool_count); constantPool.read(inputStream);
接下來再逐一讀取每項內容,並儲存到陣列cpInfo中,這裡需要注意的是,cpInfo[]下標從1開始,0無效,且真正的常數池大小為constant_pool_count-1。
public class ConstantPool { public int constant_pool_count; public ConstantInfo[] cpInfo; public ConstantPool(int count) { constant_pool_count = count; cpInfo = new ConstantInfo[constant_pool_count]; } public void read(InputStream inputStream) { for (int i = 1; i < constant_pool_count; i++) { short tag = U1.read(inputStream); ConstantInfo constantInfo = ConstantInfo.getConstantInfo(tag); constantInfo.read(inputStream); cpInfo[i] = constantInfo; if (tag == ConstantInfo.CONSTANT_Double || tag == ConstantInfo.CONSTANT_Long) { i++; } } } }
我們先來看看CONSTANT_Utf8格式,這一項裡面存放的是MUTF-8編碼的字串:
CONSTANT_Utf8_info { u1 tag; u2 length; u1 bytes[length]; }
那麼要如何讀取這項呢?
public class ConstantUtf8 extends ConstantInfo { public String value; @Override public void read(InputStream inputStream) { int length = U2.read(inputStream); byte[] bytes = new byte[length]; try { inputStream.read(bytes); } catch (IOException e) { e.printStackTrace(); } try { value = readUtf8(bytes); } catch (UTFDataFormatException e) { e.printStackTrace(); } } private String readUtf8(byte[] bytearr) throws UTFDataFormatException { //copy from java.io.DataInputStream.readUTF() } }
很简单,首先读取这一项的字节数组长度,接着调用readUtf8(),将字节数组转化为String字符串。
再来看看CONSTANT_Class这一项,这一项存储的是类或者接口的符号引用:
CONSTANT_Class_info { u1 tag; u2 name_index; }
注意这里的name_index并不是直接的字符串,而是指向常量池中cpInfo数组的name_index项,且cpInfo[name_index]一定是CONSTANT_Utf8格式。
public class ConstantClass extends ConstantInfo { public int nameIndex; @Override public void read(InputStream inputStream) { nameIndex = U2.read(inputStream); } }
常量池解析完毕后,就可以供后面的数据使用了,比方说ClassFile中的this_class指向的就是常量池中格式为CONSTANT_Class的某一项,那么我们就可以读取出类名:
int classIndex = U2.read(inputStream); ConstantClass clazz = (ConstantClass) constantPool.cpInfo[classIndex]; ConstantUtf8 className = (ConstantUtf8) constantPool.cpInfo[clazz.nameIndex]; classFile.className = className.value; System.out.print("classname:" + classFile.className + "\n");
字节码指令
解析常量池之后还需要接着解析一些类信息,如父类、接口类、字段等,但是相信大家最好奇的还是java指令的存储,大家都知道,我们平时写的java代码会被编译成java字节码,那么这些字节码到底存储在哪呢?别急,讲解指令之前,我们先来了解下ClassFile中的method_info,其格式如下:
method_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count]; }
method_info里主要是一些方法信息:如访问标志、方法名索引、方法描述符索引及属性数组。这里要强调的是属性数组,因为字节码指令就存储在这个属性数组里。属性有很多种,比如说异常表就是一个属性,而存储字节码指令的属性为CODE属性,看这名字也知道是用来存储代码的了。属性的通用格式为:
attribute_info { u2 attribute_name_index; u4 attribute_length; u1 info[attribute_length]; }
根据attribute_name_index可以从常量池中拿到属性名,再根据属性名就可以判断属性种类了。
Code属性的具体格式为:
Code_attribute { u2 attribute_name_index; u4 attribute_length; u2 max_stack; u2 max_locals; u4 code_length; u1 code[code_length]; u2 exception_table_length; { u2 start_pc; u2 end_pc; u2 handler_pc; u2 catch_type; } exception_table[exception_table_length]; u2 attributes_count; attribute_info attributes[attributes_count]; }
其中code数组里存储就是字节码指令,那么如何解析呢?每条指令在code[]中都是一个字节,我们平时javap命令反编译看到的指令其实是助记符,只是方便阅读字节码使用的,jvm有一张字节码与助记符的对照表,根据对照表,就可以将指令翻译为可读的助记符了。这里我也是在网上随便找了一个对照表,保存到本地txt文件中,并在使用时解析成HashMap。代码很简单,就不贴了,可以参考我代码中InstructionTable.java。
接下来我们就可以解析字节码了:
for (int j = 0; j < methodInfo.attributesCount; j++) { if (methodInfo.attributes[j] instanceof CodeAttribute) { CodeAttribute codeAttribute = (CodeAttribute) methodInfo.attributes[j]; for (int m = 0; m < codeAttribute.codeLength; m++) { short code = codeAttribute.code[m]; System.out.print(InstructionTable.getInstruction(code) + "\n"); } } }
运行
整个项目终于写完了,接下来就来看看效果如何,随便找一个class文件解析运行:
哈哈,是不是很赞!
以上是JVM深入學習-Java解析Class文件過程的範例程式碼的詳細內容。更多資訊請關注PHP中文網其他相關文章!

熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

Video Face Swap
使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

熱工具

記事本++7.3.1
好用且免費的程式碼編輯器

SublimeText3漢化版
中文版,非常好用

禪工作室 13.0.1
強大的PHP整合開發環境

Dreamweaver CS6
視覺化網頁開發工具

SublimeText3 Mac版
神級程式碼編輯軟體(SublimeText3)

Java 8引入了Stream API,提供了一種強大且表達力豐富的處理數據集合的方式。然而,使用Stream時,一個常見問題是:如何從forEach操作中中斷或返回? 傳統循環允許提前中斷或返回,但Stream的forEach方法並不直接支持這種方式。本文將解釋原因,並探討在Stream處理系統中實現提前終止的替代方法。 延伸閱讀: Java Stream API改進 理解Stream forEach forEach方法是一個終端操作,它對Stream中的每個元素執行一個操作。它的設計意圖是處

PHP是一種廣泛應用於服務器端的腳本語言,特別適合web開發。 1.PHP可以嵌入HTML,處理HTTP請求和響應,支持多種數據庫。 2.PHP用於生成動態網頁內容,處理表單數據,訪問數據庫等,具有強大的社區支持和開源資源。 3.PHP是解釋型語言,執行過程包括詞法分析、語法分析、編譯和執行。 4.PHP可以與MySQL結合用於用戶註冊系統等高級應用。 5.調試PHP時,可使用error_reporting()和var_dump()等函數。 6.優化PHP代碼可通過緩存機制、優化數據庫查詢和使用內置函數。 7

PHP和Python各有優勢,選擇應基於項目需求。 1.PHP適合web開發,語法簡單,執行效率高。 2.Python適用於數據科學和機器學習,語法簡潔,庫豐富。

PHP適合web開發,特別是在快速開發和處理動態內容方面表現出色,但不擅長數據科學和企業級應用。與Python相比,PHP在web開發中更具優勢,但在數據科學領域不如Python;與Java相比,PHP在企業級應用中表現較差,但在web開發中更靈活;與JavaScript相比,PHP在後端開發中更簡潔,但在前端開發中不如JavaScript。

PHP和Python各有優勢,適合不同場景。 1.PHP適用於web開發,提供內置web服務器和豐富函數庫。 2.Python適合數據科學和機器學習,語法簡潔且有強大標準庫。選擇時應根據項目需求決定。

膠囊是一種三維幾何圖形,由一個圓柱體和兩端各一個半球體組成。膠囊的體積可以通過將圓柱體的體積和兩端半球體的體積相加來計算。本教程將討論如何使用不同的方法在Java中計算給定膠囊的體積。 膠囊體積公式 膠囊體積的公式如下: 膠囊體積 = 圓柱體體積 兩個半球體體積 其中, r: 半球體的半徑。 h: 圓柱體的高度(不包括半球體)。 例子 1 輸入 半徑 = 5 單位 高度 = 10 單位 輸出 體積 = 1570.8 立方單位 解釋 使用公式計算體積: 體積 = π × r2 × h (4

PHPhassignificantlyimpactedwebdevelopmentandextendsbeyondit.1)ItpowersmajorplatformslikeWordPressandexcelsindatabaseinteractions.2)PHP'sadaptabilityallowsittoscaleforlargeapplicationsusingframeworkslikeLaravel.3)Beyondweb,PHPisusedincommand-linescrip

PHP成為許多網站首選技術棧的原因包括其易用性、強大社區支持和廣泛應用。 1)易於學習和使用,適合初學者。 2)擁有龐大的開發者社區,資源豐富。 3)廣泛應用於WordPress、Drupal等平台。 4)與Web服務器緊密集成,簡化開發部署。
