class文件的简单解析

字节码简介

由于计算机只认识0和1,所以我们的代码要翻译成0和1组成的二进制文件才能被计算机执行,确切来说,是翻译成计算机能够识别的机器码才能被计算机所识别,然而不同操作系统的机器码并不是相同的,所以代码编译而成的二进制文件在其他的操作系统里并不难被识别,为了解决这个问题就出现了Java:Write Once,Run Anywhere。

他的解决方式是在二进制文件和操作系统直接抽象出一层java虚拟机,代码编译而成的二进制文件能被虚拟机识别,然后虚拟机再去跨平台实现,这样的二进制文件——class文件就能够跨平台运行了。

Class类文件结构

我们来看下面具体的Java类:

package art.bytecode;

public class Simple
        extends BaseSimple implements ISimple {
    public final static String sField = "testStaticFinalString";
    public final String sField2 = "testFinalString";
    private BaseSimple simple;
    private int anInt;

    public int add(int a, int b) {
        if (a < 0) {
            System.out.println(a);
            return 0;
        }
        for (int i = 0; i < a; i++) {
            b = b + i;
        }
        try {
            System.out.println(b);
        } catch (Exception e) {
            e.printStackTrace();
        }

        printAddResult1("");
        return b;
    }

    @TestAnnotation
    public static void printAddResult1(@TestAnnotation String s) {
        int i = new Simple().add(10, 20);
        System.out.println(i);
    }

}

将其进行javac编译得到class文件之后来对其的字节码文件具体的分析,其字节码文件用16进制编辑器打开后如下:

字节码

字节码大概分为如下几个部分:

  1. 魔数和Class文件的版本

  2. 常量池

  3. 访问标志

  4. 类索引、父类索引、接口索引集合

  5. 字段表集合

  6. 方法表集合

  7. 属性表集合

对于class文件的结构,每一个部分多长要么是预定义好的,要么是有专门的字节来记录长度,如此才能够正确的解析。

下面我们来具体介绍每一个部分。

魔数和Class文件的版本

先看前面的16位:CAFEBABE 00000039

其中前八位是class文件的魔数,固定为CAFEBABE,他的作用是确定这个class能否被虚拟机接受,在类加载器加载class文件到内存上的时候,头8个字节不为CAFEBABE的文件会被拒绝。

然后对于版本分为主版本和次版本,其中主版本为0039,也就是57,对应于jdk13,而次版本一般为0 (JDK12之后预览版为65535)。

常量池

常量池主要是存放一些固定的资源等,主要包含包名、类名、字段名,方法名等,常量池的常亮类型如下:

常量池

对于这个class文件,其常亮的个数是0046(70),代表class文件有69个常亮,下标为1-69(第0位空着)。然后对于每一种常亮都有自己的格式,下面举例来解析字节码常亮的内容。

第一个常亮类型为0A,也就是11,可知其存放的是方法引用表,而他的结构是会跟着两个u2类型,分别为0002,0003分别指向常量池下标为2和3的内容,第一个下标指向的是方法所属的类,第二个下标是指向名称和类型描述符。

然后对于第二个常亮,其的类型是07,也就是类信息,他存放的也是一个下标0004,指向的是这个类的名字。

对于第三个常亮,类型是0C,也就是名称和类型表,然后也存放两个下标0005、0006,分别指向的是其的名称已经类型。

对于第四个常量,类型是01,也就是字符串常量,其长度是0017,然后往后找0017长度的字节,为:6172742F 62797465 636F6465 2F426173 6553696D 706C65,其换算成字符串后是art/bytecode/BaseSimple,由于第二个常量是指向这个的,也就是说类名是art/bytecode/BaseSimple,第一个常量定义的方法也是属于这个类的。

对于第五个常量,类型是01,同上可得出存的是。代表这个方法是构造方法

对于第6个常量,存的是()V。代表这个方法的输入参数表为空,返回参数为void。

所以对于第一个常量,他存的是属于art/bytecode/BaseSimple的,方法名为,方法类型为()V的方法。

使用Javap命令查看的其完整的常量池如下:

#1 = Methodref #2.#3 // art/bytecode/BaseSimple.""😦)V

#2 = Class #4 // art/bytecode/BaseSimple

#3 = NameAndType #5:#6 // “”😦)V

#4 = Utf8 art/bytecode/BaseSimple

#5 = Utf8

#6 = Utf8 ()V

#7 = String #8 // testFinalString

#8 = Utf8 testFinalString

#9 = Fieldref #10.#11 // art/bytecode/Simple.sField2:Ljava/lang/String;

#10 = Class #12 // art/bytecode/Simple

#11 = NameAndType #13:#14 // sField2:Ljava/lang/String;

#12 = Utf8 art/bytecode/Simple

#13 = Utf8 sField2

#14 = Utf8 Ljava/lang/String;

#15 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream;

#16 = Class #18 // java/lang/System

#17 = NameAndType #19:#20 // out:Ljava/io/PrintStream;

#18 = Utf8 java/lang/System

#19 = Utf8 out

#20 = Utf8 Ljava/io/PrintStream;

#21 = Methodref #22.#23 // java/io/PrintStream.println:(I)V

#22 = Class #24 // java/io/PrintStream

#23 = NameAndType #25:#26 // println:(I)V

#24 = Utf8 java/io/PrintStream

#25 = Utf8 println

#26 = Utf8 (I)V

#27 = Class #28 // java/lang/Exception

#28 = Utf8 java/lang/Exception

#29 = Methodref #27.#30 // java/lang/Exception.printStackTrace:()V

#30 = NameAndType #31:#6 // printStackTrace:()V

#31 = Utf8 printStackTrace

#32 = String #33 //

#33 = Utf8

#34 = Methodref #10.#35 // art/bytecode/Simple.printAddResult1:(Ljava/lang/String;)V

#35 = NameAndType #36:#37 // printAddResult1:(Ljava/lang/String;)V

#36 = Utf8 printAddResult1

#37 = Utf8 (Ljava/lang/String;)V

#38 = Methodref #10.#3 // art/bytecode/Simple.""😦)V

#39 = Methodref #10.#40 // art/bytecode/Simple.add:(II)I

#40 = NameAndType #41:#42 // add:(II)I

#41 = Utf8 add

#42 = Utf8 (II)I

#43 = Class #44 // art/bytecode/ISimple

#44 = Utf8 art/bytecode/ISimple

#45 = Utf8 sField

#46 = Utf8 ConstantValue

#47 = String #48 // testStaticFinalString

#48 = Utf8 testStaticFinalString

#49 = Utf8 simple

#50 = Utf8 Lart/bytecode/BaseSimple;

#51 = Utf8 anInt

#52 = Utf8 I

#53 = Utf8 Code

#54 = Utf8 LineNumberTable

#55 = Utf8 LocalVariableTable

#56 = Utf8 this

#57 = Utf8 Lart/bytecode/Simple;

#58 = Utf8 i

#59 = Utf8 e

#60 = Utf8 Ljava/lang/Exception;

#61 = Utf8 a

#62 = Utf8 b

#63 = Utf8 StackMapTable

#64 = Utf8 s

#65 = Utf8 RuntimeInvisibleAnnotations

#66 = Utf8 Lart/bytecode/TestAnnotation;

#67 = Utf8 RuntimeInvisibleParameterAnnotations

#68 = Utf8 SourceFile

#69 = Utf8 Simple.java

访问标志

常量池之后是访问标准,他保存了这个类是否是接口,是否为public,是否是abstract等等,这个class文件的访问标志是0021,代表这个class是一个public类。

类索引、父类索引、接口索引集合

类索引和父类索引是u2类型的数,指向常量池的一个序号,而接口索引是一组u2类型的数据,代表接口的集合,对于这个class文件,类索引是000A,查常量池可知其是一个class类型的常亮,而class类名是art/bytecode/Simple,父类索引是0002,对应的是名为art/bytecode/BaseSimple的类,接口的个数是0001,然后接口索引是002B,代表名为art/bytecode/ISimple的接口。

字段表集合

字段表集合用于描述类或接口中的变量,对于这个class文件,共有0004个字段,对于第一个字段其的标志位是0019,对应于final static public,字段名位002D对应于常量表里的sField,类型为000E,对应于Ljava/lang/String,之后紧跟着是这个字段的属性表,他共有0001个属性,这个属性的类型是002E,也就是ConstantValue,他的作用是通知虚拟机在创建类的时候自动为这个变量赋值。

对于这个ConstantValue,他的属性长度是00000002,这个是固定值,应为他的属性值是指向常量池索引的下标,它指向的是002F,指向的是一个字符串变量,字符串的值为testStaticFinalString。

第二个属性的标志是0011,也就是final public,同上解析为sField2,且包含字符串为testFinalString的ConstantValue属性。

然后另外两个字段分别是:

private BaseSimple simple;
private int anInt;

他们的属性长度均为0000。

方法表集合

方法表集合和字段表集合很像,也是flag加下标加类型加属性表,对于这个class类,他有0003个方法,对于第一个方法,其的标志位是0001,代表其是public方法,方法名是0005,对应于,方法的类型是0006,代表类型是()V,也就是说这个方法是这个类的无参构造方法。然后他有0001条属性,属性类型对应于常量池的下标是0035,也就是Code属性。

对于这个Code属性,他需要的栈容量是0002,然后需要的本地变量长度是0001,代码长度是0000000B,代码为2AB70001 2A1207B5 0009B1,通过查询虚拟机字节码指令表可知,其对应的代码为:

0: aload_0
1: invokespecial #1         // Method art/bytecode/BaseSimple."<init>":()V
4: aload_0
5: ldc      #7         // String testFinalString
7: putfield   #9         // Field sField2:Ljava/lang/String;
10: return

紧接着是异常表,由于这个方法里没有try、catch语句,所以异常表长度为0000,然后是这个Code属性的属性表,属性表的个数是0002,第一个属性是0036,查常量池可知是LineNumberTable,这个属性是记录java代码行与Code字节码对应的,解析之后为:

LineNumberTable:

​    line 3: 0

​    line 6: 4

第二个属性是LocalVariableTable,它记录的是局部变量的名字,解析后如下:

      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   Lart/bytecode/Simple;

其中start和length结合起来决定了局部变量在字节码里的作用范围。

对于第二个方法add,它包含异常属性001E0025 0028001B,这代表在001E到0025之间如果遇到了001B(常量表为java/lang/Exception)异常会调到0028行继续执行。

在第三个方法里,有注解属性,其的字节流如下:

00410000 00060001 004200

代表方法有art.bytecode.TestAnnotation注解,还有如下属性

00 00430000 00070100 01004200

代表方法的第一个参数也有这个注解。

属性表集合

上面已经介绍了常见的几种属性,class文件最后还有属于class文件的属性表如下:

00000100 44000000 020045

这个代表这个class文件有一个属性,类型是SourceFile,值是Simple.java,也就是这个class文件对应的java文件名。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值