目录
类结构
- 头部信息
- 常量池
- 类信息部分
- 字段信息
- 方法信息
- 属性信息
class ClassFile {
/**
***********************************************************
* 1. class头部信息部分
***********************************************************
*/
u4 magic; //标记为class文件的魔数,固定为 0xCAFEBABE
u2 minor_version; //class文件的次版本号
u2 major_version; //class文件的主版本号
/*
***********************************************************
* 2. 常量池部分
***********************************************************
*/
u2 constant_pool_count; //常量池中常量表个数
cp_info constant_pool[constant_pool_count-1]; //常量池数据
/**
***********************************************************
* 3. class信息部分
***********************************************************
*/
u2 access_flags; //类的访问权限
u2 this_class; //
u2 super_class; //
u2 interfaces_count; //
u2 interfaces[interfaces_count]; //
/**
***********************************************************
* 4. class字段部分
***********************************************************
*/
u2 fields_count;
field_info fields[fields_count];
/**
***********************************************************
* 5. class方法部分
***********************************************************
*/
u2 methods_count;
method_info methods[methods_count];
/**
***********************************************************
* 6. class属性部分
***********************************************************
*/
u2 attributes_count;
attribute_info attributes[attributes_count];
}
1. 头部信息
- magic 魔法数
- minor_version class文件的子版本号
- major_version class文件的主版本号
2. 常量池
- UTF8
存储class文件中使用到的所有字符串信息,包括用户使用到的字符串常量、类、方法、变量名及签名等,还包括class内部使用的一些关键字,如this等。- 程序内部使用的字符串,如String a=“hello,world"中的hello,world; @RequestMapping(”/api/goods/info")中的 /api/goods/info
- 类名,如 java/lang/System
- 类签名,如 Ljava/io/PrintStream;
- 变量名,如 out
- 方法名,如 append
- 没有方法名的方法签名,如 (Ljava/lang/String;)Ljava/lang/StringBuilder;,即形参是String类型,返回值是StringBuilder的方法
- class内部使用的一些关键字:如MethodParameters、LineNumberTable、LocalVariableTable、 StackMapTable、SourceFile、this
- String
字符串常量,通过一个index指向常量池中UTF8数组中的一个元素。#12 = String #83 // normal url: #83 = Utf8 normal url:
- Class
类名,同String一样,也是通过一个index指向UTF8数组中的一个元素代表某个类。#52 = Class #89 // java/io/IOException #89 = Utf8 java/io/IOException
- NameAndType
方法与类型的组合,即方法签信息,由两个UTF8数组的下标组成,如#90 = NameAndType #119:#31 // printStackTrace:()V #119 = Utf8 printStackTrace #31 = Utf8 ()V
- MethodRef
表示某个具体类的方法信息,包含类名、方法名、形参类型和返回值,由类常量+NameType常量组成,如#14 = Methodref #3.#85 // java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder; #3 = Class #72 // java/lang/StringBuilder #85 = NameAndType #103:#117 // append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
- InterfaceMethodRef
- FieldRef
表示某个具体的字段信息,包含类名、字段名及字段类型。由类常量+NameType常量组成,如#2 = Fieldref #70.#71 // java/lang/System.out:Ljava/io/PrintStream; #70 = Class #100 // java/lang/System #71 = NameAndType #101:#102 // out:Ljava/io/PrintStream;
3. 类信息
- access_flags
类的访问权限。 - this_class
类索引,当前类的包类+类名,使用一个u2类型的索引值来表示,指向常量表类型为Constan_Class_info的类描述符,根据常量表中index值找到全限定名字符串。 - super_class
父类索引,基类信息,与this_class一样,同样使用u2类型类指向常量池中的Constan_Class_info类描述 - interfaces
接口索引集合,与常量池一样,使用一个u2表示集合的大小,后面紧跟着接口在常量池中的索引。
4. 字段信息
描述字段的详情信息,包括访问权限、字段名、描述信息在常量池中的索引、属性数量和具体的属性信息。针对泛型,descriptor_index指向的是泛型类型,不包含泛型参数信息
,如List<String>类型的变量,descriptor_index指定的是常量池中的Ljava/utils/List;,而不是Ljava/utils/List<Ljava/lang/String;>;。在使用BCEL工具生成class文件时,如果让descriptor_index指定了包含泛型参数的泛型类型,则生成的变量在IDEA中会显示异常。按正常的规则指定不包含泛型参数的泛型类型,生成的变量在IDEA中显示只是List。如果想让生成的代码在IDEA中显示为List<String>需要有一个类型为Signature类型的属性(参考以下第6点),让此属性包含完成的变量类型签名即可。
class field_info {
u2 access_flags; //成员变量的访问权限
u2 name_index; //成员变量名对应的常量池下标index
u2 descriptor_index; //成员变量的签名信息对应的常量池下标index
u2 attributes_count; //成员变量的属性信息个数
attribute_info attributes[attributes_count]; //成员变量的属性信息
}
5. 方法信息
方法信息与字段信息结构一样,包含了访问权限 、方法名、方法描述在常量池中的索引及属性个数和属性信息。方法的代码包含在一个类型为Code的属性中。
class method_info {
u2 access_flags; //方法的访问权限
u2 name_index; //方法名对应的常量池下标index
u2 descriptor_index; //方法签名信息对应的常量池下标index
u2 attributes_count; //方法的属性信息个数
attribute_info attributes[attributes_count]; //方法的属性信息
}
6. 属性信息
属性的种类有以下Code、LineNumberTable、LocalVariableTable、StackMapTable、ConstrantValue、Exceptions、Signature、BootstrapMethod、InnerClass、Deprecated、Synthetic等。
Code
class Code_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 max_stack; //操作数栈深度最大值
u2 max_locals; //局部变量所需的最大空间,单位是Solt,
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];
}
LineNumberTable
描述Java源码行号与字节码行号之间的对应关系。
LocalVariableTable
描述栈中局部变量表中的变量与Java源码中定义的变量之间的关系。
StackMapTable
ConstantValue
Exceptions
列举方法中可能出现的异常
Signature
变量及方法的签名信息。因为Java语言的泛型采用擦除法实现的伪泛型,在字节码中,泛型信息在编译之后通通被擦除,如运行期做反射时无法获取泛型信息,Signature属性就是为了弥补这个缺陷而增设,可以让Java反射API能够获取反射类型。
BootstrapMethod
class BootstrapMethods_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 num_bootstrap_methods;
{ u2 bootstrap_method_ref;
u2 num_bootstrap_arguments;
u2 bootstrap_arguments[num_bootstrap_arguments];
} bootstrap_methods[num_bootstrap_methods];
}
JDK1.7版本加入到Class文件规范,表示复杂可变属性。位于类文件的属性表,用于记录invokeddynamic指定引用的引导方法限定符,最多只能有一个BootstrapMethod属性。如Lambda方法编译成class文件时,会生成一个lambda方法和一个lambda类,同时会生成一个BootstrapMethod属性。调用lambda方法时使用的即是invokedynamic
调用指令如下:
38: invokedynamic #12, 0 // InvokeDynamic #0:onDone:(Lcom/example/demo/CategoryPresenter;)Lcom/ymt/library/promise/DoneCallback;
47: invokedynamic #14, 0 // InvokeDynamic #1:onFail:(Lcom/example/demo/CategoryPresenter;)Lcom/example/library/promise/FailCallback;
常量池如下:
InvokeDynamic常量类型的第一个参数为BootstramMethod属性的index值。
#12 = InvokeDynamic #0:#68 // #0:onDone:(Lcom/example/demo/CategoryPresenter;)Lcom/example/library/promise/DoneCallback;
#14 = InvokeDynamic #1:#73 // #1:onFail:(Lcom/example/demo/CategoryPresenter;)Lcom/example/library/promise/FailCallback;
BootstrapMethod属性如下:
BootstrapMethods:
0: #64 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
Method arguments:
#65 (Ljava/lang/Object;)V
#66 invokespecial com/example/demo/CategoryPresenter.lambda$init$0:(Ljava/util/List;)V
#67 (Ljava/util/List;)V
1: #64 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
Method arguments:
#65 (Ljava/lang/Object;)V
#71 invokevirtual com/example/demo/base/BasePresenter.defaultError:(Lcom/example/library/net/bean/Error;)V
#72 (Lcom/example/library/net/bean/Error;)V
InnerClass
用于记录内部类与宿主类之间的关联关系。
Deprecated
Synthetic
工具
1. Javassit
面向Java代码的class文件生成器,不需要了解JVM相关指令。对于使用者来说相对比较友好,但它执行效率低,另外它需要全量的代码参与编译,如果要编译的class文件依赖的其他class文件不存在的话,会报错,导致编译失败。
2. ASM
asm工具直接操作class文件的字节码,需要了解JVM相关的指令。但它执行效率高,对于不会写的指令,可通过IDEA插件ASM ByteCode Viewer查看相关代码的ASM书写方式。操作时不用关心常量池问题
。推荐使用此工具进行class文件的编辑工作。
3. BCEL
直接操作字节码,可获取常量池、方法、字段等信息,对于class文件分析场景,使用此工具比较方便。生成代码时,需要关注常量池问题。
应用场景
1. 分析组件之间的调用关系
典型应用如根据class文件分析方法的调用关系,对代码修改做影响范围评估;分析组件之间的依赖关系等。
实施流程
主要是对类构造中的常量池进行分析,找到方法、类之间的关联关系。
2. 修改class文件内容
典型应用如根据注解信息生成相关代码减少运行时反射调用;Jacoco对代码插桩添加探针统计代码覆盖率;美团Robust在方法入口注入代码,实现热修复能力等。
实施流程
可使用javap查看class文件的常量池、指令等信息。先使用javap对class文件进行反编译,根据class文件的信息,使用工具库进行编码,生成相关代码。
- 考虑好要生成什么样的代码
- 编写相关的Java代码,并编译成class文件
- 使用javap工具查看字JVM指令等信息
- 根据JVM指令信息使用相关工具进行编码
- 再次使用javap工具反编译生成的class文件
- 对比编译器生成的class文件与编译器生成的class文件之间的差异。也可将class文件拖到IDEA中,快速查看它们之间的差异。
参考:
- https://zhuanlan.zhihu.com/p/25823310