zoukankan      html  css  js  c++  java
  • JVM-Class文件

      一个 Class 文件描述了类或接口的字段,方法,父类,访问权限等全部信息。其实,它只是一种能被 JVM 识别的数据格式,就和 UDP 8字节头部一样,这就是规范,标准!所谓“不闻不若闻之,闻之不若见之,见之不若知之,知之不若行之,学至于行而止矣,行之,明也”。本文最后将动手分析一个 Class 文件的字节和反编译后的伪汇编语言,来探讨其结构。

      本文将介绍:

    • Class 头部信息
    • Class 常量池信息
    • Class 父类,访问权限和接口信息
    • Class 字段及其属性
    • Class 方法及其属性
    • 其他属性和实例描述

      Class 文件是以 8-bit 为单位的字节流,并且多字节数据以 big-endian 排列(又称为网络序,高位在前,符合我们的直觉,如日常使用的十进制)。注意使用 javac –g 编译源码,-g 表示生成所有的调试信息,javap –p –v 显示全部的伪汇编语言。根据 JVM 规范,ClassFile 格式如下:

    1. Class 头部信息

      对于定长的字段,比较好解析,麻烦的是变长的字段,需要根据不同类型,提取不同长度的字节进行解析,比如常量池中的信息,不同字段的结构解析方式不同。前三个字段还是比较好解析的,前4个字节为魔数 magic=0xcafebabe,2字节的从版本号 minor=0x0000,2字节的主版本号 major=0x0034=3*16+4=52,如下:

    2. Class 常量池信息

      JVM 执行时,不依赖类,实例或接口在内存是怎么布局的,而是依赖运行时常量池,基于栈来执行指令。常量池中包含字面量和符号引用,字面量就是字符串常量和数值常量;符号引用就是用来描述类或接口、字段名和其描述符、方法名和其描述符。根据 ClassFile 的结构,可以看到 2 字节表示常量池大小,后面 cp_info 是具体内容,而 cp_info不同类型的大小不同,常量池中的类型如下:

    类型 标志 大小(字节) 描述
    CONSTANT_Utf8_info 0x01 3+len 字符串,数值常量,类全限定名,方法名,字段名,属性名等字面量
    CONSTANT_String_info 0x08 3 字符串类型符号引用,引用 utf8 表示的字面量
    CONSTANT_Class 0x07 3 类或接口的符号引用
    CONSTANT_NameAndType 0x0c 5 字段或方法的符号引用,但没有指明是哪个类或接口的方法
    CONSTANT_Fieldref 0x09 5 字段的符号引用,表明所属类,和字段名
    CONSTANT_Methodref 0x0a 5 方法的符号引用,表明所属类,和方法名
    CONSTANT_InterfaceMethodref 0x0b 5 接口中方法的符号引用,表明所属接口,和方法名
    CONSTANT_Integer 0x03 5 整型字面量
    CONSTANT_Float 0x04 5 浮点型字面量
    CONSTANT_Long 0x05 6 长整型字面量
    CONSTANT_Double 0x06 6 双精度浮点型字面量
    CONSTANT_MethodHandle 0x0f 4 表示方法句柄
    CONSTANT_MethodType 0x10 3 表示方法类型
    CONSTANT_InvokeDynamic 0x12 5 动态调用

      为了方便阅读,上表没有给出具体的结构,可查看 JVM规范 (§4)。知道了常量池中类型的大小,就可以进行解析了,下图是常量池的部分字节:

      其中,连续的,颜色相同的框表示一个类型的信息。常量池元素总数为【00 22】= 34个,JVM 会对常量池中解析的类型从 1 进行编号,比如【0a 00 06 00 14】标识为10,查询上表,可知此字段为方法的符号引用,大小为5字节,其内容为【#1 = Methodref  #6.#20】 后面引用所属类和方法描述。其他字段也是如此,如果需要引用到常量池中的其他内容解析出编号即可。再看一个常量字符串的序列【01 00 06 3c 69 6e 69 74 3e】01表明是utf8字符串字面量,【00 06】表示长度为6,后面6位转为字符串就是<init>,这是类实例化时调用的方法。就这样一次进行 33 次,就可以得到整个常量池的信息。

      在常量池中,类,字段和方法描述符:

    类和接口名,使用全限定名称 把 点. 换成 /,比如 java.lang.Object 全限定名:java/lang/Object
    方法,字段,局部变量名,使用非全限定名称 当前类的相对名称,比如 Demo
    字段描述符 描述字段类型
    B,C,D,F,I,[ 分别对应 byte,char,double,float,int,数组类型
    J,S,Z,V,L class_name 分别对应 long,short,boolean,void,class_name对象类型
    方法描述符,描述参数和返回值类型 ( {ParameterDescriptor} ) ReturnDescriptor
    ()V Object m(int i, double d, Thread t) {...}
    无参返回值为空 (IDLjava/lang/Thread;)Ljava/lang/Object;

    3. Class 父类,访问权限和接口信息

      根据ClassFIle的结构,接下来是 2 字节的 access_flags,【access_flags】表示类或接口的访问权限,具体的标志位如下表:

    标志名称 标志值 含义
    ACC_PUBLIC 0x0001 声明为 public
    ACC_FINAL 0x0010 声明为 final,不能被继承
    ACC_SUPER 0x0020 表明使用 invokespecial 调用父类方法
    ACC_INTERFACE 0x0200 声明为 接口
    ACC_ABSTRACT 0x0400 声明为抽象类,不能实例化
    ACC_SYNTHETIC 0x1000 动态生成的代码,不是用户由编写的
    ACC_ANNOTATION 0x2000 声明 注解
    ACC_ENUM 0x4000 声明 枚举

    access_flags使用 2 个字节表示,由上图可知为 0x0021,转换成二进制 【0000 0000 0020 0001】,查上表可得此类为 ACC_PUBLIC,ACC_SUPER。

      接下来就是当前类索引,父类索引和接口索引描述,其中当前类和父类描述都是通过 2 个字节,2 字节的 this_class,2 字节的 super_class。这里this_calss是 #5 表示引用常量池中第5个常量,从常量池中可以看到就是当前的Demo类;super_class是 #6 ,查常量池可知,#6 代表java.lang.Object对象,这说明了,Object是Java中所有类的直接或间接父类。

      如果类实现了接口,那么接口的信息会在接下来的字节表示出来,这里没有实现接口,字节全为 0。如果实现接口的话,首先 2 个字节表示接口的个数,接下来就是具体内容,每个接口信息占 2 个字节,并引用常量池中的Class类型的元素,并按照源代码中实现的顺序排列。

    4. Class 字段及其属性

      类字段信息主要有,访问控制和字段类型。下表是字段的访问控制说明:

    字段访问标志 标志值 含义
    ACC_PUBLIC 0x0001 声明为 public
    ACC_PRIVATE 0x0002 声明为 private
    ACC_PROTECTED 0x0004 表明为 protected
    ACC_STATIC 0x0008 声明为 static
    ACC_FINAL 0x0010 声明为 final
    ACC_VOLATILE 0x0040 声明为 volatitle
    ACC_TRANSIENT 0x0080 声明为 transient 不序列化
    ACC_SYNTHETIC 0x1000 表示由编译器生成
    ACC_ENUM 0x4000 声明为 enum

      字段有一个 ConstantValue 属性,只有当字段是 ACC_STATIC 时,此属性才生效,并且长度固定为 2,引用常量池中的 int, short, char, byte, boolean, long, float, double, String 类型字面量。

    5. Class 方法及其属性

      描述了方法的参数及其类型,返回值,字节码,以及最大操作数栈深度,局部变量个数,参数个数,访问控制等信息,首先看一下,方法的访问标志:

    方法访问标志 标志值 含义
    ACC_PUBLIC 0x0001 声明为 public
    ACC_PRIVATE 0x0002 声明为 private
    ACC_PROTECTED 0x0004 表明为 protected
    ACC_STATIC 0x0008 声明为 static
    ACC_FINAL 0x0010 声明为 final,不能重写
    ACC_SYNCHRONIZED 0x0020 synchronized,方法由管程同步
    ACC_BRIDGE 0x0040 方法由编译器产生
    ACC_VARARGS 0x0080 表示方法带有变长参数
    ACC_NATIVE 0x4100 非 java 语言的本地方法
    ACC_ABSTRACT 0x0400 abstract,方法没有具体实现
    ACC_STRICT 0x0800 strictfp,方法使用 FP-strict 浮点格式
    ACC_SYNTHETIC 0x1000 方法在源文件中不出现,由编译器产生

      方法的属性,比较重要,里面具有 JVM 要执行的指令信息。

    5.1 Exceptions 描述方法可能抛出的异常

    5.2 Code 属性

      Code 代码属性是最要的属性,描述方法的字节码或辅助信息,如果是本地方法或抽象方法,那么不能拥有此属性。Code 结构如下:

      Code 的相关属性:

    • LocalVariableTable:局部变量表
    • LocalVariableTypeTable:也是局部变量表,跟泛型有关
    • LineNumberTable:源代码行号与字节码行号对应关系
    • StackMapTable:在 JVM 验证类型时使用

    6. 其他属性信息

    类相关属性:

    • InnerClasses:内部类属性
    • EnclosingMethod:当类为内部类或局部类是此属性才存在,并且一个类至多有一个
    • SourceFile:定长,只能有一个
    • SourceDebugExtension:扩展调试信息
    • BootstrapMethods:保存 invokedynamic 指令引用的引导方法限定符

    方法属性:

    • RuntimeVisibleParameterAnnotations:方法参数的注解,可被反射 API 使用
    • RuntimeInvisibleParameterAnnotations:不能被反射 API 使用
    • MethodParameters:方法参数信息
    • AnnotationDefault:元素注解默认值

    共有属性:

    • Deprecated:表面字段,方法和类将会在后续版本被替换
    • Synthetic:源代码未出现的类成员,必须标记成合成属性,并且长度固定为 0
    • Signature:描述类,方法或接口的泛型信息
    • RuntimeVisibleAnnotations:保存类,字段和方法可见注解,必须保证可被反射使用
    • RuntimeInvisibleAnnotations:不能被反射 API 访问
    • RuntimeVisibleTypeAnnotations:
    • RuntimeInvisibleTypeAnnotations:

    7. 实例描述

    (1)常量池


    (2)方法描述

    本文使用的Demo源码和Class文件:Demo.rar

    作 者:创心coder
    QQ群:361982568
    订阅号:cxcoder
    文章不足之处,还请谅解!本文会不断完善更新,转载请保留出处。如果帮到了您,烦请点赞,以资鼓励。
  • 相关阅读:
    随机验证码生成
    python之map和filter
    Json学习笔记
    动态规划求区间最值问题RMQ(Range Minimum/Maximum Query)
    积水问题
    5亿个数找中位数
    Linux下进程间通信:命名管道mkfifo
    Trie树总结
    树的公共祖先问题LCA
    类文件结构
  • 原文地址:https://www.cnblogs.com/cwane/p/6099559.html
Copyright © 2011-2022 走看看