• JVM笔记(1)-Class文件结构——二进制编码解析


    了解Class文件结构对了解JVM运行机制大有裨益,同时,对于想要使用BCEL来动态改变Class字节码指令的工作也很有帮助(示例:JVM Class字节码之三-使用BCEL改变类属性)。

    1.Class文件总体描述

    1.1 Class文件概述

    1. Java字节码文件(.class)是Java源文件(.java)编译后产生的目标文件;
    2. 是一种8位(1字节)为基础的二进制流文件;
    3. 每个数据项按严格的结构紧密排列在一起,字节间没有空隙分隔符(一个字节一个字节都有其所在位置的定义)

    1.2 Class文件组成结构

    Class文件由一种类似C语言结构体的结构来存储数据,主要由无符号数组成。

    无符号数:基本数据类型,可以描述数字,索引符号,数量值,或UTF8编码组成的字符串值;大小有1个字节,2个字节,4个字节,8个字节四类,用(u1,u2,u4,u8)表示。

    表:由无符号数和其他表组成的复杂数据类型;习惯以“_info”结尾;描述由层次关系的复杂结构(方法,字段等),其中每个数据项位置都有严格定义。

    Class文件大致可分10个部分:魔数(MagicNumber)、Class文件版本(Version)、常量池(Constant_Pool)、访问标记(Access_flag)、本类(This_class)、父类(Super_class)、接口(Interfaces)、字段集合(Fields)、方法集合(Methods )、属性集合(Attributes)。

    具体顺序定义如图:

     2.详细结构

    2.1 示例Java源码及编译后的字节码

    Java:

    public class ByteCodeT {
        private String name;
        String sayWords;
        int m;
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public int getM() {
            return m;
        }
    
        public void setM(int m) {
            this.m = m;
        }
    
        public void hi() {
            String s12 = "hi ".concat(name);
            hello(s12);
        }
    
        public void hello(String s2) {
            sayWords = s2;
        }
    
        public static void main(String[] args) {
            ByteCodeT byteCodeT = new ByteCodeT();
            byteCodeT.setName("lisa");
            byteCodeT.hi();
        }
    }
    View Code

    字节码:

    警告: 二进制文件ByteCodeT包含com.lims.pracpro.jdkprac.ByteCodeT
    Classfile /E:/Code/flickeringproject/pracpro/target/classes/com/lims/pracpro/jdkprac/ByteCodeT.class
      Last modified 2020-9-17; size 1267 bytes
      MD5 checksum bb431381e128c261c3d6a9f4dcfb0c23
      Compiled from "ByteCodeT.java"
    public class com.lims.pracpro.jdkprac.ByteCodeT
      minor version: 0
      major version: 52
      flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
       #1 = Methodref          #13.#45        // java/lang/Object."<init>":()V
       #2 = Fieldref           #8.#46         // com/lims/pracpro/jdkprac/ByteCodeT.name:Ljava/lang/String;
       #3 = Fieldref           #8.#47         // com/lims/pracpro/jdkprac/ByteCodeT.m:I
       #4 = String             #48            // hi
       #5 = Methodref          #49.#50        // java/lang/String.concat:(Ljava/lang/String;)Ljava/lang/String;
       #6 = Methodref          #8.#51         // com/lims/pracpro/jdkprac/ByteCodeT.hello:(Ljava/lang/String;)V
       #7 = Fieldref           #8.#52         // com/lims/pracpro/jdkprac/ByteCodeT.sayWords:Ljava/lang/String;
       #8 = Class              #53            // com/lims/pracpro/jdkprac/ByteCodeT
       #9 = Methodref          #8.#45         // com/lims/pracpro/jdkprac/ByteCodeT."<init>":()V
      #10 = String             #54            // lisa
      #11 = Methodref          #8.#55         // com/lims/pracpro/jdkprac/ByteCodeT.setName:(Ljava/lang/String;)V
      #12 = Methodref          #8.#56         // com/lims/pracpro/jdkprac/ByteCodeT.hi:()V
      #13 = Class              #57            // java/lang/Object
      #14 = Utf8               name
      #15 = Utf8               Ljava/lang/String;
      #16 = Utf8               sayWords
      #17 = Utf8               m
      #18 = Utf8               I
      #19 = Utf8               <init>
      #20 = Utf8               ()V
      #21 = Utf8               Code
      #22 = Utf8               LineNumberTable
      #23 = Utf8               LocalVariableTable
      #24 = Utf8               this
      #25 = Utf8               Lcom/lims/pracpro/jdkprac/ByteCodeT;
      #26 = Utf8               getName
      #27 = Utf8               ()Ljava/lang/String;
      #28 = Utf8               setName
      #29 = Utf8               (Ljava/lang/String;)V
      #30 = Utf8               getM
      #31 = Utf8               ()I
      #32 = Utf8               setM
      #33 = Utf8               (I)V
      #34 = Utf8               hi
      #35 = Utf8               s12
      #36 = Utf8               hello
      #37 = Utf8               s2
      #38 = Utf8               main
      #39 = Utf8               ([Ljava/lang/String;)V
      #40 = Utf8               args
      #41 = Utf8               [Ljava/lang/String;
      #42 = Utf8               byteCodeT
      #43 = Utf8               SourceFile
      #44 = Utf8               ByteCodeT.java
      #45 = NameAndType        #19:#20        // "<init>":()V
      #46 = NameAndType        #14:#15        // name:Ljava/lang/String;
      #47 = NameAndType        #17:#18        // m:I
      #48 = Utf8               hi
      #49 = Class              #58            // java/lang/String
      #50 = NameAndType        #59:#60        // concat:(Ljava/lang/String;)Ljava/lang/String;
      #51 = NameAndType        #36:#29        // hello:(Ljava/lang/String;)V
      #52 = NameAndType        #16:#15        // sayWords:Ljava/lang/String;
      #53 = Utf8               com/lims/pracpro/jdkprac/ByteCodeT
      #54 = Utf8               lisa
      #55 = NameAndType        #28:#29        // setName:(Ljava/lang/String;)V
      #56 = NameAndType        #34:#20        // hi:()V
      #57 = Utf8               java/lang/Object
      #58 = Utf8               java/lang/String
      #59 = Utf8               concat
      #60 = Utf8               (Ljava/lang/String;)Ljava/lang/String;
    {
      java.lang.String sayWords;
        descriptor: Ljava/lang/String;
        flags:
    
      int m;
        descriptor: I
        flags:
    
      public com.lims.pracpro.jdkprac.ByteCodeT();
        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 9: 0
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       5     0  this   Lcom/lims/pracpro/jdkprac/ByteCodeT;
    
      public java.lang.String getName();
        descriptor: ()Ljava/lang/String;
        flags: ACC_PUBLIC
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: getfield      #2                  // Field name:Ljava/lang/String;
             4: areturn
          LineNumberTable:
            line 15: 0
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       5     0  this   Lcom/lims/pracpro/jdkprac/ByteCodeT;
    
      public void setName(java.lang.String);
        descriptor: (Ljava/lang/String;)V
        flags: ACC_PUBLIC
        Code:
          stack=2, locals=2, args_size=2
             0: aload_0
             1: aload_1
             2: putfield      #2                  // Field name:Ljava/lang/String;
             5: return
          LineNumberTable:
            line 19: 0
            line 20: 5
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       6     0  this   Lcom/lims/pracpro/jdkprac/ByteCodeT;
                0       6     1  name   Ljava/lang/String;
    
      public int getM();
        descriptor: ()I
        flags: ACC_PUBLIC
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: getfield      #3                  // Field m:I
             4: ireturn
          LineNumberTable:
            line 23: 0
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       5     0  this   Lcom/lims/pracpro/jdkprac/ByteCodeT;
    
      public void setM(int);
        descriptor: (I)V
        flags: ACC_PUBLIC
        Code:
          stack=2, locals=2, args_size=2
             0: aload_0
             1: iload_1
             2: putfield      #3                  // Field m:I
             5: return
          LineNumberTable:
            line 27: 0
            line 28: 5
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       6     0  this   Lcom/lims/pracpro/jdkprac/ByteCodeT;
                0       6     1     m   I
    
      public void hi();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=2, locals=2, args_size=1
             0: ldc           #4                  // String hi
             2: aload_0
             3: getfield      #2                  // Field name:Ljava/lang/String;
             6: invokevirtual #5                  // Method java/lang/String.concat:(Ljava/lang/String;)Ljava/lang/String;
             9: astore_1
            10: aload_0
            11: aload_1
            12: invokevirtual #6                  // Method hello:(Ljava/lang/String;)V
            15: return
          LineNumberTable:
            line 31: 0
            line 32: 10
            line 33: 15
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0      16     0  this   Lcom/lims/pracpro/jdkprac/ByteCodeT;
               10       6     1   s12   Ljava/lang/String;
    
      public void hello(java.lang.String);
        descriptor: (Ljava/lang/String;)V
        flags: ACC_PUBLIC
        Code:
          stack=2, locals=2, args_size=2
             0: aload_0
             1: aload_1
             2: putfield      #7                  // Field sayWords:Ljava/lang/String;
             5: return
          LineNumberTable:
            line 36: 0
            line 37: 5
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       6     0  this   Lcom/lims/pracpro/jdkprac/ByteCodeT;
                0       6     1    s2   Ljava/lang/String;
    
      public static void main(java.lang.String[]);
        descriptor: ([Ljava/lang/String;)V
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=2, locals=2, args_size=1
             0: new           #8                  // class com/lims/pracpro/jdkprac/ByteCodeT
             3: dup
             4: invokespecial #9                  // Method "<init>":()V
             7: astore_1
             8: aload_1
             9: ldc           #10                 // String lisa
            11: invokevirtual #11                 // Method setName:(Ljava/lang/String;)V
            14: aload_1
            15: invokevirtual #12                 // Method hi:()V
            18: return
          LineNumberTable:
            line 40: 0
            line 41: 8
            line 42: 14
            line 43: 18
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0      19     0  args   [Ljava/lang/String;
                8      11     1 byteCodeT   Lcom/lims/pracpro/jdkprac/ByteCodeT;
    }
    SourceFile: "ByteCodeT.java"
    View Code

    Notepad用十六进制查看:

     2.2 魔数

    每个Class文件头4个字节为魔数,用于确定此文件是否是虚拟机能接受的Class文件,魔数值为 0xCAFEBABE ,如果不是0xCAFEBABE 开头,则不是Java的class文件。

     2.3 Class文件版本号

    版本号用于JVM确定能否支持兼容,紧接魔数的4个字节是版本号:

    1. 次版本号(minor_version): 前2字节用于表示次版本号
    2. 主版本号(major_version): 后2字节用于表示主版本号。

      0X0034(对应十进制的50):JDK1.8      
      0X0033(对应十进制的50):JDK1.7      
      0X0032(对应十进制的50):JDK1.6      
      0X0031(对应十进制的49):JDK1.5  
      0X0030(对应十进制的48):JDK1.4  
      0X002F(对应十进制的47):JDK1.3  
      0X002E(对应十进制的46):JDK1.2 
    ps:0X表示16进制

    2.4 常量池

    紧接魔数和版本号后的是常量池,理解为Java文件的资源库。它是Java文件中与其他项目关联最多的数据项目,占用空间最大的数据项目之一,文件中第一个出现表类型的数据项目。

    主要存有符号引用字面量

    字面量:比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等。

    符号引用:属于编译原理方面的概念,包括了下面三类常量

    1. 类和接口的全限定名
    2. 字段的名称和描述符
    3. 方法的名称和描述符

    Java编译时并不像C、C++有链接这一步操作,而是虚拟机加载Class文件的时候“动态链接”。也就是说,不报存方法字段在内存的布局信息,这些字段和方法的符号引用不经过转换,则无法使用。虚拟机运行时,需要从常量池中获取对应的符号引用,在类创建或者运行时解析到具体的内存地址中

    由于常量池数量不固定,所以常量池入口处有u2类型的数据(constant_pool_count)表示常量池数量。常量池计数从1开始,索引0的位置留个JVM自己,如constant_pool一共14项,则constant_pool_count就为15。

    Class类中有很多部分都是对常量池的引用,如this_class,super_class, field_info, attribute_info等;字节码指令也会对常量池引用(当做指令操作数),常量池各项也会相互引用。

    常量池维护着经过编译“梳理”之后的相对固定的数据索引,它是站在整个JVM(进程)层面的共享池。

    constant_pool_count:2个字节,常量池数目。

     如图,常量池总共有61(0x3d)项,从1计数的常量有60项。

    constant_pool:常量池中表类型数据集合,每一项常量都是一个表。共有14种(JDK1.7前只有11种)结构各不相同的表结构数据。这14种表都有一个共同的特点,即均由一个u1类型的标志位开始,可以通过这个标志位来判断这个常量属于哪种常量类型:

     

     譬如utf-8类型的表结构数据:

    譬如fieldref类型的表结构数据:

     譬如class类型的表结构数据:

     譬如nameandtype类型的表结构数据:

     ps:什么是描述符?
    成员变量(包括静态成员变量和实例变量) 和方法都有各自的描述符。 
    对于字段而言,描述符用于描述字段的数据类型; 
    对于方法而言,描述符用于描述字段的数据类型、参数列表、返回值。
    在描述符中,基本数据类型用大写字母表示,对象类型用“L对象类型的全限定名”表示,数组用“[数组类型的全限定名”表示。 
    描述方法时,将参数根据上述规则放在()中,()右侧按照上述方法放置返回值。而且参数之间无需任何符号。

    实例说明:

    2.1字节码中,Methodref类型的常量(第1项)

      u1标记位10(0x0a),表示Methodref类型常量:

     u2为13(0x0d)号引用——class index(类索引):

    u2为45(0x2d)号索引引用——NameAndType index:

    2.1字节码中,utf-8类型的常量(第14项)

     u1标记位01,表示为utf-8类型常量:

     u2长度为4(0x04),后面4个字节为该常量; 

     4个字节长度的常量 —— “name”

    2.5 访问标记(2字节)

    访问标志(access_flags),这个标志主要用于识别一些类或接口层次的访问信息,主要包括:

    • 是否final
    • 是否public,否则是private
    • 是否是接口
    • 是否可用invokespecial字节码指令
    • 是否是abstact
    • 是否是注解
    • 是否是枚举

     access_flags一共有16个标志位可以使用,当前只定义了其中8个(JDK1.5增加后面3种),没有使用到标志位一律为0。

    JVM访问标记规范:

     2.6 类/父类/接口索引集合

    这三项数据主要用于确定这个类的继承关系。
    其中类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引(interface)集合是一组u2类型的数据。(多实现单继承)

    2.7 Fields字段表集合

    fields_count:字段表计数器(2字节)

    fields:字段表集合

    字段表结构:

    类型

    名称

    数量

    说明

    u2

    access_flags

    1

    修饰符标记位

    u2

    name_index

    1

    代表字段的简单名称,占2字节,是一个对常量池的引用 

    u2

    descriptor_index

    1

    代表字段的类型,占2个字节,是一个对常量池的引用

    u2

    attributes_count

    1

    属性计数器

    attribute_info

    attributes

    attributes_count

    属性表集合

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

    2.8 方法表集合

    methods_count:方法表计数器(2字节)

    methods:方法表集合

    方法表计数器,表示有8个方法:

    方法表结构:

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

     u2的01访问控制:

     u2的19(0x13)为名称索引——常量池19号(<init>):

    u2的20(0x14)号描述索引——常量池20号:

     u2位属性计数器——此方法有1个属性:

    属性的结构

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

     u2的21(0x15)为属性名索引——常量池21号(Code),表示Code属性

     u4表示Code属性长度为47(0x2f),后面47个字节为Code属性详细信息

    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属性详细信息,可依照上面的结构分析

    2.9 属性表集合

    起始2个字节为0x0001,说明有一个类属性。
    接下来2个字节为属性的名称,0x0010,指向常量池中第16个常量:SourceFile。
    接下来4个字节为0x00000002,说明属性体长度为2字节。
    最后2个字节为0x0011,指向常量池中第27个常量:TestClass.java,即这个Class文件的源码文件名为TestClass.java

    与Class文件中其它数据项对长度、顺序、格式的严格要求不同,属性表集合不要求其中包含的属性表具有严格的顺序,并且只要属性的名称不与已有的属性名称重复,任何人实现的编译器可以向属性表中写入自己定义的属性信息。虚拟机在运行时会忽略不能识别的属性,为了能正确解析Class文件,虚拟机规范中预定义了虚拟机实现必须能够识别的9项属性(预定义属性已经增加到21项):

    属性名称

    使用位置

    含义

    Code

    方法表

    Java代码编译成的字节码指令

    ConstantValue

    字段表

    final关键字定义的常量值

    Deprecated

    类文件、字段表、方法表

    被声明为deprecated的方法和字段

    Exceptions

    方法表

    方法抛出的异常

    InnerClasses

    类文件

    内部类列表

    LineNumberTale

    Code属性

    Java源码的行号与字节码指令的对应关系

    LocalVariableTable

    Code属性

    方法的局部变量描述(局部变量作用域)

    SourceFile

    类文件

    源文件名称

    Synthetic

    类文件、方法表、字段表

    标识方法或字段是由编译器自动生成的

     

     ps:在调试是可以通过SourceFile来关联相关的类。

    PS

    1,全限定名:将类全名中的“.”替换为“/”,为了保证多个连续的全限定名之间不产生混淆,在最后加上“;”表示全限定名结束。例如:"com.test.Test"类的全限定名为"com/test/Test;"

    2,简单名称:没有类型和参数修饰的方法或字段名称。例如:"public void add(int a,int b){...}"该方法的简单名称为"add","int a = 123;"该字段的简单名称为"a"

    3,描述符:描述字段的数据类型、方法的参数列表(包括数量、类型和顺序)和返回值。根据描述符规则,基本数据类型和代表无返回值的void类型都用一个大写字符表示,而对象类型则用字符L加对象全限定名表示

    标识字符

    含义

    B

    基本类型byte

    C

    基本类型char

    D

    基本类型double

    F

    基本类型float

    I

    基本类型int

    J

    基本类型long

    S

    基本类型short

    Z

    基本类型boolean

    V

    特殊类型void

    L

    对象类型,如:Ljava/lang/Object;

    对于数组类型,每一维将使用一个前置的“[”字符来描述,如:"int[]"将被记录为"[I","String[][]"将被记录为"[[Ljava/lang/String;"

    用描述符描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组"()"之内,如:方法"String getAll(int id,String name)"的描述符为"(I,Ljava/lang/String;)Ljava/lang/String;"

    4,Slot,虚拟机为局部变量分配内存所使用的最小单位,长度不超过32位的数据类型占用1个Slot,64位的数据类型(long和double)占用2个Slot

  • 相关阅读:
    ASP.NET 表单验证 Part.1(理解表单验证)
    Silverlight 简介 Part.3(设计 Siverlight 页面)
    ASP.NET 成员资格 Part.3(LoginStatus、LoginView、PasswordRecovery)
    ASP.NET 网站部署 Part.1(安装IIS、复制文件部署网站)
    ASP.NET Dynamic Data Part.1(创建动态数据应用程序)
    ASP.NET 安全模型 Part.2(SSL)
    ASP.NET MVC Part.2(扩展基本的 MVC 应用程序)
    ASP.NET 网站部署 Part.2(使用 Web 部署)
    开发高级 Web 部件
    创建 Web 部件(WebPart 类、简单的 Web 部件)
  • 原文地址:https://www.cnblogs.com/limaosheng/p/13683493.html
Copyright © 2020-2023  润新知