Class文件与字节码,机器码的关系,字节码整体结构 常量池表 字段表,方法表等详解

class字节码

class文件(二进制)和字节码(十六进制)的关系

  • class文件是经过编译器编译后的文件(如javac),一个class文件代表一个类或者接口;class文件主要存储的是字节码,字节码是访问jvm的重要指令,在后面的章节中会介绍字节码的相关信息。jvm规范定义了class文件结构格式,每种jvm实现必须满足规范定义,这样jvm实例才能加载class文件,运行字节码内容。但jvm的实现可以在jvm规范的约束下对具体实现做出修改和优化(如自定义属性信息,jvm会忽略不认识的属性表)。class文件的内容(.class文件本身是2进制)一般是16进制的数。 JVM加载的Class文件不一定来自磁盘,还可以来自网络数据,甚至在运行时直接编译代码字符串生成。 ---------节选《深入了解虚拟机》

  • 机器码我理解是CPU的指令集。字节码是由opcode(操作码)和操作数组成的。class文件并不叫字节码,class文件是由字节码组成的。字节码你可以理解为是JVM的指令。JVM是一个运行在CPU上的程序,JVM本身是由一条条机器码组成的,它的功能是读取一条条字节码,并在CPU上执行。

  • 也就是说是将二进制的文件加载JVM时,JVM将使用十六进制的方式读取class文件中的字节码(编译期就确定好了)

.java和.class的区别

  1. java文件是源文件,通过javac命令编译后生成.class文件;.class文件是字杰码结文件,即.java文件编译后的代码。
  2. class文件全名称为Java class文件,主要在平台无关性和网络移动性方面使Java更适合网络。
  3. 通俗来说
    java文件就是这样一个未经编译的源程序,一般是给程序员看的。class文件就是被编译器编译过的java文件,通常是给计算机看的。
  4. 也就是说.java是我们开发语言.class生成的16进制字节码是方便机器看的因为java源文件存在太多换行导致内存占用过多(机器不会识别空格)
  5. jvm规范和Java规范是不同的不能拿java的思维去套用jvm

示例

public class MyTest1 {
    
    

    private int a = 1;

    public int getA() {
    
    
        return a;
    }

    public void setA(int a) {
    
    
        this.a = a;
    }
}

执行

javac MyTest1.java
//打印公有信息
javap -verbose MyTest1.class
//将私有的方法信息也打印出来
javap -verbose -p MyTest1.class

得到对应字节码

  Last modified 2020-9-13; size 375 bytes
  MD5 checksum 8ba81fc79a80d987e02b14067a155703
  Compiled from "MyTest1.java"
public class com.example.demo.com.jvm.bytecode.MyTest1
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#17         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#18         //com/example/demo/com/jvm/bytecode/MyTest1.a:I
   #3 = Class              #19            // com/example/demo/com/jvm/bytecode/MyTest1
   #4 = Class              #20            // java/lang/Object
   #5 = Utf8               a
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               getA
  #12 = Utf8               ()I
  #13 = Utf8               setA
  #14 = Utf8               (I)V
  #15 = Utf8               SourceFile
  #16 = Utf8               MyTest1.java
  #17 = NameAndType        #7:#8          // "<init>":()V
  #18 = NameAndType        #5:#6          // a:I
  #19 = Utf8               com/example/demo/com/jvm/bytecode/MyTest1
  #20 = Utf8               java/lang/Object
{
    
    
  public com.example.demo.com.jvm.bytecode.MyTest1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>
":()V
         4: aload_0
         5: iconst_1
         6: putfield      #2                  // Field a:I
         9: return
      LineNumberTable:
        line 3: 0
        line 5: 4

  public int getA();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field a:I
         4: ireturn
      LineNumberTable:
        line 8: 0

  public void setA(int);
    descriptor: (I)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: iload_1
         2: putfield      #2                  // Field a:I
         5: return
      LineNumberTable:
        line 12: 0
        line 13: 5
}
SourceFile: "MyTest1.java"

字节码整体结构

class文件只有两种数据类型:无符号数和表
在这里插入图片描述
这里面表就类似常量池 是一种数据结构的复合形式

方法区包含如下内容

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

顺序从上到下

  1. 使用javap -verbose命令分析一个字节码文件时,将会分析该字节码文件的魔数,版本号,常量池,类信息,类的构造方法,类中的方法信息,类变量与成员变量信息。

  2. 魔数:所有的.class字节码文件的前四个字节都是魔数,魔数值为固定值:0xCAFFBABE【咖啡宝贝】(在解析时会查找固定魔数当不符合规范就不进行解析了)

    扫描二维码关注公众号,回复: 13293539 查看本文章
  3. 3.魔数之后的4个字节为版本信息,前两个字节表示minnor version(次版本号),后两个字节表示major version(主版本号)。这几的版本号为00 00 00 34,换算成十进制,表示次版本号为0,主版本号为52.所以改文件的版本号为:1.8.0 可以通过java -version命令来验证这一点。

  4. 常量池(constant pool):紧接着主版本号之后的就是常量池入口。一个java类中定义的很多信息都是由常量池来维护和描述的,可以将常量池看作是Class文件的资源仓库,比如说java类中定义的方法与变量描述信息,都是存储在常量池当中。常量池中主要存储两类常量:字面量和符号引用。字面量如文本字符串,java声明的final的常量值等,而符号引用如类和接口的全局限定名。字段的名称和描述符符,方法的名称和描述符等

  5. 常量池的总体结构:Java类所对应的常量池主要由常量池数量和常量池数组(常量表)这两部分共同构成。常量池数量紧跟在主版本号后面,占据2个字节;常量池数组则紧跟在常量池数量之后,常量池数组与一般的数组不同的是,常量池数组中不同的元素的类型,结构都是不同的,长度当然也就不同;但是每一种元素的第一个数据都是一个u1类型,该字节是个标志位,占据1个字节。JVM在解析常量池时,会根据这个u1类型来获取元素的具体类型。值得注意的是,常量池数组中元素的个数 = 常量池Count - 1(其中0暂时不使用),目的是满足某些常量池索引值的数据在特定请款项需要表达【不引用任何一个常量池】的含义;根本原因在于,索引为0也是一个常量(保留常量),只不过它位于常量表中,这个常量就对应null值;所以,常量池的索引从1而非0开始。

  6. 在JVM规范中,每个变量/字段都有描述信息,描述信息主要的作用是描述字段的数据类型。方法的参数列表(包括数量,类型和顺序)与返回值。根据描述符规则,基本数据类型和代表无返回值的void类型都用一个大写的字符来表示,对象类型则使用字符L加对象的全限定名称来表示。为了压缩字节码文件的体积,对于基本数据类型,JVM都只使用一个大写的字母来表示,如下所示:B - byte,C - char, D - double, F- float,I - int, J - lang, S - short,Z - boolean, V - void, L- 对象类型。如Ljava/lang/String; (分号不要忘记)字节码有两种类型字节类型和表类型

  7. 对于数组类型来说,每一个维度使用一个前置的[来表示,如int[]被记录为[I,String[][]被记录为[[Ljava/lang/String;

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

魔数(Magic Number)

0xCAFFBABE【咖啡宝贝】(在解析时会查找固定魔数当不符合规范就不进行解析了)

版本(Version)

在这里插入图片描述

常量池(Constant Pool)

分为两部分版本后两个字节是对应的常量count 第二部分是常量表(资源仓库)为count-1个常量

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

例如

字节码常量池对应的第一个参数 0A(固定都是第一个字节)对应的是10如图
在这里插入图片描述
此时后续4(2+2)个字节都是描述 对应的index 在这里插入图片描述
4 = 416^0 指向如图下 #4 = Class
20 = 1
16^1 +4*16 #20 = NameAndType
而 #4 指向 #23 java/lang/Object
#20 指向 #7#8 分别为 对应构造方法, ()V 对应构造方法无参无返回
组合就是java/lang/Object."":()V 对应的就是
第二个常量池信息如下图
在这里插入图片描述
对应的 com/cx/jvm/bytecode/Mytest1.a:I 对应的意思是: myTest1类 参数 int类型a属性

在这里插入图片描述
对应表格表示的是 为长度为1个字节的字符串 61 对应的 2进制为 97 97是a的ASCII码值 其他如下(我们可以在线转ASCII值)
如01 00 06 3c 69 6e 69 74 3e
在这里插入图片描述

总结

所以 jvm通过压缩将其class文件 字节码所有字节通过jvm规范通过ASCII都能转化成对应的java文件 可以看出方法构造方法和属性 父类信息都是存储在常量池的

Mytest1常量池对应的内容

在这里插入图片描述
jvm在加载的时候已经知道到需要解析的常量池对应的常量数据(都是通过他们常量的标识符来确定的) 如上 当解析到23个时就解析完成。

访问标志(Access Flags)

在这里插入图片描述
对应类的访问修饰符对应如下几种
在这里插入图片描述
ACC_PRIVATE 0X0002 表示private修饰符
当存在多种类修饰符 相当于对应类型值累加
在这里插入图片描述
对应的就是 20+1 = 21
在这里插入图片描述
在这里插入图片描述

当前Class名(This Class Name )

#
此时会去常量池(资源仓库)去找#3对应的数据 当前例子#3指向#19对应的就是com/example/demo/com/jvm/bytecode/MyTest1完全的限定名

 #19 = Utf8               com/example/demo/com/jvm/bytecode/MyTest1

父类名(super Class)

在这里插入图片描述
#4 = Class #20 // java/lang/Object 都是去常量池中找对应的数据

接口(Interface)

接口分为两部分 接口数量(super class后两个字节)以及接口表
当接口count为0 接口表就不会出现

变量(Feilds)

在这里插入图片描述

变量分为两部分变量数量(interface后两个字节 只统计类中静态变量和成员变量)以及变量表

field count

在这里插入图片描述

fields

变量表也有自己的结构的
在这里插入图片描述
access_flags 需要再修饰符中找对应的数据
在这里插入图片描述
在这里插入图片描述
此时0006 后面的两个字节是0000无特殊的attributes(如上图字段结构所示) count 就忽略不展示
就可以得出成员变量为 private int a;

方法(Methods)

方法也分为两种 count (两个字节)和方法表
 此离有三个方法 一个无参构造方法和对应get set方法

方法表(方法数组 )

结构
在这里插入图片描述
如第一个 方法
在这里插入图片描述

对应的16进制字符
在这里插入图片描述

attribute_info结构 如下
在这里插入图片描述
0001 public 00 07   0008 ()V 0001 表示 java/lang/Object."":()V 0009 Code

静态变量

当类中存在了静态成员变量此时JVM对应methods就会生成一个静态方法代码块
在这里插入图片描述

此时字节码会在methods中生成一个的方法用来描述 静态成员变量信息。
在这里插入图片描述

静态代码块

如果加上静态代码块


    static {
    
    
        System.out.println("liuli");
    }

重新编译此时也会在《clinit》里面执行相关操作
在这里插入图片描述

方法属性Code结构

每一个方法属性都有一个code属性用来描述方法内部字节信息也就是说通过这个code能知道方法内部执行代码块所有信息和对应的坐标
在这里插入图片描述
max_locals 最少有个局部变量this
在这里插入图片描述
在这里插入图片描述
code_length表示JVM虚拟机对应的需要执行的字节码的长度
如code对应的长度为10 字节如下表示JVM虚拟机对应的需要执行的字节码

助记符

2A B7 00 01 2A 04 B5 00 02 B1

在这里插入图片描述
该16进制字符Oracle 将其特定指定了对应的助记符
16进制对应助记符查询
在这里插入图片描述
如 B7 对应invokespecial 参数为b7后两个字节00 01 指向常量池在这里插入图片描述
在这里插入图片描述

对应的助记符为 改助记符对应的指令就是改方法里需要jvm执行的语句

//将索引为0的元素操作推送到栈顶 准备调用
0 aload_0
// B7 调用父类的方法 00 01 参数为两个字节 参数为常量池#1对应的数据
1 invokespecial #1 <java/lang/Object.<init>>
//将索引为0的元素操作推送到栈顶 准备调用
4 aload_0
//Push int常量 也就是给int的常量赋值为1
5 iconst_1
//为成员变量 参数a set 上面常量1这个值 (a =1 操作)
6 putfield #2 <com/example/demo/com/jvm/bytecode/MyTest1.a>
//从方法返回void 也就是不返回值 
9 return

常见助记符还有

从对象中获取参数 参数一共有2个字节 指向常量池对应数据 B5 00 02 得到如下参数
getfield #2 <com/example/demo/com/jvm/bytecode/MyTest1.a:I>
返回Int类型数据
4 ireturn

构造方法code执行流程 总结

由上述助记符我们可以看出构造方法code的执行流程是先调用父类的构造方法再去给该类的成员变量赋值(不包含静态变量)在当前的构造方法中赋值存在多个构造方法时会将变量赋值放到每个构造方法中。 静态变量在《Clinit》的code中方法完成
由该构造方法执行流程可以看出 MyTest1 变量a= 1赋值是在 对应的构造方法中完成赋值的不是一开始就有对应的值

方法附加属性

用来映射java代码对应行号当系统抛异常能准确定位坐标
在这里插入图片描述

行数表(LineNumberTable)

里面对应的LineNumberTable(属于code里的内容) 就是用来记录具体内容 如下
line_number_table_length 表示有几对字节每对字节为2+2 为4个字节
里面用记录方法字节码解析时坐标和成员变量赋值坐标两个属性

在这里插入图片描述

在这里插入图片描述
例如代码Line Number 33行
在这里插入图片描述
对应code属性(方法执行体)的Start pc 第0个助记符
在这里插入图片描述

局部变量表(LocalVariableTable)

在JVM编译的时候每个实例方法里面都至少有一个局部变量 this 生成表示对当前对象(内存地址)的隐式引用 位于方法的第一个参数处 所以我们每个方法都能访问当前对象
局部变量表一共分为两个部分
1.方法对应(参数)
2.方法代码块当中
在这里插入图片描述
在这里插入图片描述
局部变量表有两个局部参数

局堆栈映射表(StackMapTable)
/**
 * 栈帧(stack frame):(每个栈帧都是由特定的线程执行的 不存在并发情况)
 * <p>
 * 栈帧是一种用于帮助虚拟机执行方法调用与方法执行的数据结构
 * 栈帧本身是一种数据结构,封装了方法的局部变量表,动态链接信息,方法的返回地址以及操作数栈等信息。
 * (由此可以看出方法的局部变量是封装到栈当中并不是封装到堆当中)
 * 符号引用,直接引用
 * 有些符号引用,是在类加载阶段或者是第一次使用会转换为直接引用,这种转换叫做静态解析;另外一些符号引用则是在每次运行
 * 期转换为直接引用,这种转换叫做动态链接,这体现为java的多态性。
 *
 * <p>
 * 本质上栈帧进出栈是由方法完成决定的当系列链式调用时 当最后一个执行完就会弹栈 依次往下
 */
public class MyTest4 {
    
    
    /**
     * slot 存储局部变量的最小单位
     * 1.占据32个字节
     * 2.可复用 每个局部变量作用域不相同 所有生命周期也不相同 局部变量表不会区分这一点
     *  也就是说当 cd生命周期结束其占据的slot位置就有可能被fg占用
     *
     */
    public void test() {
    
    
        int a = 3;
        if (a < 4) {
    
    
            //当 cb 离开花括号生命周期就结束了 就会被垃圾回收器回收
            int c = 5;
            int b = 6;
        }
        int f = 23;
        int g = 24;
    }
}

在这里插入图片描述

异常表 (与方法code属性同级)

在这里插入图片描述

java字节码对异常的处理方式:

  1. 统一采用异常表的方式对异常进行处理
  2. 在jdk 1.4.2之前的版本中,并不是使用异常表的方式来对异常进行处理的,而是采用特定的指令方式.
  3. 当异常处理存在finally语句块时,现代化的JVM采取的处理方式是将finally语句块的字节码拼接到每一个catch块的后面, 换句话说,程序中存在多个catch块,就会在每一个catch块后面重复多个finally语句块的字节码
    */
    如test3
/**
 * 每个实例方法编译后生成的字节码当中 总会有个局部变量 this 表示对当前对象的引用
 * 它位于方法的第一个参数出  我们可以在方法中使用this参数(jvm会将当前对象的内存地址赋值给this)访问当前对象的 实例方法和属性
 *
 * 局部变量表一共分为两个部分
 *  * 1.方法对应(参数)
 *  * 2.方法代码块当中
 *
 */
public class MyTest3 {
    
    

    /**
     * 该方法有4个局部变量
     * this inputStream serverSocket catch中一个(可能不被使用)
     */
    public void test() {
    
    //args_size = 1

        try {
    
    

            InputStream inputStream = new FileInputStream("test.txt");

            ServerSocket serverSocket = new ServerSocket(9999);
            serverSocket.accept();
        } catch (FileNotFoundException e) {
    
    

        } catch (IOException e) {
    
    //三个catch只能进入一个

        } catch (Exception e) {
    
    

        } finally {
    
    
            System.out.println("finally");
        }
    }

}

在这里插入图片描述
对应test的code属性
在这里插入图片描述

test code属性对应的助记符(对应的16进制)

// 创建 java/io/FileInputStream 的对象
完成三件事
1.在堆中开辟内存
2.执行他的构造方法
3.将内存地址返回
 0 new #2 <java/io/FileInputStream>
 //压栈
 3 dup
 // //将常量池中test.txt推送到FileInputStream 的对象当中
 4 ldc #3 <test.txt>
  //调用父类的构造方法
 6 invokespecial #4 <java/io/FileInputStream.<init>>
  //将对象引用存储到astore_1 对应的局部变量数组当中
  //存储到引用数组中 doc中指出这个局部变量数组只能存引用类型
 9 astore_1
  //同上
10 new #5 <java/net/ServerSocket>
13 dup
//push short 9999推送到栈顶
14 sipush 9999
17 invokespecial #6 <java/net/ServerSocket.<init>>
20 astore_2
21 aload_2
//调用ServerSocket.accept
22 invokevirtual #7 <java/net/ServerSocket.accept>
//弹出站最顶层的数值 此时未抛异常直接执行finally语句
25 pop 
26 getstatic #8 <java/lang/System.out>
29 ldc #9 <finally>
31 invokevirtual #10 <java/io/PrintStream.println>
//跳转 return
34 goto 84 (+50)
//赋值 此时为抛出了异常 给对应的异常对象引用存到局部变量中
37 astore_1
38 getstatic #8 <java/lang/System.out>
41 ldc #9 <finally>
//finally执行语句
43 invokevirtual #10 <java/io/PrintStream.println>
46 goto 84 (+38)
49 astore_1
50 getstatic #8 <java/lang/System.out>
53 ldc #9 <finally>
55 invokevirtual #10 <java/io/PrintStream.println>
58 goto 84 (+26)
61 astore_1
62 getstatic #8 <java/lang/System.out>
65 ldc #9 <finally>
67 invokevirtual #10 <java/io/PrintStream.println>
70 goto 84 (+14)
73 astore_3
74 getstatic #8 <java/lang/System.out>
//finally代码块
77 ldc #9 <finally>
79 invokevirtual #10 <java/io/PrintStream.println>
82 aload_3
83 athrow
84 return

该Test方法异常表中 有4个异常类属性
在这里插入图片描述

例如#0 第一个
表示code对应的助记符0到26(26不包含) 方法块 字节码执行时如果出错将错误信息 赋值给对应37行的局部变量astore_1 (catch中不确定的局部变量e) 此时赋值catch_type指向常量池中FileNotFoundException 然后从助记符37行往下执行 此时执行的 38到43都是finally里的内容 接着goto跳转到84 return 程序完成
当catch= 0 表示处理所有异常,这点不像java Exception就是处理所有的 当方法块存在异常都会生成一个catch type=0 的全局异常属性

当我们throws 抛出异常会发现存在一个exceptions(声明方法上不存在code体当中) 去记录抛出的异常信息,
在这里插入图片描述

字节码属性(Attributes)

表示字节码的附带属性 类似方法变量的附加属性
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_42261668/article/details/108554317