Class 文件格式实战:使用 ASM 动态生成 class 文件
本专栏前面的文章,主要详细讲解了 Class 文件的格式,并且在上一篇文章中做了总结。 众所周知, JVM 在运行时, 加载并执行 class 文件, 这个 class 文件基本上都是由我们所写的 java 源文件通过 javac 编译而得到的。 但是, 我们有时候会遇到这种情况:在前期(编写程序时)不知道要写什么类, 只有到运行时, 才能根据当时的程序执行状态知道要使用什么类。 举一个常见的例子就是 JDK 中的动态代理。这个代理能够使用一套 API 代理所有的符合要求的类, 那么这个代理就不可能在 JDK 编写的时候写出来, 因为当时还不知道用户要代理什么类。
当遇到上述情况时, 就要考虑这种机制:在运行时动态生成 class 文件。 也就是说, 这个 class 文件已经不是由你的 Java 源码编译而来,而是由程序动态生成。 能够做这件事的,有 JDK 中的动态代理 API, 还有一个叫做 cglib 的开源库。 这两个库都是偏重于动态代理的, 也就是以动态生成 class 的方式来支持代理的动态创建。 除此之外, 还有一个叫做 ASM 的库, 能够直接生成 class 文件,它的 api 对于动态代理的 API 来说更加原生, 每个 api 都和 class 文件格式中的特定部分相吻合, 也就是说, 如果对 class 文件的格式比较熟练, 使用这套 API 就会相对简单。 下面我们通过一个实例来讲解 ASM 的使用, 并且在使用的过程中, 会对应 class 文件中的各个部分来说明。
ASM 示例:HelloWorld
ASM 的实现基于一套 Java API, 所以我们首先得到 ASM 库, 在这个我使用的是 ASM 4.0 的 jar 包 。
首先以 ASM 中的 HelloWorld 实例来讲解, 比如我们要生成以下代码对应的 class 文件:
-
public class Example {
-
-
public static void main (String[] args) {
-
System.out.println("Hello world!");
-
}
但是这个 class 文件不能在开发时通过上面的源码来编译成, 而是要动态生成。 下面我们介绍如何使用 ASM 动态生成上述源码对应的字节码。
下面是代码示例(该实例来自于 ASM 官方的 sample):
-
import java.io.FileOutputStream;
-
-
import org.objectweb.asm.ClassWriter;
-
import org.objectweb.asm.MethodVisitor;
-
import org.objectweb.asm.Opcodes;
-
-
public class Helloworld extends ClassLoader implements Opcodes {
-
-
public static void main(final String args[]) throws Exception {
-
-
-
//定义一个叫做Example的类
-
ClassWriter cw = new ClassWriter(0);
-
cw.visit(V1_1, ACC_PUBLIC, "Example", null, "java/lang/Object", null);
-
-
//生成默认的构造方法
-
MethodVisitor mw = cw.visitMethod(ACC_PUBLIC,
-
"<init>",
-
"()V",
-
null,
-
null);
-
-
//生成构造方法的字节码指令
-
mw.visitVarInsn(ALOAD, 0);
-
mw.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V");
-
mw.visitInsn(RETURN);
-
mw.visitMaxs(1, 1);
-
mw.visitEnd();
-
-
//生成main方法
-
mw = cw.visitMethod(ACC_PUBLIC + ACC_STATIC,
-
"main",
-
"([Ljava/lang/String;)V",
-
null,
-
null);
-
-
//生成main方法中的字节码指令
-
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");
-
mw.visitInsn(RETURN);
-
mw.visitMaxs(2, 2);
-
-
//字节码生成完成
-
mw.visitEnd();
-
-
// 获取生成的class文件对应的二进制流
-
byte[] code = cw.toByteArray();
-
-
-
//将二进制流写到本地磁盘上
-
FileOutputStream fos = new FileOutputStream("Example.class");
-
fos.write(code);
-
fos.close();
-
-
//直接将二进制流加载到内存中
-
Helloworld loader = new Helloworld();
-
Class<?> exampleClass = loader.defineClass("Example", code, 0, code.length);
-
-
//通过反射调用main方法
-
exampleClass.getMethods()[0].invoke(null, new Object[] { null });
-
-
-
}
-
}
下面详细介绍生成 class 的过程:
1 首先定义一个类
-
//定义一个叫做Example的类
-
ClassWriter cw = new ClassWriter(0);
-
cw.visit(V1_1, ACC_PUBLIC, "Example", null, "java/lang/Object", null);
ClassWriter 类是 ASM 中的核心 API , 用于生成一个类的字节码。 ClassWriter 的 visit 方法定义一个类。
第六个参数是 String [] 类型的, 传入当前要生成的类的直接实现的接口。 这里这个类没实现任何接口, 所以传入 null 。 这个参数对应 class 文件中的 interfaces 。
2 定义默认构造方法, 并生成默认构造方法的字节码指令
-
//生成默认的构造方法
-
MethodVisitor mw = cw.visitMethod(ACC_PUBLIC,
-
"<init>",
-
"()V",
-
null,
-
null);
-
-
//生成构造方法的字节码指令
-
mw.visitVarInsn(ALOAD, 0);
-
mw.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V");
-
mw.visitInsn(RETURN);
-
mw.visitMaxs(1, 1);
-
mw.visitEnd();
3 定义 main 方法, 并生成 main 方法中的字节码指令
-
mw = cw.visitMethod(ACC_PUBLIC + ACC_STATIC,
-
"main",
-
"([Ljava/lang/String;)V",
-
null,
-
null);
-
-
//生成main方法中的字节码指令
-
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");
-
mw.visitInsn(RETURN);
-
mw.visitMaxs(2, 2);
-
mw.visitEnd();
这个过程和上面的生成默认构造方法的过程是一致的。 读者可对比上一步执行分析。
4 生成 class 数据, 保存到磁盘中, 加载 class 数据
-
// 获取生成的class文件对应的二进制流
-
byte[] code = cw.toByteArray();
-
-
-
//将二进制流写到本地磁盘上
-
FileOutputStream fos = new FileOutputStream("Example.class");
-
fos.write(code);
-
fos.close();
-
-
//直接将二进制流加载到内存中
-
Helloworld loader = new Helloworld();
-
Class<?> exampleClass = loader.defineClass("Example", code, 0, code.length);
-
-
//通过反射调用main方法
-
exampleClass.getMethods()[0].invoke(null, new Object[] { null });
这段代码首先获取生成的 class 文件的字节流, 把它写在本地磁盘的 Example.class 文件中。 然后加载 class 字节流, 并通过反射调用 main 方法。
Hello world!
然后在当前测试工程的根目录下, 生成一个 Example.class 文件文件。
javap -c -v -classpath . -private Example
输出的完整信息如下:
-
Classfile /C:/Users/纪刚/Desktop/生成字节码/AsmJavaTest/Example.class
-
Last modified 2014-4-5; size 338 bytes
-
MD5 checksum 281abde0e2012db8ad462279a1fbb6a4
-
public class Example
-
minor version: 3
-
major version: 45
-
flags: ACC_PUBLIC
-
Constant pool:
-
#1 = Utf8 Example
-
#2 = Class #1 // Example
-
#3 = Utf8 java/lang/Object
-
#4 = Class #3 // java/lang/Object
-
#5 = Utf8 <init>
-
#6 = Utf8 ()V
-
#7 = NameAndType #5:#6 // "<init>":()V
-
#8 = Methodref #4.#7 // java/lang/Object."<init>":()V
-
#9 = Utf8 main
-
#10 = Utf8 ([Ljava/lang/String;)V
-
#11 = Utf8 java/lang/System
-
#12 = Class #11 // java/lang/System
-
#13 = Utf8 out
-
#14 = Utf8 Ljava/io/PrintStream;
-
#15 = NameAndType #13:#14 // out:Ljava/io/PrintStream;
-
#16 = Fieldref #12.#15 // java/lang/System.out:Ljava/io/PrintStream;
-
#17 = Utf8 Hello world!
-
#18 = String #17 // Hello world!
-
#19 = Utf8 java/io/PrintStream
-
#20 = Class #19 // java/io/PrintStream
-
#21 = Utf8 println
-
#22 = Utf8 (Ljava/lang/String;)V
-
#23 = NameAndType #21:#22 // println:(Ljava/lang/String;)V
-
#24 = Methodref #20.#23 // java/io/PrintStream.println:(Ljava/lang/String;)V
-
#25 = Utf8 Code
-
{
-
public Example();
-
flags: ACC_PUBLIC
-
Code:
-
stack=1, locals=1, args_size=1
-
0: aload_0
-
1: invokespecial #8 // Method java/lang/Object."<init>":()V
-
4: return
-
-
public static void main(java.lang.String[]);
-
flags: ACC_PUBLIC, ACC_STATIC
-
Code:
-
stack=2, locals=2, args_size=1
-
0: getstatic #16 // Field java/lang/System.out:Ljava/io/PrintStream;
-
3: ldc #18 // String Hello world!
-
5: invokevirtual #24 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
-
8: return
-
}
正是一个标准的 class 格式的文件, 它和以下源码是对应的:
-
public class Example {
-
-
public static void main (String[] args) {
-
System.out.println("Hello world!");
-
}
只是, 上面的 class 文件不是由这段源代码生成的, 而是使用 ASM 动态创建的。
ASM 示例二: 生成字段, 并给字段加注解
-
public class BeanTest extends ClassLoader implements Opcodes {
-
-
/*
-
* 生成以下类的字节码
-
*
-
* public class Person {
-
*
-
* @NotNull
-
* public String name;
-
*
-
* }
-
*/
-
-
public static void main(String[] args) throws Exception {
-
-
/********************************class***********************************************/
-
-
// 创建一个ClassWriter, 以生成一个新的类
-
-
ClassWriter cw = new ClassWriter(0);
-
cw.visit(V1_6, ACC_PUBLIC, "com/pansoft/espdb/bean/Person", null, "java/lang/Object", null);
-
-
-
-
/*********************************constructor**********************************************/
-
-
MethodVisitor mw = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null,
-
null);
-
mw.visitVarInsn(ALOAD, 0);
-
mw.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V");
-
mw.visitInsn(RETURN);
-
mw.visitMaxs(1, 1);
-
mw.visitEnd();
-
-
-
/*************************************field******************************************/
-
-
//生成String name字段
-
FieldVisitor fv = cw.visitField(ACC_PUBLIC, "name", "Ljava/lang/String;", null, null);
-
AnnotationVisitor av = fv.visitAnnotation("LNotNull;", true);
-
av.visit("value", "abc");
-
av.visitEnd();
-
fv.visitEnd();
-
-
-
-
/***********************************generate and load********************************************/
-
-
byte[] code = cw.toByteArray();
-
-
BeanTest loader = new BeanTest();
-
Class<?> clazz = loader.defineClass(null, code, 0, code.length);
-
-
-
/***********************************test********************************************/
-
-
Object beanObj = clazz.getConstructor().newInstance();
-
-
clazz.getField("name").set(beanObj, "zhangjg");
-
-
String nameString = (String) clazz.getField("name").get(beanObj);
-
System.out.println("filed value : " + nameString);
-
-
String annoVal = clazz.getField("name").getAnnotation(NotNull.class).value();
-
System.out.println("annotation value: " + annoVal);
-
-
}
-
}
上面代码是完整的代码, 用于生成一个和以下代码相对应的 class:
-
public class Person {
-
-
-
public String name;
-
-
}
生成类和构造方法的部分就略过了, 和上面的示例是一样的。 下面看看字段和字段的注解是如何生成的。 相关逻辑如下:
-
FieldVisitor fv = cw.visitField(ACC_PUBLIC, "name", "Ljava/lang/String;", null, null);
-
AnnotationVisitor av = fv.visitAnnotation("LNotNull;", true);
-
av.visit("value", "abc");
-
av.visitEnd();
-
fv.visitEnd();
ClassWriter 的 visitField 方法, 用于定义一个字段。 对应 class 文件中的一个 filed_info 。
ClassWriter 的其他重要方法
-
//定义一个类
-
public void visit(
-
int version,
-
int access,
-
String name,
-
String signature,
-
String superName,
-
String[] interfaces)
-
-
-
//定义源文件相关的信息,对应class文件中的Source属性
-
public void visitSource(String source, String debug)
-
-
//以下两个方法定义内部类和外部类相关的信息, 对应class文件中的InnerClasses属性
-
public void visitOuterClass(String owner, String name, String desc)
-
-
public void visitInnerClass(
-
String name,
-
String outerName,
-
String innerName,
-
int access)
-
-
-
//定义class文件中的注解信息, 对应class文件中的RuntimeVisibleAnnotations属性或者RuntimeInvisibleAnnotations属性
-
public AnnotationVisitor visitAnnotation(String desc, boolean visible)
-
-
//定义其他非标准属性
-
public void visitAttribute(Attribute attr)
-
-
-
-
//定义一个字段, 返回的FieldVisitor用于生成字段相关的信息
-
public FieldVisitor visitField(
-
int access,
-
String name,
-
String desc,
-
String signature,
-
Object value)
-
-
-
//定义一个方法, 返回的MethodVisitor用于生成方法相关的信息
-
public MethodVisitor visitMethod(
-
int access,
-
String name,
-
String desc,
-
String signature,
-
String[] exceptions)
每个方法都是和 class 文件中的某部分数据相对应的, 如果对 class 文件的格式比较熟悉的话, 使用 ASM 生成一个简单的类, 还是很容易的。
总结
在上一篇文章 深入理解 Java Class 文件格式(一) 中, 介绍了 class 文件在整个 java 体系结构中的位置和作用, 并对 class 文件的整体格式做了说明, 介绍了其中的魔数和版本号的相关内容, 并对常量池做了概述。 在本文章, 继续介绍 class 文件中的其他内容。
class 文件中的特殊字符串
(1) 类的全限定名
(2) 描述符
基本数据类型和 void 类型 | 类型的对应字符 |
byte | B |
char | C |
double | D |
float | F |
int | I |
long | J |
short | S |
boolean | Z |
void | V |
“L” + 类型的全限定名 + “;”
若干个“[” + 数组中元素类型的对应字符串
(参数1类型 参数2类型 参数3类型 ...)返回值类型
方法描述符 | 方法声明 |
()I | int getSize() |
()Ljava/lang/String; | String toString() |
([Ljava/lang/String;)V | void main(String[] args) |
()V | void wait() |
(JI)V | void wait(long timeout, int nanos) |
(ZILjava/lang/String;II)Z | boolean regionMatches(boolean ignoreCase, int toOffset, String other, int ooffset, int len) |
([BII)I | int read(byte[] b, int off, int len ) |
()[[Ljava/lang/Object; | Object[][] getObjectArray() |
(3) 特殊方法的方法名
总结
常量池中各数据项类型详解
关于常量池的大概内容, 已经在 深入理解 Java Class 文件格式(一) 中讲解过了, 这篇文章中还介绍了常量池中的 11 种数据类型。 本文的任务是详细讲解这 11 种数据类型, 深度剖析源文件中的各种信息是以什么方式存放在常量池中的。
我们知道, 常量池中的数据项是通过索引来引用的, 常量池中的各个数据项之间也会相互引用。在这 11 中常量池数据项类型中, 有两种比较基础, 之所以说它们基础, 是因为这两种类型的数据项会被其他类型的数据项引用。 这两种数据类型就是 CONSTANT_Utf8 和 CONSTANT_NameAndType , 其中 CONSTANT_NameAndType 类型的数据项(CONSTANT_NameAndType_info)也会引用 CONSTANT_Utf8 类型的数据项(CONSTANT_Utf8_info) 。 与其他介绍常量池的书籍或其他资料不同, 本着循序渐进和先后分明的原则, 我们首先对这两种比较基本的类型的数据项进行介绍, 然后再依次介绍其他 9 中数据项。
(1) CONSTANT_Utf8_info
- 程序中的字符串常量
- 常量池所在当前类(包括接口和枚举)的全限定名
- 常量池所在当前类的直接父类的全限定名
- 常量池所在当前类型所实现或继承的所有接口的全限定名
- 常量池所在当前类型中所定义的字段的名称和描述符
- 常量池所在当前类型中所定义的方法的名称和描述符
- 由当前类所引用的类型的全限定名
- 由当前类所引用的其他类中的字段的名称和描述符
- 由当前类所引用的其他类中的方法的名称和描述符
- 与当前 class 文件中的属性相关的字符串, 如属性名等
总结一下, 其中有这么五类: 程序中的字符串常量, 类型的全限定名, 方法和字段的名称, 方法和字段的描述符, 属性相关字符串。 程序中的字符串常量不用多说了, 我们经常使用它们创建字符串对象, 属性相关的字符串, 等到讲到 class 中的属性信息(attibute)时自会提及。 方法和字段的名称也不用多说了 。 剩下的就是类型的全限定名,方法和字段的描述符, 这就是上篇文章中提及的 "特殊字符串", 不熟悉的同学可以先读一下上篇文章 深入理解 Java Class 文件格式(二) 。 还有一点需要说明, 类型的全限定名, 方法和字段的名称, 方法和字段的描述符, 可以是本类型中定义的, 也可能是本类中引用的其他类的。
-
package com.jg.zhang;
-
-
public class Programer extends Person {
-
-
static String company = "CompanyA";
-
-
static{
-
System.out.println("staitc init");
-
}
-
-
-
String position;
-
Computer computer;
-
-
public Programer() {
-
this.position = "engineer";
-
this.computer = new Computer();
-
}
-
-
public void working(){
-
System.out.println("coding...");
-
computer.working();
-
}
-
}
#2 = Utf8 com/jg/zhang/Programer //当前类的全限定名
#4 = Utf8 com/jg/zhang/Person //父类的全限定名
#5 = Utf8 company //company字段的名称
#6 = Utf8 Ljava/lang/String; //company和position字段的描述符
#7 = Utf8 position //position字段的名称
#8 = Utf8 computer //computer字段的名称
#9 = Utf8 Lcom/jg/zhang/Computer; //computer字段的描述符
#10 = Utf8 <clinit> //类初始化方法(即静态初始化块)的方法名
#11 = Utf8 ()V //working方法的描述符
#12 = Utf8 Code //Code属性的属性名
#14 = Utf8 CompanyA //程序中的常量字符串
#19 = Utf8 java/lang/System //所引用的System类的全限定名
#21 = Utf8 out //所引用的out字段的字段名
#22 = Utf8 Ljava/io/PrintStream; //所引用的out字段的描述符
#24 = Utf8 staitc init //程序中的常量字符串
#27 = Utf8 java/io/PrintStream //所引用的PrintStream类的全限定名
#29 = Utf8 println //所引用的println方法的方法名
#30 = Utf8 (Ljava/lang/String;)V //所引用的println方法的描述符
#31 = Utf8 LineNumberTable //LineNumberTable属性的属性名
#32 = Utf8 LocalVariableTable //LocalVariableTable属性的属性名
#33 = Utf8 <init> //当前类的构造方法的方法名
#41 = Utf8 com/jg/zhang/Computer //所引用的Computer类的全限定名
#45 = Utf8 this //局部变量this的变量名
#46 = Utf8 Lcom/jg/zhang/Programer; //局部变量this的描述符
#47 = Utf8 working //woking方法的方法名
#49 = Utf8 coding... //程序中的字符串常量
#52 = Utf8 SourceFile //SourceFile属性的属性名
#53 = Utf8 Programer.java //当前类所在的源文件的文件名
(2) CONSTANT_NameAndType 类型的数据项
-
package com.jg.zhang;
-
-
public class Person {
-
-
int age;
-
-
int getAge(){
-
return age;
-
}
-
}
#1 = Class #2 // com/jg/zhang/Person
#2 = Utf8 com/jg/zhang/Person
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 age
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Methodref #3.#11 // java/lang/Object."<init>":()V
#11 = NameAndType #7:#8 // "<init>":()V
#12 = Utf8 LineNumberTable
#13 = Utf8 LocalVariableTable
#14 = Utf8 this
#15 = Utf8 Lcom/jg/zhang/Person;
#16 = Utf8 getAge
#17 = Utf8 ()I
#18 = Fieldref #1.#19 // com/jg/zhang/Person.age:I
#19 = NameAndType #5:#6 // age:I
#20 = Utf8 SourceFile
#21 = Utf8 Person.java
常量池一共有 21 项, 我们可以看到, 一共有两个 CONSTANT_NameAndType_info 数据项, 分别是第 #11 项和第 #19 项, 其中第 #11 项的 CONSTANT_NameAndType_info 又引用了常量池中的第 #7 项和第 #8 项, 被引用的这两项都是 CONSTANT_Utf8_info , 它们中存储的字符串常量值分别是 <init> 和 ()V。 其实他们加起来表示的就是父类 Object 的构造方法。 那么这里为什么会是父类 Object 的构造方法而不是本类的构造方法呢? 这是因为类中定义的方法如果不被引用(也就是说在当前类中不被调用), 那么常量池中是不会有相应的 CONSTANT_NameAndType_info 与之对应的, 只有引用了一个方法, 才有相应的 CONSTANT_NameAndType_info 与之对应。 这也是为什么说 CONSTANT_NameAndType_info 是方法的符号引用的一部分的原因。 (这里提到一个新的概念, 叫做方法的符号引用, 这个概念会在后面的博客中进行讲解) 可以看到, 在源码存在两个方法, 分别是编译器默认添加的构造方法和我们自己定义的 getAge 方法, 因为并没有在源码中显示的调用这两个方法,所以在常量池中并不存在和这两个方法相对应的 CONSTANT_NameAndType_info 。 之所以会存在父类 Object 的构造方法对应的 CONSTANT_NameAndType_info , 是因为子类构造方法中会默认调用父类的无参数构造方法。 我们将常量中的其他信息去掉, 可以看得更直观:
-
int getAge(){
-
return age;
-
}
总结
- CONSTANT_Utf8_info
- CONSTANT_NameAndType_info 。
(3)CONSTANT_Integer_info
-
package com.jg.zhang;
-
-
public class TestInt {
-
-
void printInt(){
-
System.out.println(65535);
-
}
-
}
将上面的类生成的 class 文件反编译:
D:\Workspace\AndroidWorkspace\BlogTest\bin>javap -v -c -classpath . com.jg.zhang.TestInt
下面列出反编译的结果, 由于反编译结果较长, 我们省略了大部分信息:
..................
..................
Constant pool:
..................
..................
#21 = Integer 65535
..................
..................
{
..................
..................
void printInt();
flags:
Code:
stack=2, locals=1, args_size=1
0: getstatic #15 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #21 // int 65535
5: invokevirtual #22 // Method java/io/PrintStream.println:(I)V
8: return
LineNumberTable:
line 6: 0
line 7: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcom/jg/zhang/TestInt;
}
上面的输出结果中, 保留了 printInt 方法的反编译结果, 并且保留了常量池中的第 21 项。 首先看 printInt 方法反编译结果中的索引为 3 的字节码指令:
3: ldc #21 // int 65535
这条 ldc 指令, 引用了常量池中的第 21 项, 而第 21 项是一个 CONSTANT_Integer_info, 并且这个 CONSTANT_Integer_info 存储的整型值为 65535 。
(4)CONSTANT_Float_info
-
void printFloat(){
-
System.out.println(1234.5f);
-
}
那么在这个类的常量池中就会有一个 CONSTANT_Float_info 与之相对应, 这个 CONSTANT_Float_info 的形式如下:
Constant pool:
.............
.............
#29 = Float 1234.5f
............
............
{
............
............
void printFloat();
flags:
Code:
stack=2, locals=1, args_size=1
0: getstatic #15 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #29 // float 1234.5f
5: invokevirtual #30 // Method java/io/PrintStream.println:(F)V
8: return
LineNumberTable:
line 10: 0
line 11: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcom/jg/zhang/TestInt;
}
(5)CONSTANT_Long_info
-
void printLong(){
-
System.out.println(123456L);
-
}
Constant pool:
..............
..............
#21 = Long 123456l
..............
..............
{
..............
..............
void printLong();
flags:
Code:
stack=3, locals=1, args_size=1
0: getstatic #15 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc2_w #21 // long 123456l
6: invokevirtual #23 // Method java/io/PrintStream.println:(J)V
9: return
LineNumberTable:
line 7: 0
line 8: 9
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lcom/jg/zhang/TestInt;
}
(6)CONSTANT_Double_info
-
void printDouble(){
-
System.out.println(123456D);
-
}
Constant pool:
..............
..............
#21 = Double 123456.0d
..............
..............
{
..............
..............
void printDouble();
flags:
Code:
stack=3, locals=1, args_size=1
0: getstatic #15 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc2_w #21 // double 123456.0d
6: invokevirtual #23 // Method java/io/PrintStream.println:(D)V
9: return
LineNumberTable:
line 7: 0
line 8: 9
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lcom/jg/zhang/TestInt;
}
(7) CONSTANT_String_info
-
void printStrng(){
-
System.out.println("abcdef");
-
}
Constant pool:
..............
..............
#21 = String #22 // abcdef
#22 = Utf8 abcdef
..............
..............
{
..............
..............
void printStrng();
flags:
Code:
stack=2, locals=1, args_size=1
0: getstatic #15 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #21 // String abcdef
5: invokevirtual #23 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 7: 0
line 8: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcom/jg/zhang/TestInt;
}
其中 printString 方法中索引为 3 的字节码指令 ldc 引用常量池中的第 21 项, 第 21 项是一个 CONSTANT_String_info, 这个位于第 21 项的 CONSTANT_String_info 又引用了常量池的第 22 项, 第 22 项是一个 CONSTANT_Utf8_info, 这个 CONSTANT_Utf8_info 中存储的字符串是 abcdef 。 引用关系的内存布局如下:
总结
- CONSTANT_Class_info
- CONSTANT_Fieldref_info
- CONSTANT_Methodref_info
- CONSTANT_InterfaceMethodref_info
本专栏的前几篇博文, 对 class 文件中的常量池进行了详细的解释。 前文讲解了常量池中的 7 种数据项, 它们分别是:
- CONSTANT_Utf8_info
- CONSTANT_NameAndType_info
- CONSTANT_Integer_info
- CONSTANT_Float_info
- CONSTANT_Long_info
- CONSTANT_Double_info
- CONSTANT_String_info
常量池中各数据项类型详解(续)
(8) CONSTANT_Class_info
(9) CONSTANT_Fieldref_info
-
package com.jg.zhang;
-
-
public class TestInt {
-
int a = 10;
-
void print(){
-
System.out.println(a);
-
}
-
}
在 print 方法中, 引用了本类中的字段 a。 代码很简单, 我们一眼就可以看到 print 方法中是如何引用本类中定义的字段 a 的。 那么在 class 文件中, 对字段 a 的引用是如何描述的呢? 下面我们将这段代码使用 javap 反编译, 给出简化后的反编译结果:
Constant pool:
#1 = Class #2 // com/jg/zhang/TestInt
#2 = Utf8 com/jg/zhang/TestInt
......
#5 = Utf8 a
#6 = Utf8 I
......
#12 = Fieldref #1.#13 // com/jg/zhang/TestInt.a:I
#13 = NameAndType #5:#6 // a:I
......
{
void print();
flags:
Code:
stack=2, locals=1, args_size=1
0: getstatic #19 // Field java/lang/System.out:Ljava/io/PrintStream;
3: aload_0
4: getfield #12 // Field a:I
7: invokevirtual #25 // Method java/io/PrintStream.println:(I)V
10: return
}
(10) CONSTANT_Methodref_info
-
package com.jg.zhang;
-
-
public class Programer {
-
-
Computer computer;
-
-
public Programer(Computer computer){
-
this.computer = computer;
-
}
-
-
public void doWork(){
-
computer.calculate();
-
}
-
}
-
package com.jg.zhang;
-
-
public class Computer {
-
-
public void calculate() {
-
System.out.println("working...");
-
-
}
-
}
-
Constant pool:
-
.........
-
-
-
#12 = Utf8 ()V
-
-
-
#20 = Methodref #21.#23 // com/jg/zhang/Computer.calculate:()V
-
#21 = Class #22 // com/jg/zhang/Computer
-
#22 = Utf8 com/jg/zhang/Computer
-
#23 = NameAndType #24:#12 // calculate:()V
-
#24 = Utf8 calculate
-
-
{
-
-
com.jg.zhang.Computer computer;
-
flags:
-
-
.........
-
-
public void doWork();
-
flags: ACC_PUBLIC
-
Code:
-
stack=1, locals=1, args_size=1
-
0: aload_0
-
1: getfield #13 // Field computer:Lcom/jg/zhang/Computer;
-
4: invokevirtual #20 // Method com/jg/zhang/Computer.calculate:()V
-
7: return
-
}
可以看到, doWork 方法的位置为 4 的字节码指令 invokevirtual 引用了索引为 20 的常量池数据项, 常量池中索引为 20 的数据项是一个 CONSTANT_Methodref_info, 这个 CONSTANT_Methodref_info 又引用了索引为 21 和 23 的两个数据项, 索引为 21 的数据项是一个 CONSTANT_Class_info, 这个 CONSTANT_Class_info 数据项又引用了索引为 22 的数据项, 索引为 22 的数据项是一个 CONSTANT_Utf8_info , 他存储了被引用的 Computer 类中的 calculate 方法所在的类的全限定名 com/jg/zhang/Computer 。 而 CONSTANT_Methodref_info 所引用的索引为 23 的数据项是一个 CONSTANT_NameAndType_info, 它又引用了两个数据项, 分别为第 24 项和第 12 项, 这是两个 CONSTANT_Utf8_info , 分别存储了被引用的方法 calculate 的方法名 calculate, 和该方法的描述符 () V 。
(11) CONSTANT_InterfaceMethodref_info
下面结合实际代码来说明, 代码如下:
-
package com.jg.zhang;
-
-
public class Plane {
-
-
IFlyable flyable;
-
-
void flyToSky(){
-
flyable.fly();
-
}
-
}
-
package com.jg.zhang;
-
-
public interface IFlyable {
-
-
void fly();
-
}
在上面的代码中, 定义可一个类 Plane, 在这个类中有一个 IFlyable 接口类型的字段 flyable, 然后在 Plane 的 flyToSky 方法中调用了 IFlyable 中的 fly 方法。 这就是源代码中对一个接口中的方法的引用方式, 下面我们反编译 Plane, 看看在 class 文件层面, 对一个接口中的方法的引用是如何描述的。
下面给出反编译结果, 为了简洁期间, 省略了一些不相关的内容:
Constant pool:
.........
#8 = Utf8 ()V
#19 = InterfaceMethodref #20.#22 // com/jg/zhang/IFlyable.fly:()V
#20 = Class #21 // com/jg/zhang/IFlyable
#21 = Utf8 com/jg/zhang/IFlyable
#22 = NameAndType #23:#8 // fly:()V
#23 = Utf8 fly
{
.........
com.jg.zhang.IFlyable flyable;
flags:
.........
void flyToSky();
flags:
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #17 // Field flyable:Lcom/jg/zhang/IFlyable;
4: invokeinterface #19, 1 // InterfaceMethod com/jg/zhang/IFlyable.fly:()V
9: return
}
可以看到, flyToSky 方法的位置为 4 的字节码指令 invokeinterface 引用了索引为 19 的常量池数据项, 常量池中索引为 19 的数据项是一个 CONSTANT_InterfaceMethodref_info, 这个 CONSTANT_InterfaceMethodref_info 又引用了索引为 20 和 22 的两个数据项, 索引为 20 的数据项是一个 CONSTANT_Class_info, 这个 CONSTANT_Class_info 数据项又引用了索引为 21 的数据项, 索引为 21 的数据项是一个 CONSTANT_Utf8_info , 他存储了被引用的方法 fly 所在的接口的全限定名 com/jg/zhang/IFlyable 。 而 CONSTANT_InterfaceMethodref_info 所引用的索引为 22 的数据项是一个 CONSTANT_NameAndType_info, 它又引用了两个数据项, 分别为第 23 项和第 8 项, 这是两个 CONSTANT_Utf8_info , 分别存储了被引用的方法 fly 的方法名 fly, 和该方法的描述符 () V 。
总结
经过前几篇文章, 终于将常量池介绍完了, 之所以花这么大的功夫介绍常量池, 是因为对于理解 class 文件格式,常量池是必须要了解的, 因为 class 文件中其他地方,大量引用了常量池中的数据项。 对于还不了解常量池的读者, 如果想要深入了解 class 文件格式, 或者想继续读这篇博客和本专栏以后的博客, 那么我建议先把我前面的几篇博客读一下,把常量池的结构熟悉一下, 对于理解后面的内容很有帮助。
虽然介绍完了常量池, 但是 class 文件中位于常量池下面的内容还有很多呢。 接下来, 我们就分析 class 文件中位于常量池下面的内容, 不用担心, 只要把常量池搞明白了, 这些内容就会很容易理解。
在开始进入正文之前, 在这里再次给出 class 文件的整体格式。 这个表格曾出现在 深入理解 Java Class 文件格式(一) 这篇文章中。 之所以把这个表格列在这里, 是想再次给读者一个 class 文件的总体概览。表格的内容如下:
类型 | 名称 | 数量 |
u4 | magic | 1 |
u2 | minor_version | 1 |
u2 | major_version | 1 |
u2 | constant_pool_count | 1 |
cp_info | constant_pool | constant_pool_count - 1 |
u2 | access_flags | 1 |
u2 | this_class | 1 |
u2 | super_class | 1 |
u2 | interfaces_count | 1 |
u2 | interfaces | interfaces_count |
u2 | fields_count | 1 |
field_info | fields | fields_count |
u2 | methods_count | 1 |
method_info | methods | methods_count |
u2 | attribute_count | 1 |
attribute_info | attributes | attributes_count |
下面我们就开始介绍 class 文件中的其他内容。
class 文件中的访问标志信息
标志名 | 标志值 | 标志含义 | 针对的对像 |
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 | 枚举类型 | 枚举 |
其他标志就不做介绍了, 这些标志都很简单。 读者感觉比较陌生的可能是 ACC_SUPER 这个标志。 读者会想, 类型不能被 super 关键字修饰啊, 那这个 ACC_SUPER 是做什么的呢?表中可以看出, 它的含义是:使用新的 invokespecial 语义 。 invokespecial 是一个字节码指令, 用于调用一个方法, 一般情况下, 调用构造方法或者使用 super 关键字显示调用父类的方法时, 会使用这条字节码指令。 这正是 ACC_SUPER 这个名字的由来。 在 java 1.2 之前, invokespecial 对方法的调用都是静态绑定的, 而 ACC_SUPER 这个标志位在 java 1.2 的时候加入到 class 文件中, 它为 invokespecial 这条指令增加了动态绑定的功能。 这里可能有几个概念读者不是很明白, 如静态绑定, 动态绑定等, 这些概念会在以后的博客中详细介绍。
class 文件中的 this_class
访问标志 access_flags 下面的两个字节叫做 this_class, 它是对当前类的描述。 它的两个字节的数据是对常量池中的一个 CONSTANT_Class_info 数据项的一个索引。 CONSTANT_Class_info 在上面的文章中已经介绍过了。 CONSTANT_Class_info 中有一个字段叫做 name_index , 指向一个 CONSTANT_Utf8_info , 在这个 CONSTANT_Utf8_info 中存放着当前类的全限定名。
-
package com.jg.zhang;
-
-
public class Person {
-
-
int age;
-
-
int getAge(){
-
return age;
-
}
-
}
将 Person.class 反编译后, 可以在常量池中看到如下两项:
-
Constant pool:
-
#1 = Class #2 // com/jg/zhang/Person
-
#2 = Utf8 com/jg/zhang/Person
-
-
.........
-
.........
class 文件中的 super_class
-
package com.jg.zhang;
-
-
public class Programer extends Person{
-
-
Computer computer;
-
-
public Programer(Computer computer){
-
this.computer = computer;
-
}
-
-
public void doWork(){
-
computer.calculate();
-
}
-
}
上面的 Programer 类继承自 Person 类。 那么反编译 Programer .class , 它的常量池中会存在如下信息:
-
Constant pool:
-
-
.........
-
.........
-
-
#3 = Class #4 // com/jg/zhang/Person
-
#4 = Utf8 com/jg/zhang/Person
这两项就是当前类的父类的信息。 其中索引为 3 的 CONSTANT_Class_info 会被 class 文件中的 super_class 引用。 下面给出示例图(其中虚线范围内表示常量池的区域):
class 文件中的 interfaces_count 和 interfaces
-
package com.jg.zhang;
-
-
public class Plane implements IFlyable, Cloneable{
-
-
-
public void fly() {
-
-
}
-
}
Plane 类实现了一个自定义的 IFlyable 接口, 还实现了一个 JDK 中的 Cloneable 接口, 那么它的常量池中会有如下信息:
-
Constant pool:
-
-
.........
-
.........
-
-
#5 = Class #6 // com/jg/zhang/IFlyable
-
#6 = Utf8 com/jg/zhang/IFlyable
-
#7 = Class #8 // java/lang/Cloneable
-
#8 = Utf8 java/lang/Cloneable
-
-
.........
-
.........
这四项数据就是当前的 Plane 类所实现的接口的信息。 第五项和第六项描述了 Plane 所实现的 IFlyable 接口, 第七项和第八项描述了 Plane 所实现的接口 Cloneable 接口。 下面是示意图(其中虚线范围内表示常量池的区域):
总结
本专栏列前面的一系列博客, 对 Class 文件中的一部分数据项进行了介绍。 本文将会继续介绍 class 文件中未讲解的信息。 先回顾一下上面一篇文章。 在上一篇博客中, 我们介绍了:
- this_class 对当前类的描述
- super_class 对当前类的超类的描述
- interfaces_count 当前类直接实现的接口的数量或当前接口直接继承的接口的数量
- interfaces 对当前类或当前接口直接实现或继承的所有接口的描述
详细信息请移步至上一篇博客 深入理解 Java Class 文件格式(六)。 更多关于 Java Class 文件和 JVM 的文章请关注我的专栏深入理解 Java 语言 。
下面继续介绍 class 文件中的其他信息。
class 文件中的 fields_count 和 fields
(1)access_flags
其中 access_flags 占两个字节, 描述的是字段的访问标志信息。 这里就不在详细介绍了, 下面给出一张表格(该表格来自《深入 Java 虚拟机》):
标志位名称 | 值 | 含义 | 设定者 |
ACC_PUBLIC | 0x0001 | 字段被设为 public | 类和接口 |
ACC_PRIVATE | 0x0002 | 字段被设为 private | 类 |
ACC_PROTECTED | 0x0004 | 字段被设为 protected | 类 |
ACC_STATIC | 0x0008 | 字段被设为 static | 类和接口 |
ACC_FINAL | 0x0010 | 字段被设为 final | 类和接口 |
ACC_VOLATILE | 0x0040 | 字段被设为 volatile | 类 |
ACC_TRANSIENT | 0x0080 | 字段被设为 transient | 类 |
(2)name_index
access_flags 下面的两个字节是 name_index, 这是一个指向常量池的索引, 它描述的是当前字段的字段名。 这个索引指向常量池中的一个 CONSTANT_Utf8_info 数据项。 这个 CONSTANT_Utf8_info 数据项中存放的字符串就是当前字段的字段名。
(3)descriptor_index
-
package com.jg.zhang;
-
-
public class Programer extends Person{
-
-
-
private Computer computer;
-
-
public Programer(Computer computer){
-
this.computer = computer;
-
}
-
-
public void doWork(){
-
computer.calculate();
-
}
-
}
-
Constant pool:
-
-
.........
-
.........
-
-
#5 = Utf8 computer
-
#6 = Utf8 Lcom/jg/zhang/Computer;
-
-
.........
-
.........
-
-
{
-
-
private com.jg.zhang.Computer computer;
-
flags: ACC_PRIVATE
-
-
.........
-
.........
-
-
}
从反编译的结果可以看出, 源文件中定义了一个 Computer 类型的字段 computer, 并且是 private 的。 然后常量池中有这个字段的字段名和描述符。 其中常量池第五项的 CONSTANT_Utf8_info 是字段名, 第六项的 CONSTANT_Utf8_info 是该字段的描述符。这里有一点需要说明, 在反编译 Programer.class 时,由于 computer 是私有的, 要加 - private 选项, 否则的话, 虽然常量池中有字段引用信息, 但是不会输出字段信息, 即下面这两行不会输出 :
-
private com.jg.zhang.Computer computer;
-
flags: ACC_PRIVATE
javap -c -v -private -classpath . com.jg.zhang.Programer
class 文件中的 methods_count 和 methods
(1)access_flags
其中 access_flags 占两个字节, 描述的是方法的访问标志信息。 这里就不在详细介绍了, 下面给出一张表格(该表格来自《深入 Java 虚拟机》):标志位名称 | 标志值 | 设定含义 | 设定者 |
ACC_PUBLIC | 0x0001 | 方法设为 public | 类和接口 |
ACC_PRIVATE | 0x0002 | 方法设为 private | 类 |
ACC_PROTECTED | 0x0004 | 方法设为 protected | 类 |
ACC_STATIC | 0x0008 | 方法设为 static | 类 |
ACC_FINAL | 0x0010 | 方法设为 final | 类 |
ACC_SYNCHRONIZED | 0x0020 | 方法设为 sychronized | 类 |
ACC_NATIVE | 0x0100 | 方法设为 native | 类 |
ACC_ABSTRACT | 0x0400 | 方法设为 abstract | 类和接口 |
ACC_STRICT | 0x0800 | 方法设为 strictFP | 类和接口的 <clinit> 方法 |
(2)name_index
access_flags 下面的两个字节是 name_index, 这是一个指向常量池的索引, 它描述的是当前方法的方法名。 这个索引指向常量池中的一个 CONSTANT_Utf8_info 数据项。 这个 CONSTANT_Utf8_info 数据项中存放的字符串就是当前方法的方法名。
(3)descriptor_index
-
package com.jg.zhang;
-
-
public class Programer extends Person{
-
-
-
private Computer computer;
-
-
public Programer(Computer computer){
-
this.computer = computer;
-
}
-
-
public void doWork(){
-
computer.calculate();
-
}
-
}
反编译之后, 常量池中会有如下信息(这里省略了大部分无关信息):
-
Constant pool:
-
-
.........
-
-
#7 = Utf8 <init>
-
#8 = Utf8 (Lcom/jg/zhang/Computer;)V
-
-
.........
-
-
#12 = Utf8 ()V
-
-
.........
-
-
#19 = Utf8 doWork
-
-
{
-
-
.........
-
-
public com.jg.zhang.Programer(com.jg.zhang.Computer);
-
flags: ACC_PUBLIC
-
-
.........
-
-
public void doWork();
-
flags: ACC_PUBLIC
-
-
.........
-
}
由反编译结果可以看出, 该类中定义了两个方法, 其中一个是构造方法, 一个是 doWork 方法, 且这两个方法都是 public 的。 这两个方法的描述信息都存放在常量池。 其中第 7 项的 CONSTANT_Utf8_info 为构造方法的方法名, 第 8 项的 CONSTANT_Utf8_info 为构造方法的方法描述符, 第 19 项的 CONSTANT_Utf8_info 为 doWork 方法的方法名, 第 12 项的 CONSTANT_Utf8_info 为 doWork 方法的方法描述符。
总结
在本专栏的第一篇文章 深入理解 Java 虚拟机到底是什么 中, 我们主要讲解了什么是虚拟机, 这篇博客是对 JVM 的一个概述。 在随后的几篇文章中,一直在讲解 class 文件格式。 在今天这篇博客中, 将会继续讲解 class 文件中的其他信息。 在本文中, 将会讲解 class 文件中的最后一部分, 属性(attributes) 。 这里的属性和源文件中的属性不是一个概念。 在源文件中, 我们把在类中定义的字段也叫做属性。 而 class 文件中的属性, 可以看做是存储一些额外信息的数据结构。 下面我们就来介绍属性。
class 文件中的 attributes_count 和 attributes
ClassFile 中的 SourceFile 属性
-
package com.jg.zhang;
-
-
public class Person {
-
-
int age;
-
-
int getAge(){
-
return age;
-
}
-
}
反编译后的相关信息:
-
public class com.jg.zhang.Person
-
-
SourceFile: "Person.java"
-
-
Constant pool:
-
-
.........
-
-
#20 = Utf8 SourceFile
-
#21 = Utf8 Person.java
-
-
.........
反编译结果中的 SourceFile: "Person.java" 一行是 SourceFile 属性的简单表示形式。 可以把它看做一个可读的 attribute_info 。 下面常量池中的第 20 项的 CONSTANT_Utf8_info 是对这个属性的属性名(attribute_name_index)的描述 , 第 21 项的 CONSTANT_Utf8_info 是对源文件的文件名的描述。
ClassFile 中的 InnerClasses 属性
Synthetic 属性
-
package com.jg.zhang;
-
-
public class Person {
-
-
static{
-
System.out.println("static");
-
-
}
-
-
int age;
-
-
int getAge(){
-
return age;
-
}
-
}
反编译后的相关信息如下:
-
{
-
int age;
-
flags:
-
-
-
static {};
-
-
.........
-
-
public com.jg.zhang.Person();
-
-
.........
-
-
int getAge();
-
-
.........
-
}
由反编译结果可以看出, 编译器自动生成了静态初始化方法和构造方法。 可能是因为 Synthetic 属性是可选的(也就是说某个版本的编译器可以选择不加入 Synthetic 属性) ,所以在反编译后的结果中没有发现 Synthetic 属性。
ConstantValue 属性
-
package com.jg.zhang;
-
-
public class Person {
-
-
static final int a = 1;
-
-
int age;
-
-
int getAge(){
-
return age;
-
}
-
}
反编译后的相关结果如下:
-
......
-
-
Constant pool:
-
-
#7 = Utf8 ConstantValue
-
#8 = Integer 1
-
-
-
{
-
static final int a;
-
flags: ACC_STATIC, ACC_FINAL
-
ConstantValue: int 1
-
-
.........
-
}
可以看到, 源文件中的 a 字段, 是 static final 的, 所以编译器为这个字段的 filed_info 生成了 ConstantValue 属性。 这个属性的示意图如下所示, 注意, 虚线范围内表示常量池区域:
-
package com.jg.zhang;
-
-
public class Person {
-
-
int age;
-
-
-
int getAge(){
-
return age;
-
}
-
}
在 getAge 方法上使用了 @deprecated 。 下面是反编译之后的相关信息:
-
......
-
-
Constant pool:
-
......
-
-
#18 = Utf8 Deprecated
-
-
......
-
-
{
-
-
......
-
-
int getAge();
-
flags:
-
Deprecated: true
-
-
......
-
-
}
可以看到, 在 getAge 方法相关的信息中, 有一行 Deprecated: true , 这说明编译器在 getAge 方法的 method_info 中加入了 Deprecated 属性。 常量池第 18 项的 CONSTANT_Utf8_info 中存放的是 Deprecated 属性的属性名 “Deprecated” 。
总结
经过前八篇关于 class 文件的博客, 关于 class 文件格式的内容也基本上讲完了。 本文是关于 class 文件格式的最后一篇。 在这篇博客中, 将会讲解关于方法的几个属性。 理解这篇博客的内容, 对于理解 JVM 执行引擎起着重要作用。 关于虚拟机执行引擎有关的内容, 会在本专栏后面的博客中涉及。
在前面几篇博客中, 我们知道在 class 文件中描述一个方法, 会使用一个 method_info 。 这个 method_info 中存放了方法的修饰符标志位,还引用了常量池中的项, 这些常量池数据项描述了在当前类中定义的某个方法的方法名, 方法的描述符。 关于这部分的内容, 请参考我之前的博客:深入理解 Java Class 文件格式(七) 。
但是 method_info 中并没有存放方法的字节码, 也就是指令。 我们知道, 对于一个方法来说, 只要它不是抽象的(抽象类中的抽象方法或者接口中的方法), 那么肯定就会存在指令。 那么这些指令存放在哪里呢? 还有, 方法中的异常处理器(try-catch 块)是如何在 class 文件中表述的? 方法声明抛出的异常是如何描述的呢? 如果你对这几个问题感兴趣, 或许你会在这篇博客中找到答案, 或者受到一些启发。
为了知识的连贯性, 我们首先简单回顾一下 method_info 的结构, 因为 method_info 与本文有着密切的关系。method_info 的结构如下:
深入理解 Java Class 文件格式(七)这篇博客中已经讲解过 access_flags , name_index, descriptor_index 。 他们分别描述方法的访问修饰符, 方法名和方法描述符。 从上图可以看出, method_info 中还有 attributes_count 和 attributes。 也就是说每个方法可以有另个或多个属性。 本文要讲解的方法中的字节码指令, 异常处理器和方法声明抛出的异常, 都存放在这些属性中。
Code 属性
LineNumberTable 属性
LocalVariableTable 属性
Exceptions 属性
总结
-
package com.jg.zhang;
-
-
public class Test {
-
-
public void test() throws Exception{
-
-
int localVar = 0;
-
-
try{
-
-
Class.forName("com.jg.zhang.Person");
-
-
}catch(ClassNotFoundException e){
-
-
throw e;
-
}finally{
-
System.out.println(localVar);
-
}
-
-
}
-
}
反编译后的 test 方法部分(省略了常量池等信息):
-
public void test() throws java.lang.Exception;
-
flags: ACC_PUBLIC
-
Exceptions:
-
throws java.lang.Exception
-
Code:
-
stack=2, locals=4, args_size=1
-
0: iconst_0
-
1: istore_1
-
2: ldc #18 // String com.jg.zhang.Person
-
4: invokestatic #20 // Method java/lang/Class.forName:(Ljava/lang/String;)Ljava/lang/Class;
-
7: pop
-
8: goto 24
-
11: astore_2
-
12: aload_2
-
13: athrow
-
14: astore_3
-
15: getstatic #26 // Field java/lang/System.out:Ljava/io/PrintStream;
-
18: iload_1
-
19: invokevirtual #32 // Method java/io/PrintStream.println:(I)V
-
22: aload_3
-
23: athrow
-
24: getstatic #26 // Field java/lang/System.out:Ljava/io/PrintStream;
-
27: iload_1
-
28: invokevirtual #32 // Method java/io/PrintStream.println:(I)V
-
31: return
-
Exception table:
-
from to target type
-
2 8 11 Class java/lang/ClassNotFoundException
-
2 14 14 any
-
LineNumberTable:
-
line 7: 0
-
line 11: 2
-
line 13: 8
-
line 15: 12
-
line 16: 14
-
line 17: 15
-
line 18: 22
-
line 17: 24
-
line 20: 31
-
LocalVariableTable:
-
Start Length Slot Name Signature
-
0 32 0 this Lcom/jg/zhang/Test;
-
2 30 1 localVar I
-
12 2 2 e Ljava/lang/ClassNotFoundException;