关于jvm:JVM学习笔记七Class文件结构

40次阅读

共计 5852 个字符,预计需要花费 15 分钟才能阅读完成。

1 起源

  • 起源:《Java 虚拟机 JVM 故障诊断与性能优化》——葛一鸣
  • 章节:第九章

本文是第九章的一些笔记整顿。

2 概述

本文次要介绍了 Class 文件的次要组成,包含魔数、版本号、常量池、拜访标记等。

3 Class文件概览

依据 JVM 标准,一个 Class 文件能够十分谨严地形容为:

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];
}

上面会按程序具体介绍外面的各个字段。

4 魔数

魔数(Magic Number)作为 Class 的标记,用来通知 JVM 这是一个 Class 文件,魔数是一个 4 字节的无符号整数,固定为 0xCAFEBABE。如果一个Class 文件不以 0xCAFEBABE 结尾,那么会抛出如下谬误:

Linux下能够间接应用 vim 关上 class 文件进行查看,比方须要关上一个 Test.class 文件,能够输出如下命令:

vim -b Test.class
:%!xxd

切换到十六进制后就能够看到魔数了:

5 版本

魔数前面紧跟着 Class 的小版本和大版本号,这示意以后 Class 文件是由哪个版本的编译期产生的。小版本和大版本后都是占用两个字节,比方下图:

  • 0000是小版本号
  • 0037是大版本号,十进制为 55,也就是对应JDK 11 版本的编译期

6 常量池

在版本号前面,紧跟着就是常量池的数量以及若干个常量池表项:

其中每一个常量池表项都具备标签属性:

对应关系举例如下:

  • tag为 3:类型为CONSTANT_Integer
  • tag为 4:类型为CONSTANT_Float

等等,比方 CONSTANT_Integer 构造如下:

CONSTANT_Integer_info {
    u1 tag;
    u4 bytes;
}

一个 tag 加上一个四字节的无符号整数。其余类型大部分相似,篇幅限度,具体请看 JVM 标准。

7 拜访标记

拜访标记应用两个字节示意,用于表明该类的访问信息,比方 public/abstract 等,对应关系如下:

  • ACC_PUBLIC0x0001,示意 public
  • ACC_FINAL0x0010,示意是否为 final
  • ACC_SUPER0x0020,示意应用加强的办法调用父类的办法
  • ACC_INTERFACE0x0200,示意是否为接口
  • ACC_ABSTRACT0x0400,示意是否为抽象类
  • ACC_SYNTHETIC0x1000,由编译期产生的类,没有源码对应
  • ACC_ANNOTATION0x2000,示意是否是正文
  • ACC_ENUM0x4000,示意是否为枚举

8 以后类、父类和接口

格局如下:

u2             this_class;                                    
u2             super_class;
u2             interfaces_count;
u2             interfaces[interfaces_count];

其中 this_classsuper_class都是两个字节的无符号整数,指向常量池中的一个 CONSTANT_Class,示意以后的类型以及父类。另外,因为一个类能够实现多个接口,因而须要以数组模式保留多个接口的索引,如果没有实现任何接口,则interfaces_count 为 0。

9 字段

字段的格局如下:

u2             fields_count;
field_info     fields[fields_count];

fields_count是一个 2 字节的无符号整数,字段数量之后是具体的字段信息,每个字段都是一个 field_info 的构造,如下所示:

field_info {
    u2             access_flags;                         // 拜访标记,相似于类的拜访标记,能够示意 public/private/static 等等
    u2             name_index;                           // 两字节整数,指向常量池中的 CONSTANT_Utf8
    u2             descriptor_index;                     // 也是两字节整数,用于形容字段类型,也指向常量池中的 CONSTANT_Utf8
    u2             attributes_count;                     // 属性数量
    attribute_info attributes[attributes_count];         // 属性,比方存储初始化值,一些正文信息等,须要应用 attribute_info
}

attribute_info {
    u2 attribute_name_index;                             // 属性名字,指向常量池的索引
    u4 attribute_length;                                 // 属性长度
    u1 info[attribute_length];                           // 字节数组示意的信息
}

10 办法

10.1 办法根本构造

办法的格局如下:

u2             methods_count;
method_info    methods[methods_count];

其中每一个 method_info 构造示意一个办法:

method_info {
    u2             access_flags;                            // 拜访标记,标记办法为 public/private 等等
    u2             name_index;                              // 办法名称,一个指向常量池的索引
    u2             descriptor_index;                        // 办法描述符,也是一个指向常量符的索引
    u2             attributes_count;                        // 属性数量
    attribute_info attributes[attributes_count];            // 属性,和字段相似,办法也能够携带属性,一个属性数量 + 一个属性形容数组
}

10.2 Code属性

办法的次要内容寄存在属性中,在属性外面最重要的一个属性就是 CodeCode 寄存着办法的字节码等信息,构造如下:

Code_attribute {
    u2 attribute_name_index;                      // 属性名称,指向常量池的索引
    u4 attribute_length;                          // 属性长度,不包含前 6 字节(u2+u4)u2 max_stack;                                 // 操作数栈最大深度
    u2 max_locals;                                // 局部变量表的最大值
    u4 code_length;                               // 字节码长度
    u1 code[code_length];                         // 字节码内容自身
    u2 exception_table_length;                    // 异样处理表长度
    {   u2 start_pc;                              // 四个字段示意在 start_pc 到 end_pc 两个偏移量之间
        u2 end_pc;                                // 如果遇到了 catch_type 指向的异样
        u2 handler_pc;                            // 代码就跳转到 handler_pc 地位执行
        u2 catch_type;                            
    } exception_table[exception_table_length];    // 异样表
    u2 attributes_count;
    attribute_info attributes[attributes_count];
}

Code属性自身也蕴含其余属性以进一步存储一些额定信息,次要包含:

  • LineNumberTable
  • LocalVariableTable
  • StackMapTable

10.2.1 LineNumberTable

LineNumberTable用于记录字节码偏移量和行号的对应关系,构造如下:

LineNumberTable_attribute {
    u2 attribute_name_index;                             // 指向常量池的索引
    u4 attribute_length;                                 // 属性长度
    u2 line_number_table_length;                         // 表项记录条数
    {   u2 start_pc;                                     // 字节码偏移量
        u2 line_number;                                     // 字节码偏移量对应的行号
    } line_number_table[line_number_table_length];       // 表数组,每一个元素对应的是一个 <start_pc,line_number> 元组
}

10.2.2 LocalVariableTable

这个属性也叫局部变量表,记录了一个办法中所有的局部变量,构造如下:

LocalVariableTable_attribute {
    u2 attribute_name_index;                                     // 以后属性名字,指向常量池的索引
    u4 attribute_length;                                         // 属性长度
    u2 local_variable_table_length;                              // 局部变量表的表项条目
    {   u2 start_pc;                                             // 以后局部变量开始地位
        u2 length;                                               // 以后局部变量长度(可用于计算完结地位)u2 name_index;                                           // 局部变量名称,指向常量池的索引
        u2 descriptor_index;                                     // 局部变量的类型形容,指向常量池的索引
        u2 index;                                                // 局部变量在以后栈帧的局部变量表中的槽位
    } local_variable_table[local_variable_table_length];         
}

10.2.3 StackMapTable

StackMapTable中含有若干个栈映射帧(Stack Map Frame)的数据,不蕴含运行时所须要的信息,仅用作 Class 文件的类型校验,构造如下:

StackMapTable_attribute {
    u2              attribute_name_index;                         // 常量池索引,恒为 "StackMapTable"
    u4              attribute_length;                             // 属性长度
    u2              number_of_entries;                            // 栈映射帧的数量
    stack_map_frame entries[number_of_entries];                   // 具体的栈映射帧
}

union stack_map_frame {                                           // 每个栈映射帧被定义为一个枚举值,取值如下
    same_frame;                                                   // 具体每个取值的意义能够查看 JVM 标准
    same_locals_1_stack_item_frame;                               //https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-4.html#jvms-4.7.4
    same_locals_1_stack_item_frame_extended;
    chop_frame;
    same_frame_extended;
    append_frame;
    full_frame;
}

每个栈映射帧是为了阐明在一个特定的字节码偏移地位上,零碎的数据类型是什么,包含局部变量表的类型和操作数栈的类型。

11 附录:ASM简略应用

ASM是一个 Java 字节码操作库,很多驰名的库都依赖于该库,比方 AspectJCGLIB 等等。然而 ASM 的性能远远超过 CGLIB 等高层字节码库,因为 ASM 更加靠近底层,应用更为灵便且性能更为弱小。

上面是一个简略的应用 ASM 输入 Hello World 的例子:

package com.company;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

public class Main extends ClassLoader implements Opcodes {public static void main(String[] args) throws Exception{
        // 创立 ClassWriter,指定 COMPUTE_MAXS 和 COMPUTE_FRAMES,别离示意计算最大局部变量表以及最深操作数栈
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
        // 通过 ClassWriter 设置类的根本信息,比方 public 拜访标记,类名为 Example
        cw.visit(V11,ACC_PUBLIC,"Example",null,"java/lang/Object",null);
        // 生成 Example 的构造方法
        MethodVisitor mw = cw.visitMethod(ACC_PUBLIC ,"<init>","()V",null,null);
        mw.visitVarInsn(ALOAD,0);
        mw.visitMethodInsn(INVOKESPECIAL,"java/lang/Object","<init>","()V",false);
        mw.visitInsn(RETURN);
        mw.visitMaxs(0,0);
        mw.visitEnd();

        // 生成 public static void main(String []args)办法,并生成了 main()办法的字节码
        // 要求运行时调用 System.out.println(),并输入 "Hello world":mw = cw.visitMethod(ACC_PUBLIC+ACC_STATIC,"main","([Ljava/lang/String;)V",null,null);
        mw.visitFieldInsn(GETSTATIC,"java/lang/System","out","Ljava/io/PrintStream;");
        mw.visitLdcInsn("Hello world!");
        mw.visitMethodInsn(INVOKEVIRTUAL,"java/io/PrintStream","println","(Ljava/lang/String;)V",false);
        mw.visitInsn(RETURN);
        mw.visitMaxs(0,0);
        mw.visitEnd();

        // 获取二进制示意
        byte[] code = cw.toByteArray();
        Main m = new Main();
        // 将 class 文件载入零碎,通过反射调用 `main()` 办法,输入后果
        Class<?> mainClass = m.defineClass("Example",code,0,code.length);
        mainClass.getMethods()[0].invoke(null, new Object[]{null});
    }
}

正文完
 0