偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

JVM深入學(xué)習(xí):Java解析Class文件過(guò)程解析

開(kāi)發(fā) 后端
java之所以能夠?qū)崿F(xiàn)跨平臺(tái),便在于其編譯階段不是將代碼直接編譯為平臺(tái)相關(guān)的機(jī)器語(yǔ)言,而是先編譯成二進(jìn)制形式的java字節(jié)碼,放在Class文件之中,虛擬機(jī)再加載Class文件,解析出程序運(yùn)行所需的內(nèi)容。每個(gè)類都會(huì)被編譯成一個(gè)單獨(dú)的class文件,內(nèi)部類也會(huì)作為一個(gè)獨(dú)立的類,生成自己的class。

前言:

身為一個(gè)java程序員,怎么能不了解JVM呢,倘若想學(xué)習(xí)JVM,那就又必須要了解Class文件,Class之于虛擬機(jī),就如魚(yú)之于水,虛擬機(jī)因?yàn)镃lass而有了生命?!渡钊肜斫鈐ava虛擬機(jī)》中花了一整個(gè)章節(jié)來(lái)講解Class文件,可是看完后,一直都還是迷迷糊糊,似懂非懂。正好前段時(shí)間看見(jiàn)一本書(shū)很不錯(cuò):《自己動(dòng)手寫(xiě)Java虛擬機(jī)》,作者利用go語(yǔ)言實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的JVM,雖然沒(méi)有完整實(shí)現(xiàn)JVM的所有功能,但是對(duì)于一些對(duì)JVM稍感興趣的人來(lái)說(shuō),可讀性還是很高的。作者講解的很詳細(xì),每個(gè)過(guò)程都分為了一章,其中一部分就是講解如何解析Class文件。

這本書(shū)不太厚,很快就讀完了,讀完后,收獲頗豐。但是紙上得來(lái)終覺(jué)淺,絕知此事要躬行,我便嘗試著自己解析Class文件。go語(yǔ)言雖然很優(yōu)秀,但是終究不熟練,尤其是不太習(xí)慣其把類型放在變量之后的語(yǔ)法,還是老老實(shí)實(shí)用java吧。

話不多說(shuō),先貼出項(xiàng)目地址:https://github.com/HalfStackDeveloper/ClassReader

Class文件

什么是Class文件?

java之所以能夠?qū)崿F(xiàn)跨平臺(tái),便在于其編譯階段不是將代碼直接編譯為平臺(tái)相關(guān)的機(jī)器語(yǔ)言,而是先編譯成二進(jìn)制形式的java字節(jié)碼,放在Class文件之中,虛擬機(jī)再加載Class文件,解析出程序運(yùn)行所需的內(nèi)容。每個(gè)類都會(huì)被編譯成一個(gè)單獨(dú)的class文件,內(nèi)部類也會(huì)作為一個(gè)獨(dú)立的類,生成自己的class。

基本結(jié)構(gòu)

隨便找到一個(gè)class文件,用Sublime Text打開(kāi)是這樣的:

屏幕快照 2017-02-06 上午8.44.42.png

是不是一臉懵逼,不過(guò)java虛擬機(jī)規(guī)范中給出了class文件的基本格式,只要按照這個(gè)格式去解析就可以了:

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,這是什么類型呢?其實(shí)很簡(jiǎn)單,就是分別表示1個(gè)字節(jié),2個(gè)字節(jié)和4個(gè)字節(jié)。

開(kāi)頭四個(gè)字節(jié)為:Magic,是用來(lái)唯一標(biāo)識(shí)文件格式的,一般被稱作magic number(魔數(shù)),這樣虛擬機(jī)才能識(shí)別出所加載的文件是否是class格式,class文件的魔數(shù)為cafebabe。不只是class文件,基本上大部分文件都有魔數(shù),用來(lái)標(biāo)識(shí)自己的格式。

接下來(lái)的部分主要是class文件的一些信息,如常量池、類訪問(wèn)標(biāo)志、父類、接口信息、字段、方法等,具體的信息可參考《Java虛擬機(jī)規(guī)范》。

解析

字段類型

上面說(shuō)到ClassFile中的字段類型有u1、u2、u4,分別表示1個(gè)字節(jié),2個(gè)字節(jié)和4個(gè)字節(jié)的無(wú)符號(hào)整數(shù)。java中short、int、long分別為2、4、8個(gè)字節(jié)的有符號(hào)整數(shù),去掉符號(hào)位,剛好可以用來(lái)表示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文件了,首先是讀取魔數(shù)之類的基本信息,這部分很簡(jiǎn)單:

FileInputStream inputStream = new FileInputStream(file);
ClassFile classFile = new ClassFile();
classFile.magic = U4.read(inputStream);
classFile.minorVersion = U2.read(inputStream);
classFile.majorVersion = U2.read(inputStream);

這部分只是熱熱身,接下來(lái)的大頭在于常量池。解析常量池之前,我們先來(lái)解釋一下常量池是什么。

常量池,顧名思義,存放常量的資源池,這里的常量指的是字面量和符號(hào)引用。字面量指的是一些字符串資源,而符號(hào)引用分為三類:類符號(hào)引用、方法符號(hào)引用和字段符號(hào)引用。通過(guò)將資源放在常量池中,其他項(xiàng)就可以直接定義成常量池中的索引了,避免了空間的浪費(fèi),不只是class文件,Android可執(zhí)行文件dex也是同樣如此,將字符串資源等放在DexData中,其他項(xiàng)通過(guò)索引定位資源。java虛擬機(jī)規(guī)范給出了常量池中每一項(xiàng)的格式:

cp_info {
    u1 tag;
    u1 info[]; 
}

上面的這個(gè)格式只是一個(gè)通用格式,常量池中真正包含的數(shù)據(jù)有14種格式,每種格式的tag值不同,具體如下所示:

[[182707]]

由于格式太多,文章中只挑選一部分講解:

這里首先讀取常量池的大小,初始化常量池:

//解析常量池
int constant_pool_count = U2.read(inputStream);
ConstantPool constantPool = new ConstantPool(constant_pool_count);
constantPool.read(inputStream);

接下來(lái)再逐個(gè)讀取每項(xiàng)內(nèi)容,并存儲(chǔ)到數(shù)組cpInfo中,這里需要注意的是,cpInfo[]下標(biāo)從1開(kāi)始,0無(wú)效,且真正的常量池大小為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++;
            }
        }
    }
}

我們先來(lái)看看CONSTANT_Utf8格式,這一項(xiàng)里面存放的是MUTF-8編碼的字符串:

CONSTANT_Utf8_info { 
    u1 tag;
    u2 length;
    u1 bytes[length]; 
}

那么如何讀取這一項(xiàng)呢?

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()
    }
}

很簡(jiǎn)單,首先讀取這一項(xiàng)的字節(jié)數(shù)組長(zhǎng)度,接著調(diào)用readUtf8(),將字節(jié)數(shù)組轉(zhuǎn)化為String字符串。

再來(lái)看看CONSTANT_Class這一項(xiàng),這一項(xiàng)存儲(chǔ)的是類或者接口的符號(hào)引用:

CONSTANT_Class_info {
    u1 tag;
    u2 name_index;
}

注意這里的name_index并不是直接的字符串,而是指向常量池中cpInfo數(shù)組的name_index項(xiàng),且cpInfo[name_index]一定是CONSTANT_Utf8格式。

public class ConstantClass extends ConstantInfo {
    public int nameIndex;

    @Override
    public void read(InputStream inputStream) {
        nameIndex = U2.read(inputStream);
    }
}

常量池解析完畢后,就可以供后面的數(shù)據(jù)使用了,比方說(shuō)ClassFile中的this_class指向的就是常量池中格式為CONSTANT_Class的某一項(xiàng),那么我們就可以讀取出類名:

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");

字節(jié)碼指令

解析常量池之后還需要接著解析一些類信息,如父類、接口類、字段等,但是相信大家***奇的還是java指令的存儲(chǔ),大家都知道,我們平時(shí)寫(xiě)的java代碼會(huì)被編譯成java字節(jié)碼,那么這些字節(jié)碼到底存儲(chǔ)在哪呢?別急,講解指令之前,我們先來(lái)了解下ClassFile中的method_info,其格式如下:

method_info {
    u2 access_flags;
    u2 name_index;
    u2 descriptor_index;
    u2 attributes_count;
    attribute_info attributes[attributes_count];
}

method_info里主要是一些方法信息:如訪問(wèn)標(biāo)志、方法名索引、方法描述符索引及屬性數(shù)組。這里要強(qiáng)調(diào)的是屬性數(shù)組,因?yàn)樽止?jié)碼指令就存儲(chǔ)在這個(gè)屬性數(shù)組里。屬性有很多種,比如說(shuō)異常表就是一個(gè)屬性,而存儲(chǔ)字節(jié)碼指令的屬性為CODE屬性,看這名字也知道是用來(lái)存儲(chǔ)代碼的了。屬性的通用格式為:

attribute_info {
    u2 attribute_name_index;
    u4 attribute_length;
    u1 info[attribute_length];
}

根據(jù)attribute_name_index可以從常量池中拿到屬性名,再根據(jù)屬性名就可以判斷屬性種類了。

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數(shù)組里存儲(chǔ)就是字節(jié)碼指令,那么如何解析呢?每條指令在code[]中都是一個(gè)字節(jié),我們平時(shí)javap命令反編譯看到的指令其實(shí)是助記符,只是方便閱讀字節(jié)碼使用的,jvm有一張字節(jié)碼與助記符的對(duì)照表,根據(jù)對(duì)照表,就可以將指令翻譯為可讀的助記符了。這里我也是在網(wǎng)上隨便找了一個(gè)對(duì)照表,保存到本地txt文件中,并在使用時(shí)解析成HashMap。代碼很簡(jiǎn)單,就不貼了,可以參考我代碼中InstructionTable.java。

接下來(lái)我們就可以解析字節(jié)碼了:

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");
        }
    }
}

運(yùn)行

整個(gè)項(xiàng)目終于寫(xiě)完了,接下來(lái)就來(lái)看看效果如何,隨便找一個(gè)class文件解析運(yùn)行:

屏幕快照 2017-02-06 下午3.55.10.png

哈哈,是不是很贊!

責(zé)任編輯:張燕妮 來(lái)源: HalfStackDeveloper
相關(guān)推薦

2010-09-27 08:38:49

JVM堆JVM棧

2012-03-05 11:09:01

JavaClass

2010-09-25 12:54:24

JVM內(nèi)存

2020-10-19 09:09:46

Class文件加載過(guò)程

2010-09-17 14:17:05

JVM內(nèi)存設(shè)置

2014-06-23 10:31:09

Android啟動(dòng)過(guò)程

2010-07-16 16:09:05

Perl文件

2010-09-25 14:38:18

Java內(nèi)存分配

2015-09-29 08:57:46

javascript對(duì)象

2009-11-17 14:13:34

PHP配置

2016-10-31 19:41:29

Java垃圾回收

2010-03-30 13:37:14

Nginx負(fù)載均衡

2023-05-05 18:33:15

2010-08-31 13:06:45

CSS

2020-03-23 14:15:51

RadonDB安裝數(shù)據(jù)庫(kù)

2010-09-28 09:22:34

DOM模型Html

2015-09-29 09:27:04

JavaScript對(duì)象

2010-06-29 15:29:22

UML建模流程

2011-12-01 14:56:30

Java字節(jié)碼

2009-03-16 15:47:16

Java線程多線程
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)