手动解析java字节码文件

前言

我们平时编写的java源文件,也就是.java文件在经过编译过后会成为jvm能识别的.class文件,也就是编译成了字节码文件,jvm的执行引擎目前有两种执行的方式,字节码解释执行和模板解释执行,我们的通常的字节码文件要通过jvm(c++)解释成计算机能识别的硬编码,也就是汇编;而模板解释器是直接不通过C++代码进行解释执行,而是通过模板解释器直接解释成计算机能识别的硬编码,而这些被模板解释器解释成的硬编码也是热点代码,热点代码具有热点代码缓存区,这个也是jvm调优的一部分,但是jvm有默认的热点代码缓存大小,如果不是太懂最好不要调整这个值的大小;关于执行引擎的解释器后面的笔记再行记录,这篇笔记主要记录下我们通过手动的方式是如何解析字节码文件的,进而了解字节码文件是什么样的形式进行存储。

字节码文件原貌

字节码文件即.java文件通过javac命令生成的.class文件 我们在编译器比如Eclipse或者Idea中编写了java类,但是编译工具会自动给我们编译成.class文件,也就是字节码文件;
看看字节码文件的原貌
比如我这边有个java文件,如下:

public class ByteCode {
    
    


    private static int count = 1;

    public static void main(String[] args) {
    
    
        System.out.println(count);
    }
}

然后我们在windows下面通过javac或者开发工具给我们编译出来的字节码文件如下:

// class version 52.0 (52)
// access flags 0x21
public class com/bml/jvm/ByteCode {
    
    

  // compiled from: ByteCode.java

  // access flags 0xA
  private static I count

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 3 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this Lcom/bml/jvm/ByteCode; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x9
  public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 9 L0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    GETSTATIC com/bml/jvm/ByteCode.count : I
    INVOKEVIRTUAL java/io/PrintStream.println (I)V
   L1
    LINENUMBER 10 L1
    RETURN
   L2
    LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
    MAXSTACK = 2
    MAXLOCALS = 1

  // access flags 0x8
  static <clinit>()V
   L0
    LINENUMBER 6 L0
    ICONST_1
    PUTSTATIC com/bml/jvm/ByteCode.count : I
    RETURN
    MAXSTACK = 1
    MAXLOCALS = 0
}

注意看上面的前两行字节码文件
// class version 52.0 (52)
// access flags 0x21

意思就是我们的编写的这个java类的版本号是52,52是jdk的那个版本呢? 我们来看一张图:
在这里插入图片描述

所以我们的class的大版本major是52,是jdk1.8的版本
然后access flags 0x21指的就是我们的class的访问权限,我们的class ByteCode肯定是public,因为java类文件已经在上面了,所以access_flags 0x21就是标示我们的ByteCode的访问权限时public,那么0x21是什么意思呢,我们再来看下面一张图:
在这里插入图片描述
所以0x21代表的就是0x0001?

看什么的private static int I,I是什么意思呢?I表示就是我们的int类型,具体看下图
在这里插入图片描述
byte ->B
char->C
double->D

在上面类似这种Ljava/lang.String就是代本的String类型的参数,如果是数组的就是[Ljava/lang/String;
比如我写一个方法的描述符:
([[Ljava/lang/String;, I, Ljava/bml/Test;)Ljava/lang/String;

String XXX(String[][] strArrs, int a, Test test)

我们再来看下字节码的16进制文件
在这里插入图片描述

手动解析字节码文件

我们先来看下字节码文件包含了那些部分在里面,我们如何解析:
在这里插入图片描述
看结构从上到下,左边的U2 U4代表就是本结构多少字节,我们都知道1字节代本2位;
所以字节码文件的前四个字节u4=cafebabe(小端模式),而计算机一般传输过程中会转出大端模式
如果是大端模式就是bebafdca

一般jvm在解析class字节码文件的时候,验证的时候就首先判断魔数是不是以cafebabe开头,如果不是就是一个非法的class文件

在上图中“!”表示不确定有,长度大小不确定,好比实现的接口数量,我们的类可能没有接口,如果解析出来没有那么这个域就不会存在
我们来解析一次:
魔数(u4):cafebabe
minor version(u2):0000(次版本号为0)
major version(主版本号u2):0034(十进制为52)
常量池大小(u2):0024(十进制36)
我们来看这个常量池大小,在idea中用jclasslib就可以看到
在这里插入图片描述

常量池解析

常量池列表:
在这里插入图片描述
是从01开始的到35,真实的常量池个数 = 字节码文件中的常量池个数 - 1
解析常量池就要借助一个表格来,常量池规则表:
在这里插入图片描述
我们这边解析几个常量池,36个太多了
规则:
1.看上表中每一项的tag都是u1,那么就代表着我们的后面的解析每次取1字节
2.取到1字节过后,看值是多少,然后找到所对应的常量
3.找到所对应的常量过后依次把常量池解析出来
从上表中的0024过后的第一字节是:0a
第一个常量:
tag(u1):0a(0a十进制是10,在常量池结构表中对应的是Constant_Methodref_info)
index(u2):0006(是一个索引,6代表指向的是第6个,一般在字节码中未#6)
name_and_type(u2):0017(十进制23,23代表指向的是第23个,一般在字节码中表示#23
我们来验证一下:
看下截图:
在这里插入图片描述
我们解析的时候可以根据idea生成的可视化和我们自己手动解析的对应起来就知道有没有解析对
在idea中如果我们点击#6或者#23会直接跳到对应的类型上去,#6 #23代表的就是引用。

第二个常量:
tag(u1):09(9代表的就是Constant_Fieldref_info)
class_info(u2):0018(十进制24,代表指向#24)
name_and_type_info(u2):0019(十进制25,代表指向#25)
我们再来验证一下:
在这里插入图片描述

完全正确,当然了,这边手动解析只是为了了解class的文件结构,真实的情况下都是写程序取解析的,比如字节码技术asm

access flag:0021(public,前面已经说过)如图:
在这里插入图片描述
this_class(u2):0005(十进制是5,也就是#5)看idea的截图如下:
在这里插入图片描述
super_class:(u2):0006(看上图一目了然)
interfacce_count(u2):0000(表示接口为0)
interfaces[]:因为interface_count为0,所以这个域就不会存在了
fields_count(u2):00 01(表示有一个字段属性)

解析字段属性

接下来是解析我们的具体字段属性,字段属性的解析规则如下:
在这里插入图片描述
fields_1(第一个属性):
在这里插入图片描述

access_flags(u2):00 0a(十进制为10,是private static)
看下图:
name_index(u2):00 07
descriptor_index(u2):00 08
attributes_count(u2):0000(代表属性个数ConstantValue为0)
attributes:如果属性的数量=0,这块区域在字节码文件中就不会出现。

方法解析

方法解析前先看我们的方法信息:
在这里插入图片描述
从上图中可以看出我们的类ByteCode中有三个方法,分别是、main、
init:字节码为我们的java类生成的构造方法
client:当我们的类中有静态属性或者静态代码块的时候会生成clinit方法
因为我们的ByteCode类中有静态属性存在,所以生成的方法有三个,默认构造方法、man方法和静态初始化方法
接下来是方法个数
methods_count(u2):00 03(代表我们有3个方法)
这里解析方法,我演示解析我们的方法
在这里插入图片描述
上图是方法解析的规则
第一个方法init
access_flags:00 01(public)
name_index:00 09(引用到常量池中第9个常量)
desc_index: 00 0A(引用到常量池中第10个常量)
attr_count: 00 01(有一个属性)
attrs(如果属性=0,字节码文件中就没有这个区域):
属性内容解析规则:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
attribute_name_index:00 0B(引用到常量池中第11个常量)
attribute_length: 00 00 00 2F(十进制47,表示长度为47)
max_stack:00 01
max_locals:00 01
code_length:00 00 05
codecode_length:2a b7 00 01 b1(根据长度取字节数)
exception_length:00 00 (无申明异常)
attribute_count:00 02(代表我们的方法有两个属性,也就是局部变量表和LineNumberTable)
attribute[attribute_count]:
Code属性的属性
在这里插入图片描述
attr_name_index: 00 0C(引用常量池12) LineNumberTable
att_length:00 00 00 06(长度为6)
line_number_length: 00 01(有一个LineNumber表)
[
start_pc:00 00
line_number: 00 03(具体的代码行数为3)
]
根据ByteCode的局部LineNumberTable在idea中所示:
在这里插入图片描述
LineNumberTable也就是jvm为什么会很准确定义到我们的代码错误行数等原理

          attr_name_index:00 0D(引用常量池13)LocalVariableTable 方法的局部变量表
          attr_len:00 00 00 0C(变量表长度为12)
          table_length:00 01(有一个局部变量表)
               [
                  start_pc: 00 00
                  length: 00 05(长度为5)
                  name_index: 00 0E(引用的是常量表的14)
                  des_index: 00  0F(引用的是常量表的15)
                  index: 00 00
               ]

根据ByteCode的局部变量表在idea中所示:
在这里插入图片描述
对应到上面的手动解析一目了然

还有最后一个属性的解析,非常简单,我这边就不解析,规则如下:
在这里插入图片描述
最后如果我们在电脑上如何查看自己的class的文件字节码,如果查看二进制的,直接用ue把class文件打开即可;
如果是为了查看字节码还可以在class的所在目录执行:
javap -verbose ByteCode.class

Last modified 2020-8-7; size 602 bytes
  MD5 checksum 367be1125b0a815faf617b48e78c0d20
  Compiled from "ByteCode.java"
public class com.bml.jvm.ByteCode
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#23         // java/lang/Object."<init>":()V
   #2 = Fieldref           #24.#25        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Fieldref           #5.#26         // com/bml/jvm/ByteCode.count:I
   #4 = Methodref          #27.#28        // java/io/PrintStream.println:(I)V
   #5 = Class              #29            // com/bml/jvm/ByteCode
   #6 = Class              #30            // java/lang/Object
   #7 = Utf8               count
   #8 = Utf8               I
   #9 = Utf8               <init>
  #10 = Utf8               ()V
  #11 = Utf8               Code
  #12 = Utf8               LineNumberTable
  #13 = Utf8               LocalVariableTable
  #14 = Utf8               this
  #15 = Utf8               Lcom/bml/jvm/ByteCode;
  #16 = Utf8               main
  #17 = Utf8               ([Ljava/lang/String;)V
  #18 = Utf8               args
  #19 = Utf8               [Ljava/lang/String;
  #20 = Utf8               <clinit>
  #21 = Utf8               SourceFile
  #22 = Utf8               ByteCode.java
  #23 = NameAndType        #9:#10         // "<init>":()V
  #24 = Class              #31            // java/lang/System
  #25 = NameAndType        #32:#33        // out:Ljava/io/PrintStream;
  #26 = NameAndType        #7:#8          // count:I
  #27 = Class              #34            // java/io/PrintStream
  #28 = NameAndType        #35:#36        // println:(I)V
  #29 = Utf8               com/bml/jvm/ByteCode
  #30 = Utf8               java/lang/Object
  #31 = Utf8               java/lang/System
  #32 = Utf8               out
  #33 = Utf8               Ljava/io/PrintStream;
  #34 = Utf8               java/io/PrintStream
  #35 = Utf8               println
  #36 = Utf8               (I)V
{
    
    
  public com.bml.jvm.ByteCode();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/bml/jvm/ByteCode;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: getstatic     #3                  // Field count:I
         6: invokevirtual #4                  // Method java/io/PrintStream.println:(I)V
         9: return
      LineNumberTable:
        line 9: 0
        line 10: 9
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  args   [Ljava/lang/String;

  static {
    
    };
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: iconst_1
         1: putstatic     #3                  // Field count:I
         4: return
      LineNumberTable:
        line 6: 0
}


结束语

上面就是差不多一个类的手动解析的一个过程,当然了还有很多没有写上,比如有继承的该如何解析,接口的怎么解析,其实都是一样的道理,手动解析这个实在太麻烦,意在明白这个解析的过程
如果真的要解析class字节码文件,那么我们肯定根据规则去编写自己的应用程序取解析,去完成我们的需求,这边只是将字节码的结构和底层原理分析出来,根据这个思想和思路去解析我们的字节码文件;其实你不懂字节码文件也不影响你工作,不影响你写代码,但是知识的储备不就是为了丰富你的人生,精彩你的生活吗?不要永远守着那点知识“啃老”,要学习进步,不仅仅是为了能够生存,也为了能够让我们的人生更精彩。
其实了解我们的JVM对大家也是非常有用的,为什么呢?因为现在运行在jvm上的语言不仅仅是java了,很多语言都是自己实现了编译,让后在jvm上运行,所以jvm是相对永恒的,java可能是暂时的,所以我们了解下底层的一些知识点没有什么损失,也就浪费点业余时间而已。
java
groovy
kotlin
Scala的源文件 ,经过Scala的编译器,编译生成了.class文件
只要符合jvm规范的class文件都可以在jvm上运行,不仅仅是java
在这里插入图片描述
上图就是不同语言在jvm上运行,所以学好jvm真的很有必要

猜你喜欢

转载自blog.csdn.net/scjava/article/details/108277662
今日推荐