zoukankan      html  css  js  c++  java
  • JVM学习笔记(七):Class文件结构

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

    在这里插入图片描述

  • 相关阅读:
    SDN第4次上机作业
    SDN第三次作业
    SDN第三次上机作业
    SDN第二次上机作业
    SND第二次作业
    【Beta Scrum】冲刺!4/5
    【Beta Scrum】冲刺! 3/5
    【Beta Scrum】冲刺! 2/5
    【Beta Scrum】冲刺! 1/5
    SDN第二次上机作业
  • 原文地址:https://www.cnblogs.com/6b7b5fc3/p/14726676.html
Copyright © 2011-2022 走看看