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 文件:

 

 
  1. public class Example {
  2.  
  3. public static void main (String[] args) {
  4. System.out.println("Hello world!");
  5. }
 


但是这个 class 文件不能在开发时通过上面的源码来编译成, 而是要动态生成。 下面我们介绍如何使用 ASM 动态生成上述源码对应的字节码。

 

 

下面是代码示例(该实例来自于 ASM 官方的 sample):

 

 
  1. import java.io.FileOutputStream;
  2.  
  3. import org.objectweb.asm.ClassWriter;
  4. import org.objectweb.asm.MethodVisitor;
  5. import org.objectweb.asm.Opcodes;
  6.  
  7. public class Helloworld extends ClassLoader implements Opcodes {
  8.  
  9. public static void main(final String args[]) throws Exception {
  10.  
  11.  
  12. //定义一个叫做Example的类
  13. ClassWriter cw = new ClassWriter(0);
  14. cw.visit(V1_1, ACC_PUBLIC, "Example", null, "java/lang/Object", null);
  15.  
  16. //生成默认的构造方法
  17. MethodVisitor mw = cw.visitMethod(ACC_PUBLIC,
  18. "<init>",
  19. "()V",
  20. null,
  21. null);
  22.  
  23. //生成构造方法的字节码指令
  24. mw.visitVarInsn(ALOAD, 0);
  25. mw.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V");
  26. mw.visitInsn(RETURN);
  27. mw.visitMaxs(1, 1);
  28. mw.visitEnd();
  29.  
  30. //生成main方法
  31. mw = cw.visitMethod(ACC_PUBLIC + ACC_STATIC,
  32. "main",
  33. "([Ljava/lang/String;)V",
  34. null,
  35. null);
  36.  
  37. //生成main方法中的字节码指令
  38. mw.visitFieldInsn(GETSTATIC,
  39. "java/lang/System",
  40. "out",
  41. "Ljava/io/PrintStream;");
  42.  
  43. mw.visitLdcInsn("Hello world!");
  44. mw.visitMethodInsn(INVOKEVIRTUAL,
  45. "java/io/PrintStream",
  46. "println",
  47. "(Ljava/lang/String;)V");
  48. mw.visitInsn(RETURN);
  49. mw.visitMaxs(2, 2);
  50.  
  51. //字节码生成完成
  52. mw.visitEnd();
  53.  
  54. // 获取生成的class文件对应的二进制流
  55. byte[] code = cw.toByteArray();
  56.  
  57.  
  58. //将二进制流写到本地磁盘上
  59. FileOutputStream fos = new FileOutputStream("Example.class");
  60. fos.write(code);
  61. fos.close();
  62.  
  63. //直接将二进制流加载到内存中
  64. Helloworld loader = new Helloworld();
  65. Class<?> exampleClass = loader.defineClass("Example", code, 0, code.length);
  66.  
  67. //通过反射调用main方法
  68. exampleClass.getMethods()[0].invoke(null, new Object[] { null });
  69.  
  70.  
  71. }
  72. }
 


下面详细介绍生成 class 的过程:

 

 

1 首先定义一个类

 
相关代码片段如下:
 
 
  1. //定义一个叫做Example的类
  2. ClassWriter cw = new ClassWriter(0);
  3. cw.visit(V1_1, ACC_PUBLIC, "Example", null, "java/lang/Object", null);
 

ClassWriter 类是 ASM 中的核心 API , 用于生成一个类的字节码。 ClassWriter 的 visit 方法定义一个类。 
 
第一个参数 V1_1 是生成的 class 的版本号, 对应 class 文件中的主版本号和次版本号, 即 minor_version 和 major_version 。 
 
第二个参数 ACC_PUBLIC 表示该类的访问标识。这是一个 public 的类。 对应 class 文件中的 access_flags 。
 
第三个参数是生成的类的类名。 需要注意,这里是类的全限定名。 如果生成的 class 带有包名, 如 com.jg.zhang.Example, 那么这里传入的参数必须是 com/jg/zhang/Example  。对应 class 文件中的 this_class  。
 
第四个参数是和泛型相关的, 这里我们不关新, 传入 null 表示这不是一个泛型类。这个参数对应 class 文件中的 Signature 属性(attribute) 。
 
第五个参数是当前类的父类的全限定名。 该类直接继承 Object。 这个参数对应 class 文件中的 super_class 。 
 

第六个参数是 String [] 类型的, 传入当前要生成的类的直接实现的接口。 这里这个类没实现任何接口, 所以传入 null 。 这个参数对应 class 文件中的 interfaces 。 

 

2 定义默认构造方法, 并生成默认构造方法的字节码指令 

 
相关代码片段如下:
 
  1. //生成默认的构造方法
  2. MethodVisitor mw = cw.visitMethod(ACC_PUBLIC,
  3. "<init>",
  4. "()V",
  5. null,
  6. null);
  7.  
  8. //生成构造方法的字节码指令
  9. mw.visitVarInsn(ALOAD, 0);
  10. mw.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V");
  11. mw.visitInsn(RETURN);
  12. mw.visitMaxs(1, 1);
  13. mw.visitEnd();
 


使用上面创建的 ClassWriter 对象, 调用该对象的 visitMethod 方法, 得到一个 MethodVisitor 对象, 这个对象定义一个方法。 对应 class 文件中的一个 method_info 。 
 
 
第一个参数是 ACC_PUBLIC , 指定要生成的方法的访问标志。 这个参数对应 method_info 中的 access_flags 。 
 
第二个参数是方法的方法名。 对于构造方法来说, 方法名为 <init> 。 这个参数对应 method_info 中的 name_index , name_index 引用常量池中的方法名字符串。 
 
第三个参数是方法描述符, 在这里要生成的构造方法无参数, 无返回值, 所以方法描述符为 () V  。 这个参数对应 method_info 中的 descriptor_index 。 
 
第四个参数是和泛型相关的, 这里传入 null 表示该方法不是泛型方法。这个参数对应 method_info 中的 Signature 属性。
 
第五个参数指定方法声明可能抛出的异常。 这里无异常声明抛出, 传入 null 。 这个参数对应 method_info 中的 Exceptions 属性。
 
接下来调用 MethodVisitor 中的多个方法, 生成当前构造方法的字节码。 对应 method_info 中的 Code 属性。
 
1 调用 visitVarInsn 方法,生成 aload 指令, 将第 0 个本地变量(也就是 this)压入操作数栈。
 
2 调用 visitMethodInsn 方法, 生成 invokespecial 指令, 调用父类(也就是 Object)的构造方法。
 
3 调用 visitInsn 方法,生成 return 指令, 方法返回。 
 
4 调用 visitMaxs 方法, 指定当前要生成的方法的最大局部变量和最大操作数栈。 对应 Code 属性中的 max_stack 和 max_locals 。 
 
5 最后调用 visitEnd 方法, 表示当前要生成的构造方法已经创建完成。 
 
 

3 定义 main 方法, 并生成 main 方法中的字节码指令

 
对应的代码片段如下:
 
  1. mw = cw.visitMethod(ACC_PUBLIC + ACC_STATIC,
  2. "main",
  3. "([Ljava/lang/String;)V",
  4. null,
  5. null);
  6.  
  7. //生成main方法中的字节码指令
  8. mw.visitFieldInsn(GETSTATIC,
  9. "java/lang/System",
  10. "out",
  11. "Ljava/io/PrintStream;");
  12.  
  13. mw.visitLdcInsn("Hello world!");
  14. mw.visitMethodInsn(INVOKEVIRTUAL,
  15. "java/io/PrintStream",
  16. "println",
  17. "(Ljava/lang/String;)V");
  18. mw.visitInsn(RETURN);
  19. mw.visitMaxs(2, 2);
  20. mw.visitEnd();
 

这个过程和上面的生成默认构造方法的过程是一致的。 读者可对比上一步执行分析。
 
 

4 生成 class 数据, 保存到磁盘中, 加载 class 数据

 
对应代码片段如下:
 
  1. // 获取生成的class文件对应的二进制流
  2. byte[] code = cw.toByteArray();
  3.  
  4.  
  5. //将二进制流写到本地磁盘上
  6. FileOutputStream fos = new FileOutputStream("Example.class");
  7. fos.write(code);
  8. fos.close();
  9.  
  10. //直接将二进制流加载到内存中
  11. Helloworld loader = new Helloworld();
  12. Class<?> exampleClass = loader.defineClass("Example", code, 0, code.length);
  13.  
  14. //通过反射调用main方法
  15. exampleClass.getMethods()[0].invoke(null, new Object[] { null });
 

这段代码首先获取生成的 class 文件的字节流, 把它写在本地磁盘的 Example.class 文件中。 然后加载 class 字节流, 并通过反射调用 main 方法。
 
这段代码执行完, 可以看到控制台有以下输出:
Hello world!

然后在当前测试工程的根目录下, 生成一个 Example.class 文件文件。
 
 
下面我们使用 javap 反编译这个 class 文件:
javap -c -v -classpath . -private Example

输出的完整信息如下:
 
  1. Classfile /C:/Users/纪刚/Desktop/生成字节码/AsmJavaTest/Example.class
  2. Last modified 2014-4-5; size 338 bytes
  3. MD5 checksum 281abde0e2012db8ad462279a1fbb6a4
  4. public class Example
  5. minor version: 3
  6. major version: 45
  7. flags: ACC_PUBLIC
  8. Constant pool:
  9. #1 = Utf8 Example
  10. #2 = Class #1 // Example
  11. #3 = Utf8 java/lang/Object
  12. #4 = Class #3 // java/lang/Object
  13. #5 = Utf8 <init>
  14. #6 = Utf8 ()V
  15. #7 = NameAndType #5:#6 // "<init>":()V
  16. #8 = Methodref #4.#7 // java/lang/Object."<init>":()V
  17. #9 = Utf8 main
  18. #10 = Utf8 ([Ljava/lang/String;)V
  19. #11 = Utf8 java/lang/System
  20. #12 = Class #11 // java/lang/System
  21. #13 = Utf8 out
  22. #14 = Utf8 Ljava/io/PrintStream;
  23. #15 = NameAndType #13:#14 // out:Ljava/io/PrintStream;
  24. #16 = Fieldref #12.#15 // java/lang/System.out:Ljava/io/PrintStream;
  25. #17 = Utf8 Hello world!
  26. #18 = String #17 // Hello world!
  27. #19 = Utf8 java/io/PrintStream
  28. #20 = Class #19 // java/io/PrintStream
  29. #21 = Utf8 println
  30. #22 = Utf8 (Ljava/lang/String;)V
  31. #23 = NameAndType #21:#22 // println:(Ljava/lang/String;)V
  32. #24 = Methodref #20.#23 // java/io/PrintStream.println:(Ljava/lang/String;)V
  33. #25 = Utf8 Code
  34. {
  35. public Example();
  36. flags: ACC_PUBLIC
  37. Code:
  38. stack=1, locals=1, args_size=1
  39. 0: aload_0
  40. 1: invokespecial #8 // Method java/lang/Object."<init>":()V
  41. 4: return
  42.  
  43. public static void main(java.lang.String[]);
  44. flags: ACC_PUBLIC, ACC_STATIC
  45. Code:
  46. stack=2, locals=2, args_size=1
  47. 0: getstatic #16 // Field java/lang/System.out:Ljava/io/PrintStream;
  48. 3: ldc #18 // String Hello world!
  49. 5: invokevirtual #24 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
  50. 8: return
  51. }
 

正是一个标准的 class 格式的文件, 它和以下源码是对应的:
 
 
  1. public class Example {
  2.  
  3. public static void main (String[] args) {
  4. System.out.println("Hello world!");
  5. }
 

只是, 上面的 class 文件不是由这段源代码生成的, 而是使用 ASM 动态创建的。 
 
 
 

ASM 示例二: 生成字段, 并给字段加注解

 
上面的 HelloWorld 示例演示了如何生成类和方法, 该示例演示如何生成字段, 并给字段加注解。 
 
 
  1. public class BeanTest extends ClassLoader implements Opcodes {
  2.  
  3. /*
  4. * 生成以下类的字节码
  5. *
  6. * public class Person {
  7. *
  8. * @NotNull
  9. * public String name;
  10. *
  11. * }
  12. */
  13.  
  14. public static void main(String[] args) throws Exception {
  15.  
  16. /********************************class***********************************************/
  17.  
  18. // 创建一个ClassWriter, 以生成一个新的类
  19.  
  20. ClassWriter cw = new ClassWriter(0);
  21. cw.visit(V1_6, ACC_PUBLIC, "com/pansoft/espdb/bean/Person", null, "java/lang/Object", null);
  22.  
  23.  
  24.  
  25. /*********************************constructor**********************************************/
  26.  
  27. MethodVisitor mw = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null,
  28. null);
  29. mw.visitVarInsn(ALOAD, 0);
  30. mw.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V");
  31. mw.visitInsn(RETURN);
  32. mw.visitMaxs(1, 1);
  33. mw.visitEnd();
  34.  
  35.  
  36. /*************************************field******************************************/
  37.  
  38. //生成String name字段
  39. FieldVisitor fv = cw.visitField(ACC_PUBLIC, "name", "Ljava/lang/String;", null, null);
  40. AnnotationVisitor av = fv.visitAnnotation("LNotNull;", true);
  41. av.visit("value", "abc");
  42. av.visitEnd();
  43. fv.visitEnd();
  44.  
  45.  
  46.  
  47. /***********************************generate and load********************************************/
  48.  
  49. byte[] code = cw.toByteArray();
  50.  
  51. BeanTest loader = new BeanTest();
  52. Class<?> clazz = loader.defineClass(null, code, 0, code.length);
  53.  
  54.  
  55. /***********************************test********************************************/
  56.  
  57. Object beanObj = clazz.getConstructor().newInstance();
  58.  
  59. clazz.getField("name").set(beanObj, "zhangjg");
  60.  
  61. String nameString = (String) clazz.getField("name").get(beanObj);
  62. System.out.println("filed value : " + nameString);
  63.  
  64. String annoVal = clazz.getField("name").getAnnotation(NotNull.class).value();
  65. System.out.println("annotation value: " + annoVal);
  66.  
  67. }
  68. }
 

上面代码是完整的代码, 用于生成一个和以下代码相对应的 class:
 
  1. public class Person {
  2.  
  3. @NotNull
  4. public String name;
  5.  
  6. }
 

生成类和构造方法的部分就略过了, 和上面的示例是一样的。 下面看看字段和字段的注解是如何生成的。 相关逻辑如下:
 
 
  1. FieldVisitor fv = cw.visitField(ACC_PUBLIC, "name", "Ljava/lang/String;", null, null);
  2. AnnotationVisitor av = fv.visitAnnotation("LNotNull;", true);
  3. av.visit("value", "abc");
  4. av.visitEnd();
  5. fv.visitEnd();
 

ClassWriter 的 visitField 方法, 用于定义一个字段。 对应 class 文件中的一个 filed_info 。 
 
第一个参数是字段的访问修饰符, 这里传入 ACC_PUBLIC 表示是一个 public 的属性。 这个参数和 filed_info 中的 access_flags 相对应。
 
第二个参数是字段的字段名。 这个参数和 filed_info 中的 name_index 相对应。
 
第三个参数是字段的描述符, 这个字段是 String 类型的,它的字段描述符为 "Ljava/lang/String;" 。 这个参数和 filed_info 中的 descriptor_index 相对应。
 
第四个参数和泛型相关的, 这里传入 null, 表示该字段不是泛型的。 这个参数和 filed_info 中的 Signature 属性相对应。
 
第五个参数是字段的值, 只适用于静态字段,当前要生成的字段不是静态的, 所以传入 null 。 这个参数和 filed_info 中的 ConstantValue 属性相对应。
 
使用 visitField 方法定义完当前字段, 返回一个 FieldVisitor 对象。 下面调用这个对象的 visitAnnotation 方法, 为该字段生成注解信息。 visitAnnotation 的两个参数如下:
 
第一个参数是要生成的注解的描述符, 传入 "LNotNull;" 。
 
第二个参数表示该注解是否运行时可见。 如果传入 true, 表示运行时可见, 这个注解信息就会生成 filed_info 中的一个 RuntimeVisibleAnnotation 属性。 传入 false, 表示运行时不可见,个注解信息就会生成 filed_info 中的一个 RuntimeInvisibleAnnotation 属性 。 
 
接下来调用上一步返回的 AnnotationVisitor 对象的 visit 方法, 来生成注解的值信息。 
 
 

ClassWriter 的其他重要方法

 
ClassWriter 中还有其他一些重要方法, 这些方法能够生成 class 文件中的所有相关信息。 这些方法, 以及对象生成 class 文件中的什么信息, 都列在下面:
 
 
  1. //定义一个类
  2. public void visit(
  3. int version,
  4. int access,
  5. String name,
  6. String signature,
  7. String superName,
  8. String[] interfaces)
  9.  
  10.  
  11. //定义源文件相关的信息,对应class文件中的Source属性
  12. public void visitSource(String source, String debug)
  13.  
  14. //以下两个方法定义内部类和外部类相关的信息, 对应class文件中的InnerClasses属性
  15. public void visitOuterClass(String owner, String name, String desc)
  16.  
  17. public void visitInnerClass(
  18. String name,
  19. String outerName,
  20. String innerName,
  21. int access)
  22.  
  23.  
  24. //定义class文件中的注解信息, 对应class文件中的RuntimeVisibleAnnotations属性或者RuntimeInvisibleAnnotations属性
  25. public AnnotationVisitor visitAnnotation(String desc, boolean visible)
  26.  
  27. //定义其他非标准属性
  28. public void visitAttribute(Attribute attr)
  29.  
  30.  
  31.  
  32. //定义一个字段, 返回的FieldVisitor用于生成字段相关的信息
  33. public FieldVisitor visitField(
  34. int access,
  35. String name,
  36. String desc,
  37. String signature,
  38. Object value)
  39.  
  40.  
  41. //定义一个方法, 返回的MethodVisitor用于生成方法相关的信息
  42. public MethodVisitor visitMethod(
  43. int access,
  44. String name,
  45. String desc,
  46. String signature,
  47. String[] exceptions)
 

每个方法都是和 class 文件中的某部分数据相对应的, 如果对 class 文件的格式比较熟悉的话, 使用 ASM 生成一个简单的类, 还是很容易的。
 
 

总结

 
在本文中, 通过使用开源的 ASM 库, 动态生成了两个类。 通过讲解这两个类的生成过程, 可以加深对 class 文件格式的理解。 因为 ASM 库中的每个 API 都是对应 class 文件中的某部分信息的。 如果对 class 文件格式不熟悉, 可以参考本专栏之前的讲解 class 文件格式的一系列博客。 
 
本文使用的两个示例都放在了一个单独的, 可直接运行的工程中, 该工程已经上传到我的百度网盘, 这个工程的 lib 目录中, 有 ASM 4.0 的 jar 包。 和该工程一起打包的, 还有 ASM 4.0 的源码和示例程序。 
 

在上一篇文章 深入理解 Java Class 文件格式(一) 中, 介绍了 class 文件在整个 java 体系结构中的位置和作用, 并对 class 文件的整体格式做了说明, 介绍了其中的魔数和版本号的相关内容, 并对常量池做了概述。 在本文章, 继续介绍 class 文件中的其他内容。

 

class 文件中的特殊字符串

 
首先说明一下, 所谓的特殊字符串出现在 class 文件中的常量池中, 所以在上一篇博客中, 只是对常量池介绍了一个大概。 本着循序渐进和减少跨度的原则, 首先把 class 文件中的特殊字符串做一个详细的介绍, 然后再回过头来继续讲解常量池。 
 
在上文中, 我们提到特殊字符串是常量池中符号引用的一部分, 至于符号引用的概念, 会在以后提到。 现在我们将重点放在特殊字符串上。 特殊字符串包括三种: 类的全限定名, 字段和方法的描述符, 特殊方法的方法名。 下面我们就分别介绍这三种特殊字符串。
 

(1) 类的全限定名

 
在常量池中, 一个类型的名字并不是我们在源文件中看到的那样, 也不是我们在源文件中使用的包名加类名的形式。 源文件中的全限定名和 class 文件中的全限定名不是相同的概念。 源文件中的全新定名是包名加类名, 包名的各个部分之间,包名和类名之间, 使用点号分割。 如 Object 类, 在源文件中的全限定名是 java.lang.Object 。 而 class 文件中的全限定名是将点号替换成 “/” 。 例如, Object 类在 class 文件中的全限定名是 java/lang/Object 。 如果读者之前没有接触过 class 文件格式, 是 class 文件格式的初学者, 在这里不必知道全限定名在 class 文件中是如何使用的, 只需要知道, 源文件中一个类的名字, 在 class 文件中是用全限定名表述的。 
 

(2) 描述符

 
我们知道在一个类中可以有若干字段和方法, 这些字段和方法在源文件中如何表述, 我们再熟悉不过了。 既然现在我们要学习 class 文件格式, 那么我们就要问, 一个字段或一个方法在 class 文件中是如何表述的? 在本文中, 我们会讨论方法和字段在 class 文件中的描述。 方法和字段的描述符并不会把方法和字段的所有信息全都描述出来, 毕竟描述符只是一个简单的字符串。 
 
在讲解描述符之前, 要先说明一个问题, 那就是所有的类型在描述符中都有对应的字符或字符串来对应。 比如, 每种基本数据类型都有一个大写字母做对应, void 也有一个大写字符做对应。 下表是 void 和基本数据类型在描述符中的对应。
 
基本数据类型和 void 类型 类型的对应字符
byte B
char C
double D
float F
int I
long J
short S
boolean Z
void V
 
基本上都是以类型的首字符变成大写来对应的, 其中 long 和 boolean 是特例, long 类型在描述符中的对应字符是 J, boolean 类型在描述符中的对应字符是 Z 。 
 
基本类型和 void 在描述符中都有一个大写字符和他们对应, 那么引用类型(类和接口,枚举)在描述符中是如何对应的呢? 引用类型的对应字符串(注意, 引用类型在描述符中使用一个字符串做对应) , 这个字符串的格式是:
 
 “L” + 类型的全限定名 + “;”
 
注意,这三个部分之间没有空格, 是紧密排列的。 如 Object 在描述符中的对应字符串是: Ljava/lang/Object;  ; ArrayList 在描述符中的对应字符串是: Ljava/lang/ArrayList;  ; 自定义类型 com.example.Person 在描述符中的对应字符串是: Lcom/example/Person; 。
 
我们知道, 在 Java 语言中数组也是一种类型, 一个数组的元素类型和他的维度决定了他的类型。 比如, 在 int [] a 声明中, 变量 a 的类型是 int [] , 在 int [][] b 声明中, 变量 b 的类型是 int [][] , 在 Object [] c 声明中, 变量 c 的类型是 Object [] 。既然数组是类型, 那么在描述符中, 也应该有数组类型的对应字符串。 在 class 文件的描述符中, 数组的类型中每个维度都用一个 [ 代表, 数组类型整个类型的对应字符串的格式如下:
 
若干个“[”  +  数组中元素类型的对应字符串 
 
下面举例来说名。 int [] 类型的对应字符串是: [I  。 int [][] 类型的对应字符串是: [[I 。 Object [] 类型的对应字符串是: [Ljava/lang/Object; 。 Object [][][] 类型的对应字符串是: [[[Ljava/lang/Object; 。
 
介绍完每种类型在描述符中的对应字符串, 下面就开始讲解字段和方法的描述符。 
 
字段的描述符就是字段的类型所对应的字符或字符串。 如: int i 中, 字段 i 的描述符就是 I 。 Object o 中, 字段 o 的描述符就是 Ljava/lang/Object;  。 double [][] d 中, 字段 d 的描述符就是 [[D 。 
 
方法的描述符比较复杂, 包括所有参数的类型列表和方法返回值。 它的格式是这样的:
 
   (参数1类型 参数2类型 参数3类型 ...)返回值类型
 
其中, 不管是参数的类型还是返回值类型, 都是使用对应字符和对应字符串来表示的, 并且参数列表使用小括号括起来, 并且各个参数类型之间没有空格, 参数列表和返回值类型之间也没有空格。 
 
下面举例说明(此表格来源于《深入 Java 虚拟机》)。
 
方法描述符 方法声明
()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) 特殊方法的方法名

 
首先要明确一下, 这里的特殊方法是指的类的构造方法和类型初始化方法。 构造方法就不用多说了, 至于类型的初始化方法, 对应到源码中就是静态初始化块。 也就是说, 静态初始化块, 在 class 文件中是以一个方法表述的, 这个方法同样有方法描述符和方法名。 
 
类的构造方法的方法名使用字符串 <init> 表示, 而静态初始化方法的方法名使用字符串 <clinit> 表示。 除了这两种特殊的方法外, 其他普通方法的方法名, 和源文件中的方法名相同。

总结

 
到此为止, 关于特殊字符串就讲解完了。 最后做一下总结:
 
class 文件中的特殊字符串包括类(包括接口, 枚举)的全限定名, 字段的描述符和方法的描述符。 其中类的全限定名比较简单易于理解, 字段和方法的描述符由于涉及到每种类型的映射, 可能稍显复杂。 要理解描述符, 主要是要熟记每种类型(包括 8 种基本数据类型,类类型, 数组类型和 void)在描述符中所对应的描述字符或字符串。 
 
还有一点需要注意, 就是方法和字段的描述符中, 不包括字段名和方法名, 字段描述符中只包括字段类型, 方法描述符中只包括参数列表和返回值类型。 
 
本文只是介绍 class 中的特殊字符串, 关于在 class 文件中如何使用这些字符串, 将会在下一篇博客中讲解, 敬请关注。 
首先, 让我们回顾一下关于 class 文件格式的之前两篇博客的主要内容。 在 深入理解 Java Class 文件格式(一) 中, 讲解了 class 文件在整个 java 体系结构中的位置和作用, 讲解了 class 文件中的魔数和版本号相关的信息, 并且对常量池进行了概述。 在 深入理解 Java Class 文件格式(二) 中, 主要讲解了 class 文件中的特殊字符串, 包括类的全限定名, 字段描述符和方法描述符, 这些特殊字符串大量出现在 class 文件的常量池中, 是理解常量池的基础。 本文会详细讲解常量池中的各个数据项。
 
如果你还没有读过前两篇文章, 建议先去读一下, 这样才能保持知识的连贯性。 前两篇文章的链接已经在上面给出。 下面开始讲解常量池。
 
 

常量池中各数据项类型详解

 

关于常量池的大概内容, 已经在 深入理解 Java Class 文件格式(一) 中讲解过了, 这篇文章中还介绍了常量池中的 11 种数据类型。 本文的任务是详细讲解这 11 种数据类型, 深度剖析源文件中的各种信息是以什么方式存放在常量池中的。 

 

我们知道, 常量池中的数据项是通过索引来引用的, 常量池中的各个数据项之间也会相互引用。在这 11 中常量池数据项类型中, 有两种比较基础, 之所以说它们基础, 是因为这两种类型的数据项会被其他类型的数据项引用。 这两种数据类型就是 CONSTANT_Utf8 和 CONSTANT_NameAndType , 其中 CONSTANT_NameAndType 类型的数据项(CONSTANT_NameAndType_info)也会引用 CONSTANT_Utf8 类型的数据项(CONSTANT_Utf8_info) 。 与其他介绍常量池的书籍或其他资料不同, 本着循序渐进和先后分明的原则, 我们首先对这两种比较基本的类型的数据项进行介绍, 然后再依次介绍其他 9 中数据项。 

 

 

(1) CONSTANT_Utf8_info

 
一个 CONSTANT_Utf8_info 是一个 CONSTANT_Utf8 类型的常量池数据项, 它存储的是一个常量字符串。 常量池中的所有字面量几乎都是通过 CONSTANT_Utf8_info 描述的。下面我们首先讲解 CONSTANT_Utf8_info 数据项的存储格式。在前面的文章中, 我们提到, 常量池中数据项的类型由一个整型的标志值(tag)决定, 所以所有常量池类型的 info 中都必须有一个 tag 信息, 并且这个 tag 值位于数据项的第一个字节上。 一个 11 中常量池数据类型, 所以就有 11 个 tag 值表示这 11 中类型。而 CONSTANT_Utf8_info 的 tag 值为 1, 也就是说如果虚拟机要解析一个常量池数据项, 首先去读这个数据项的第一个字节的 tag 值, 如果这个 tag 值为 1, 那么就说明这个数据项是一个 CONSTANT_Utf8 类型的数据项。 紧挨着 tag 值的两个字节是存储的字符串的长度 length, 剩下的字节就存储着字符串。 所以, 它的格式是这样的:
其中 tag 占一个字节, length 占 2 个字节, bytes 代表存储的字符串, 占 length 字节。所以, 如果这个 CONSTANT_Utf8_info 存储的是字符串 "Hello", 那么他的存储形式是这样的:
现在我们知道了 CONSTANT_Utf8_info 数据项的存储形式, 那么 CONSTANT_Utf8_info 数据项都存储了什么字符串呢? CONSTANT_Utf8_info 可包括的字符串主要以下这些:
  • 程序中的字符串常量
  • 常量池所在当前类(包括接口和枚举)的全限定名
  • 常量池所在当前类的直接父类的全限定名
  • 常量池所在当前类型所实现或继承的所有接口的全限定名
  • 常量池所在当前类型中所定义的字段的名称和描述符
  • 常量池所在当前类型中所定义的方法的名称和描述符
  • 由当前类所引用的类型的全限定名
  • 由当前类所引用的其他类中的字段的名称和描述符
  • 由当前类所引用的其他类中的方法的名称和描述符
  • 与当前 class 文件中的属性相关的字符串, 如属性名等

总结一下, 其中有这么五类: 程序中的字符串常量, 类型的全限定名, 方法和字段的名称, 方法和字段的描述符, 属性相关字符串。 程序中的字符串常量不用多说了, 我们经常使用它们创建字符串对象, 属性相关的字符串, 等到讲到 class 中的属性信息(attibute)时自会提及。 方法和字段的名称也不用多说了 。 剩下的就是类型的全限定名,方法和字段的描述符, 这就是上篇文章中提及的 "特殊字符串", 不熟悉的同学可以先读一下上篇文章 深入理解 Java Class 文件格式(二) 。 还有一点需要说明, 类型的全限定名, 方法和字段的名称, 方法和字段的描述符, 可以是本类型中定义的, 也可能是本类中引用的其他类的。 

下面我们通过一个例子来进行说明。 示例源码:
 
  1. package com.jg.zhang;
  2.  
  3. public class Programer extends Person {
  4.  
  5. static String company = "CompanyA";
  6.  
  7. static{
  8. System.out.println("staitc init");
  9. }
  10.  
  11.  
  12. String position;
  13. Computer computer;
  14.  
  15. public Programer() {
  16. this.position = "engineer";
  17. this.computer = new Computer();
  18. }
  19.  
  20. public void working(){
  21. System.out.println("coding...");
  22. computer.working();
  23. }
  24. }
 


别看这个类简单, 但是反编译后, 它的常量池有 53 项之多。 在这 53 项常量池数据项中, 各种类型的数据项都有, 当然也包括不少的 CONSTANT_Utf8_info 。 下面只列出反编译后常量池中的 CONSTANT_Utf8_info 数据项:
#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                  //当前类所在的源文件的文件名
 
上面只列出了反编译结果中常量池中的 CONSTANT_Utf8_info 数据项。 其中第三列不是 javap 反编译的输出结果, 而是我加上的注释。 读者可以对比上面的程序源码来看一下, 这样的话, 就可以清楚的看出, 源文件中的各种字符串, 是如何和存放到 CONSTANT_Utf8_info 中的。
 
这里要强调一下, 源文件中的几乎所有可见的字符串都存放在 CONSTANT_Utf8_info 中, 其他类型的常量池项只不过是对 CONSTANT_Utf8_info 的引用。 其他常量池项, 把引用的 CONSTANT_Utf8_info 组合起来, 进而可以描述更多的信息。 下面将要介绍的 CONSTANT_NameAndType_info 就可以验证这个结论。
 
 
 

(2) CONSTANT_NameAndType 类型的数据项

 
常量池中的一个 CONSTANT_NameAndType_info 数据项, 可以看做 CONSTANT_NameAndType 类型的一个实例 。 从这个数据项的名称可以看出, 它描述了两种信息,第一种信息是名称(Name), 第二种信息是类型(Type) 。 这里的名称是指方法的名称或者字段的名称, 而 Type 是广义上的类型, 它其实描述的是字段的描述符或方法的描述符。 也就是说, 如果 Name 部分是一个字段名称, 那么 Type 部分就是相应字段的描述符; 如果 Name 部分描述的是一个方法的名称, 那么 Type 部分就是对应的方法的描述符。 也就是说, 一个 CONSTANT_NameAndType_info 就表示了一个方法或一个字段。 
 
下面先看一下 CONSTANT_NameAndType_info 数据项的存储格式。 既然是常量池中的一种数据项类型, 那么它的第一个字节也是 tag, 它的 tag 值是 12, 也就是说, 当虚拟机读到一个 tag 为 12 的常量池数据项, 就可以确定这个数据项是一个 CONSTANT_NameAndType_info 。 tag 值一下的两个字节叫做 name_index, 它指向常量池中的一个 CONSTANT_Utf8_info, 这个 CONSTANT_Utf8_info 中存储的就是方法或字段的名称。 name_index 以后的两个字节叫做 descriptor_index, 它指向常量池中的一个 CONSTANT_Utf8_info, 这个 CONSTANT_Utf8_info 中存储的就是方法或字段的描述符。 下图表示它的存储布局:
 
下面举一个实例进行说明, 实例的源码为:
 
  1. package com.jg.zhang;
  2.  
  3. public class Person {
  4.  
  5. int age;
  6.  
  7. int getAge(){
  8. return age;
  9. }
  10. }
 


这个 Person 类很简单, 只有一个字段 age, 和一个方法 getAge 。 将这段代码使用 javap 工具反编译之后, 常量池信息如下:
 
   #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 , 是因为子类构造方法中会默认调用父类的无参数构造方法。 我们将常量中的其他信息去掉, 可以看得更直观:
 
 
下面讲解常量池第 #19 项的 CONSTANT_NameAndType_info , 它引用了常量池第 #5 项和第 #6 项, 这两项也是 CONSTANT_Utf8_info 项, 其中存储的字符串分别是 age 和 I, 其中 age 是源码中字段 age 的字段名, I 是 age 字段的描述符。 所以这个 CONSTANT_NameAndType_info 就表示对本类中的字段 age 的引用。 除去常量池中的其他信息, 可以看得更直观:
 
 
和方法相同, 只定义一个字段而不引用它(在源码中表现为不访问这个变量), 那么在常量池中也不会存在和该字段相对应的 CONSTANT_NameAndType_info 项。这也是为什么说 CONSTANT_NameAndType_info 作为字段符号引用的一部分的原因。 (这里提到一个新的概念, 叫做字段的符号引用, 这个概念会在后面的博客中进行讲解) 在本例中之所以会出现这个 CONSTANT_NameAndType_info , 是因为在源码的 getAge 方法中访问了这个字段:
 
 
  1. int getAge(){
  2. return age;
  3. }
 
 
下面给出这两个 CONSTANT_NameAndType_info 真实的内存布局图:
 
和 Object 构造方法相关的 CONSTANT_NameAndType_info 的示意图:
 
 
和 age 字段相关的 CONSTANT_NameAndType_info 示意图:
 
这两张图能够很好的反映出 CONSTANT_NameAndType_info 和 CONSTANT_Utf8_info 这两种常量池数据项的数据存储方式, 也能够真实的反应 CONSTANT_NameAndType_info 和 CONSTANT_Utf8_info 的引用关系。 
 
 

总结

 
本篇博客就到此为止, 在本文中我们主要介绍了常量池中的两种数据项: CONSTANT_NameAndType_info 和 CONSTANT_Utf8_info  。 其中 CONSTANT_Utf8_info 存储的是源文件中的各种字符串, 而 CONSTANT_NameAndType_info 表述的是源文件中对一个字段或方法的符号引用的一部分(即 方法名加方法描述符, 或者是 字段名加字段描述符)。在下一篇博客中, 继续讲解常量池中的其他类型的数据项 。 
在上一篇博客深入理解 Java Class 文件格式(三) 中, 介绍了常量池中的两种类型的数据项, 分别是
  1. CONSTANT_Utf8_info
  2. CONSTANT_NameAndType_info 。
CONSTANT_Utf8_info 中存储了几乎所有类型的字符串, 包括方法名, 字段名, 描述符等等。 而 CONSTANT_NameAndType_info 是方法符号引用或字段的符号引用的一部分, 也就是说, 如果在源文件中调用了一个方法, 或者引用了一个字段(不管是本类中的方法和字段, 还是引用其他类中的方法和字段), 那么和这个方法或在字段相对应的 CONSTANT_NameAndType_info 就会出现在常量池中。 注意, 只有引用了一个方法或字段, 常量池中才会存在和它对应的 CONSTANT_NameAndType_info , 如果只在当前类中定义了一个字段而不访问它, 或者定义了一个方法而不调用它, 那么常量池中就不会出现对应的 CONSTANT_NameAndType_info 数据项。 CONSTANT_NameAndType_info 中引用了两个 CONSTANT_Utf8_info, 一个叫做 name_index, 存储方法名或字段名, 一个叫做 descriptor_index, 存储方法描述符或字段描述符。 
 
关于这两种常量池数据项的详细介绍, 请参阅上一篇博客:深入理解 Java Class 文件格式(三) 。 关于虚拟机和 class 文件格式的前几篇文章,已经收录在我的博客专栏中, 如果想全面了解这些知识,建议按顺序阅读我的专栏中的博客, 专栏地址:
 
 
后续关于深入理解 java 的其他一些文章, 也会收录在本专栏中, 欢迎关注和交流。 
 
下面我们继续详细讲解常量池中的其他类型的数据项, 本文接着前几篇文章写的, 建议先读前几篇文章。   
 
 

常量池中各数据项类型详解(续)

(3)CONSTANT_Integer_info

 
一个常量池中的 CONSTANT_Integer_info 数据项,可以看做是 CONSTANT_Integer 类型的一个实例。 它存储的是源文件中出现的 int 型数据的值。 同样, 作为常量池中的一种数据类型, 它的第一个字节也是一个 tag 值, 它的 tag 值为 3, 也就是说, 当虚拟机读到一个 tag 值为 3 的数据项时, 就知道这个数据项是一个 CONSTANT_Integer_info, 它存储的是 int 型数值的值。 紧挨着 tag 的下面 4 个字节叫做 bytes, 就是 int 型数值的整型值。 它的内存布局如下:
 
下面以示例代码进行说明, 示例代码如下:
 
 
  1. package com.jg.zhang;
  2.  
  3. public class TestInt {
  4.  
  5. void printInt(){
  6. System.out.println(65535);
  7. }
  8. }
 

将上面的类生成的 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

 
一个常量池中的 CONSTANT_Float_info 数据项,可以看做是 CONSTANT_Float 类型的一个实例。 它存储的是源文件中出现的 float 型数据的值。 同样, 作为常量池中的一种数据类型, 它的第一个字节也是一个 tag 值, 它的 tag 值为 4, 也就是说, 当虚拟机读到一个 tag 值为 4 的数据项时, 就知道这个数据项是一个 CONSTANT_Float_info, 并且知道它存储的是 float 型数值。 紧挨着 tag 的下面 4 个字节叫做 bytes, 就是 float 型的数值。 它的内存布局如下:
 
举例说明, 如果源文件中的一句代码使用了一个 float 值, 如下所示:
 
  1. void printFloat(){
  2. System.out.println(1234.5f);
  3. }
 

那么在这个类的常量池中就会有一个 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

 
一个常量池中的 CONSTANT_Long_info 数据项,可以看做是 CONSTANT_Long 类型的一个实例。 它存储的是源文件中出现的 long 型数据的值。 同样, 作为常量池中的一种数据类型, 它的第一个字节也是一个 tag 值, 它的 tag 值为 5, 也就是说, 当虚拟机读到一个 tag 值为 5 的数据项时, 就知道这个数据项是一个 CONSTANT_Long_info, 并且知道它存储的是 long 型数值。 紧挨着 tag 的下面 8 个字节叫做 bytes, 就是 long 型的数值。 它的内存布局如下:
 
举例说明, 如果源文件中的一句代码使用了一个 long 型的数值, 如下所示:
 
  1. void printLong(){
  2. System.out.println(123456L);
  3. }
 
那么在这个类的常量池中就会有一个 CONSTANT_Long_info 与之相对应, 这个 CONSTANT_Long_info 的形式如下:
 
代码反编译结果为:
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

 
一个常量池中的 CONSTANT_Double_info 数据项,可以看做是 CONSTANT_Double 类型的一个实例。 它存储的是源文件中出现的 double 型数据的值。 同样, 作为常量池中的一种数据类型, 它的第一个字节也是一个 tag 值, 它的 tag 值为 6, 也就是说, 当虚拟机读到一个 tag 值为 6 的数据项时, 就知道这个数据项是一个 CONSTANT_Double_info, 并且知道它存储的是 double 型数值。 紧挨着 tag 的下面 8 个字节叫做 bytes, 就是 double 型的数值。 它的内存布局如下:
 
举例说明, 如果源文件中的一句代码使用了一个 double 型的数值, 如下所示:
 
  1. void printDouble(){
  2. System.out.println(123456D);
  3. }
 


那么在这个类的常量池中就会有一个 CONSTANT_Double_info 与之相对应, 这个 CONSTANT_Double_info 的形式如下:
 
代码反编译结果为:
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

 
在常量池中, 一个 CONSTANT_String_info 数据项, 是 CONSTANT_String 类型的一个实例。 它的作用是存储文字字符串, 可以把他看做是一个存在于 class 文件中的字符串对象。 同样, 它的第一个字节是 tag 值, 值为 8 , 也就是说, 虚拟机访问一个数据项时, 判断 tag 值为 8 , 就说明访问的数据项是一个 CONSTANT_String_info 。 紧挨着 tag 的后两个字节是一个叫做 string_index 的常量池引用, 它指向一个 CONSTANT_Utf8_info, 这个 CONSTANT_Utf8_info 存放的才是字符串的字面量。 它的内存布局如下:
 
举例说明, 如果源文件中的一句代码使用了一个字符串常量, 如下所示:
 
  1. void printStrng(){
  2. System.out.println("abcdef");
  3. }
 
那么在这个类的常量池中就会有一个 CONSTANT_String_info 与之相对应, 反编译结果如下:
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_Integer_info, CONSTANT_Float_info, CONSTANT_Long_info, CONSTANT_Double_info 和 CONSTANT_String_info 。 这几种常量池数据项都是直接存储的常量值,而不是符号引用。 这里又一次出现了符号引用的概念, 这个概念将会在下一篇博客中详细讲解, 因为下一篇博客要介绍的剩下的四种常量池数据项, 都是符号引用, 这四种表示符号引用的数据项又会直接或间接引用上篇文章中介绍的 CONSTANT_NameAndType_info 和 CONSTANT_Utf8_info, 所以说 CONSTANT_NameAndType_info 是符号引用的一部分。 
 
从本文中我们还可以知道。 虽然说 CONSTANT_String_info 是直接存储值的数据项, 但是 CONSTANT_String_info 有点特别, 因为它不是直接存储字符串, 而是引用了一个 CONSTANT_Utf8_info, 这个被引用的 CONSTANT_Utf8_info 中存储了字符串。 
 
最后, 列出下一篇博文要介绍的剩下的四种常量池数据项:
  1. CONSTANT_Class_info
  2. CONSTANT_Fieldref_info
  3. CONSTANT_Methodref_info
  4. CONSTANT_InterfaceMethodref_info

本专栏的前几篇博文, 对 class 文件中的常量池进行了详细的解释。 前文讲解了常量池中的 7 种数据项, 它们分别是:

 

  1. CONSTANT_Utf8_info
  2. CONSTANT_NameAndType_info
  3. CONSTANT_Integer_info
  4. CONSTANT_Float_info
  5. CONSTANT_Long_info
  6. CONSTANT_Double_info
  7. CONSTANT_String_info
 
关于这七种数据项, 前面的文章已经讲得很详细了, 不了解的同学请先参阅前面的博文。 此外, 如果想要全面的了解 JVM 和 Class 文件格式, 建议按顺序阅读我专栏中的博客。此外,本文是建立在前几篇博客的基础之上的, 是接着前面的的博客写的,所以, 同样建议先阅读专栏前面的博客, 以保证知识的完整性。 专栏地址:
 
 
 更多关于深入理解 java 的文章会陆续收录到该专栏中, 欢迎关注, 欢迎交流。 
 
 

常量池中各数据项类型详解(续)

 
 

(8) CONSTANT_Class_info

 
常量池中的一个 CONSTANT_Class_info, 可以看做是 CONSTANT_Class 数据类型的一个实例。 他是对类或者接口的符号引用。 它描述的可以是当前类型的信息, 也可以描述对当前类的引用, 还可以描述对其他类的引用。 也就是说, 如果访问了一个类字段, 或者调用了一个类的方法, 对这些字段或方法的符号引用, 必须包含它们所在的类型的信息, CONSTANT_Class_info 就是对字段或方法符号引用中类型信息的描述。 
 
 
CONSTANT_Class_info 的第一个字节是 tag, 值为 7, 也就是说, 当虚拟机访问到一个常量池中的数据项, 如果发现它的 tag 值为 7, 就可以判断这是一个 CONSTANT_Class_info 。 tag 下面的两个字节是一个叫做 name_index 的索引值, 它指向一个 CONSTANT_Utf8_info, 这个 CONSTANT_Utf8_info 中存储了 CONSTANT_Class_info 要描述的类型的全限定名。 全限定名的概念在前面的博文 深入理解 Java Class 文件格式(二) 中将结果, 不熟悉的同学可以先阅读这篇文章。  
 
此外要说明的是, java 中数组变量也是对象, 那么数组也就有相应的类型, 并且数组的类型也是使用 CONSTANT_Class_info 描述的, 并且数组类型和普通类型的描述有些区别。 普通类型的 CONSTANT_Class_info 中存储的是全限定名, 而数组类型对应的 CONSTANT_Class_info 中存储的是数组类型相对应的描述符字符串。 举例说明:
 
与 Object 类型对应的 CONSTANT_Class_info 中存储的是: java/lang/Object 
与 Object [] 类型对应的 CONSTANT_Class_info 中存储的是: [Ljava/lang/Object; 
 
 
下面看 CONSTANT_Class_info 的存储布局:
 
 
例如, 如果在一个类中引用了 System 这个类, 那么就会在这个类的常量池中出现以下信息:
 
 
 
 
 

(9) CONSTANT_Fieldref_info

 
常量池中的一个 CONSTANT_Fieldref_info , 可以看做是 CONSTANT_Field 数据类型的一个实例。 该数据项表示对一个字段的符号引用, 可以是对本类中的字段的符号引用, 也可以是对其他类中的字段的符号引用, 可以是对成员变量字段的符号引用, 也可以是对静态变量的符号引用,其中 ref 三个字母就是 reference 的简写。 之前的文章中, “符号引用” 这个名词出现了很多次, 可能有的同学一直不是很明白, 等介绍完 CONSTANT_Fieldref_info, 就可以很清晰的了解什么是符号引用。 下面分析 CONSTANT_Fieldref_info 中的内容都存放了什么信息。 
 
和其他类型的常量池数据项一样, 它的第一个字节也必然是 tag, 它的 tag 值为 9 。 也就是说, 当虚拟机访问到一个常量池中的一项数据, 如果发现这个数据的 tag 值为 9, 就可以确定这个被访问的数据项是一个 CONSTANT_Fieldref_info, 并且知道这个数据项表示对一个字段的符号引用。 
 
tag 值下面的两个字节是一个叫做 class_index 的索引值, 它指向一个 CONSTANT_Class_info 数据项, 这个数据项表示被引用的字段所在的类型, 包括接口。 所以说, CONSTANT_Class_info 可以作为字段符号引用的一部分。 
 
class_index 以下的两个字节是一个叫做 name_and_type_index 的索引, 它指向一个 CONSTANT_NameAndType_info, 这个 CONSTANT_NameAndType_info 前面的博客中已经解释过了, 不明白的朋友可以先看前面的博客:深入理解 Java Class 文件格式(三) 。 这个 CONSTANT_NameAndType_info 描述的是被引用的字段的名称和描述符。 我们在前面的博客中也提到过, CONSTANT_NameAndType_info 可以作为字段符号引用的一部分。
 
到此, 我们可以说, CONSTANT_Fieldref_info 就是对一个字段的符号引用, 这个符号引用包括两部分, 一部分是该字段所在的类, 另一部分是该字段的字段名和描述符。 这就是所谓的 “对字段的符号引用” 。
 
下面结合实际代码来说明, 代码如下:
 
 
  1. package com.jg.zhang;
  2.  
  3. public class TestInt {
  4. int a = 10;
  5. void print(){
  6. System.out.println(a);
  7. }
  8. }
 

在 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
}


 
可以看到, print 方法的位置为 4 的字节码指令 getfield 引用了索引为 12 的常量池数据项, 常量池中索引为 12 的数据项是一个 CONSTANT_Fieldref_info, 这个 CONSTANT_Fieldref_info 又引用了索引为 1 和 13 的两个数据项, 索引为 1 的数据项是一个 CONSTANT_Class_info, 这个 CONSTANT_Class_info 数据项又引用了索引为 2 的数据项, 索引为 2 的数据项是一个 CONSTANT_Utf8_info , 他存储了字段 a 所在的类的全限定名 com/jg/zhang/TestInt 。 而 CONSTANT_Fieldref_info 所引用的索引为 13 的数据项是一个 CONSTANT_NameAndType_info, 它又引用了两个数据项, 分别为第 5 项和第 6 项, 这是两个 CONSTANT_Utf8_info , 分别存储了字段 a 的字段名 a, 和字段 a 的描述符 I 。 
 
下面给出内存布局图, 这个图中涉及的东西有点多, 因为 CONSTANT_Fieldref_info 引用了 CONSTANT_Class_info 和 CONSTANT_NameAndType_info, CONSTANT_Class_info 又引用了一个 CONSTANT_Utf8_info , 而 CONSTANT_NameAndType_info 又引用了两个 CONSTANT_Utf8_info 。 
 
 
 
 

(10) CONSTANT_Methodref_info

 
常量池中的一个 CONSTANT_Methodref_info , 可以看做是 CONSTANT_Methodref 数据类型的一个实例。 该数据项表示对一个类中方法的符号引用, 可以是对本类中的方法的符号引用, 也可以是对其他类中的方法的符号引用, 可以是对成员方法字段的符号引用, 也可以是对静态方法的符号引用,但是不会是对接口中的方法的符号引用。 其中 ref 三个字母就是 reference 的简写。 在上一小节中介绍了 CONSTANT_Fieldref_info, 它是对字段的符号引用, 本节中介绍的 CONSTANT_Methodref_info 和 CONSTANT_Fieldref_info 很相似。既然是符号 “引用”, 那么只有在原文件中调用了一个方法, 常量池中才有和这个被调用方法的相对应的符号引用, 即存在一个 CONSTANT_Methodref_info。 如果只是在类中定义了一个方法, 但是没调用它, 则不会在常量池中出现和这个方法对应的 CONSTANT_Methodref_info 。 
 
和其他类型的常量池数据项一样, 它的第一个字节也必然是 tag, 它的 tag 值为 10 。 也就是说, 当虚拟机访问到一个常量池中的一项数据, 如果发现这个数据的 tag 值为 10, 就可以确定这个被访问的数据项是一个 CONSTANT_Methodref_info, 并且知道这个数据项表示对一个方法的符号引用。 
 
tag 值下面的两个字节是一个叫做 class_index 的索引值, 它指向一个 CONSTANT_Class_info 数据项, 这个数据项表示被引用的方法所在的类型。 所以说, CONSTANT_Class_info 可以作为方法符号引用的一部分。 
 
class_index 以下的两个字节是一个叫做 name_and_type_index 的索引, 它指向一个 CONSTANT_NameAndType_info, 这个 CONSTANT_NameAndType_info 前面的博客中已经解释过了, 不明白的朋友可以先看前面的博客:深入理解 Java Class 文件格式(三) 。 这个 CONSTANT_NameAndType_info 描述的是被引用的方法的名称和描述符。 我们在前面的博客中也提到过, CONSTANT_NameAndType_info 可以作为方法符号引用的一部分。
 
到此, 我们可以知道, CONSTANT_Methodref_info 就是对一个字段的符号引用, 这个符号引用包括两部分, 一部分是该方法所在的类, 另一部分是该方法的方法名和描述符。 这就是所谓的 “对方法的符号引用” 。
下面结合实际代码来说明, 代码如下:
 
  1. package com.jg.zhang;
  2.  
  3. public class Programer {
  4.  
  5. Computer computer;
  6.  
  7. public Programer(Computer computer){
  8. this.computer = computer;
  9. }
  10.  
  11. public void doWork(){
  12. computer.calculate();
  13. }
  14. }
 
 
 
  1. package com.jg.zhang;
  2.  
  3. public class Computer {
  4.  
  5. public void calculate() {
  6. System.out.println("working...");
  7.  
  8. }
  9. }
 


上面的代码包括两个类, 其中 Programer 类引用了 Computer 类, 在 Programer 类的 doWork 方法中引用(调用)了 Computer 类的 calculate 方法。源码中对一个方法的描述形式我们再熟悉不过了, 现在我们就反编译 Programer, 看看 Programer 中对 Computer 的 doWork 方法的引用, 在 class 文件中是如何描述的。 
 
下面给出 Programer 的反编译结果, 其中省去了一些不相关的信息:
 
  1. Constant pool:
  2. .........
  3.  
  4.  
  5. #12 = Utf8 ()V
  6.  
  7.  
  8. #20 = Methodref #21.#23 // com/jg/zhang/Computer.calculate:()V
  9. #21 = Class #22 // com/jg/zhang/Computer
  10. #22 = Utf8 com/jg/zhang/Computer
  11. #23 = NameAndType #24:#12 // calculate:()V
  12. #24 = Utf8 calculate
  13.  
  14. {
  15.  
  16. com.jg.zhang.Computer computer;
  17. flags:
  18.  
  19. .........
  20.  
  21. public void doWork();
  22. flags: ACC_PUBLIC
  23. Code:
  24. stack=1, locals=1, args_size=1
  25. 0: aload_0
  26. 1: getfield #13 // Field computer:Lcom/jg/zhang/Computer;
  27. 4: invokevirtual #20 // Method com/jg/zhang/Computer.calculate:()V
  28. 7: return
  29. }
 

可以看到, 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 。 
 
下面给出内存布局图, 这个图中涉及的东西同样有点多, 因为 CONSTANT_Methodref_info 引用了 CONSTANT_Class_info 和 CONSTANT_NameAndType_info, CONSTANT_Class_info 又引用了一个 CONSTANT_Utf8_info , 而 CONSTANT_NameAndType_info 又引用了两个 CONSTANT_Utf8_info 。 
 
 
 

(11) CONSTANT_InterfaceMethodref_info

 
常量池中的一个 CONSTANT_InterfaceMethodref_info , 可以看做是 CONSTANT_InterfaceMethodref 数据类型的一个实例。 该数据项表示对一个接口方法的符号引用, 不能是对类中的方法的符号引用。 其中 ref 三个字母就是 reference 的简写。 在上一小节中介绍了 CONSTANT_Methodref_info, 它是对类中的方法的符号引用, 本节中介绍的 CONSTANT_InterfaceMethodref 和 CONSTANT_Methodref_info 很相似。既然是符号 “引用”, 那么只有在原文件中调用了一个接口中的方法, 常量池中才有和这个被调用方法的相对应的符号引用, 即存在一个 CONSTANT_InterfaceMethodref。 如果只是在接口中定义了一个方法, 但是没调用它, 则不会在常量池中出现和这个方法对应的 CONSTANT_InterfaceMethodref 。 
 
和其他类型的常量池数据项一样, 它的第一个字节也必然是 tag, 它的 tag 值为 11 。 也就是说, 当虚拟机访问到一个常量池中的一项数据, 如果发现这个数据的 tag 值为 11, 就可以确定这个被访问的数据项是一个 CONSTANT_InterfaceMethodref, 并且知道这个数据项表示对一个接口中的方法的符号引用。 
 
tag 值下面的两个字节是一个叫做 class_index 的索引值, 它指向一个 CONSTANT_Class_info 数据项, 这个数据项表示被引用的方法所在的接口。 所以说, CONSTANT_Class_info 可以作为方法符号引用的一部分。 
 
class_index 以下的两个字节是一个叫做 name_and_type_index 的索引, 它指向一个 CONSTANT_NameAndType_info, 这个 CONSTANT_NameAndType_info 前面的博客中已经解释过了, 不明白的朋友可以先看前面的博客:深入理解 Java Class 文件格式(三) 。 这个 CONSTANT_NameAndType_info 描述的是被引用的方法的名称和描述符。 我们在前面的博客中也提到过, CONSTANT_NameAndType_info 可以作为方法符号引用的一部分。
 
到此, 我们可以知道, CONSTANT_InterfaceMethodref 就是对一个接口中的方法的符号引用, 这个符号引用包括两部分, 一部分是该方法所在的接口, 另一部分是该方法的方法名和描述符。 这就是所谓的 “对接口中的方法的符号引用” 。

下面结合实际代码来说明, 代码如下:

 

 
  1. package com.jg.zhang;
  2.  
  3. public class Plane {
  4.  
  5. IFlyable flyable;
  6.  
  7. void flyToSky(){
  8. flyable.fly();
  9. }
  10. }
 

 

 
  1. package com.jg.zhang;
  2.  
  3. public interface IFlyable {
  4.  
  5. void fly();
  6. }
 


在上面的代码中, 定义可一个类 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 。 
 
下面给出内存布局图, 这个图中涉及的东西同样有点多, 因为 CONSTANT_InterfaceMethodref_info 引用了 CONSTANT_Class_info 和 CONSTANT_NameAndType_info, CONSTANT_Class_info 又引用了一个 CONSTANT_Utf8_info , 而 CONSTANT_NameAndType_info 又引用了两个 CONSTANT_Utf8_info 。 
 

 

 

 

 

总结

 
到此为止, class 文件中的常量池部分就已经讲解完了。 进行一下总结。对于深入理解 Java 和 JVM , 理解 class 文件的格式至关重要, 而在 class 文件中, 常量池是一项非常重要的信息。 常量池中有 11 种数据项, 这个 11 种数据项存储了各种信息, 包括常量字符串, 类的信息, 方法的符号引用, 字段的符号引用等等。 常量池中的数据项通过索引来访问, 访问形式类似于数组。 常量池中的各个数据项之前会通过索引相互引用, class 文件的其他地方也会引用常量池中的数据项 , 如方法的字节码指令。 
 
在下面的文章中, 会继续介绍 class 文件中, 位于常量池以下的其他信息。 这些信息包括:对本类的描述, 对父类的描述, 对实现的接口的描述, 本类中声明的字段的描述, 本类汇总定义的方法的描述,还有各种属性。 
 
 

经过前几篇文章, 终于将常量池介绍完了, 之所以花这么大的功夫介绍常量池, 是因为对于理解 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 文件中的访问标志信息

 
 
从上面的表格可以看出, 位于常量池下面的 2 个字节是 access_flags 。 access_flags 描述的是当前类(或者接口)的访问修饰符, 如 public, private 等, 此外, 这里面还存在一个标志位, 标志当前的额这个 class 描述的是类, 还是接口。access_flags 的信息比较简单, 下面列出 access_flags 中的各个标志位的信息。本来写这个系列博客参考的是《深入 java 虚拟机》, 但是这本书比较老了, 关于 java 5 以后的新特性没有进行解释,这本书中指列出了 5 个标志值, 而最新的 JVM 规范是针对 java 7 的, 其中加入了额外的三个标志位。 分别是 ACC_SYNTHETIC, ACC_ANNOTATION 和 ACC_ENUM  。
 

标志名 标志值 标志含义 针对的对像
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 这条指令增加了动态绑定的功能。 这里可能有几个概念读者不是很明白, 如静态绑定, 动态绑定等, 这些概念会在以后的博客中详细介绍。
 
还有一点需要说明, 既然 access_flags 出现在 class 文件中的类的层面上, 那么它只能描述类型的修饰符, 而不能描述字段或方法的修饰符, 希望读者不要将这里的 access_flags 和后面要介绍的方法表和字段表中的访问修饰符相混淆。
 
此外, 在 Java 5 的中, 引入和注解和枚举的新特性, 那么可以推测,  ACC_ANNOTATION 和 ACC_ENUM 是在 Java 5 版本中加入的。 class 文件虽然总体上保持前后一致性, 但他也不是一成不变的, 也会跟着 Java 版本的提升而有所改变, 但是总体来说, class 文件格式还是相对稳定的, 变动的地方不是很多。 
 
 

class 文件中的 this_class


访问标志 access_flags 下面的两个字节叫做 this_class, 它是对当前类的描述。 它的两个字节的数据是对常量池中的一个 CONSTANT_Class_info 数据项的一个索引。 CONSTANT_Class_info 在上面的文章中已经介绍过了。 CONSTANT_Class_info 中有一个字段叫做 name_index , 指向一个 CONSTANT_Utf8_info , 在这个 CONSTANT_Utf8_info 中存放着当前类的全限定名。 
 
如果当前类为 Person:
 
  1. package com.jg.zhang;
  2.  
  3. public class Person {
  4.  
  5. int age;
  6.  
  7. int getAge(){
  8. return age;
  9. }
  10. }
 

将 Person.class 反编译后, 可以在常量池中看到如下两项:
 
 
  1. Constant pool:
  2. #1 = Class #2 // com/jg/zhang/Person
  3. #2 = Utf8 com/jg/zhang/Person
  4.  
  5. .........
  6. .........
 


这两项就是当前类的信息。 其中索引为 1 的 CONSTANT_Class_info 会被 class 文件中的 this_class 所引用。 下面给出示例图(其中虚线范围内表示常量池的区域):
 

class 文件中的 super_class

 
 
super_class 紧跟在 this_class 之后。 它和 this_class 一样是一个指向常量池数据项的索引。 它指向一个 CONSTANT_Class_info, 这个 CONSTANT_Class_info 数据项描述的是当前类的超类的信息。CONSTANT_Class_info 中的 name_index 指向常量池中的一个 CONSTANT_Utf8_info ,CONSTANT_Utf8_info 中存放的是当前类的超类的全限定名。 如果没有显式的继承一个,也就是说如果当前类是直接继承 Object 的, 那么 super_class 值为 0 。 我们在前面的文章中提到过, 如果一个索引值为 0, 那么就说明这个索引不引用任何常量池中的数据项, 因为常量池中的数据项是从 1 开始的。 也就是说, 如果一个类的 class 文件中的 super_class 为 0 , 那么就代表该类直接继承 Object 类。 
 
下面以代码来说明:
 
  1. package com.jg.zhang;
  2.  
  3. public class Programer extends Person{
  4.  
  5. Computer computer;
  6.  
  7. public Programer(Computer computer){
  8. this.computer = computer;
  9. }
  10.  
  11. public void doWork(){
  12. computer.calculate();
  13. }
  14. }
 

上面的 Programer 类继承自 Person 类。 那么反编译 Programer .class , 它的常量池中会存在如下信息:
 
  1. Constant pool:
  2.  
  3. .........
  4. .........
  5.  
  6. #3 = Class #4 // com/jg/zhang/Person
  7. #4 = Utf8 com/jg/zhang/Person
 

这两项就是当前类的父类的信息。 其中索引为 3 的 CONSTANT_Class_info 会被 class 文件中的 super_class 引用。 下面给出示例图(其中虚线范围内表示常量池的区域):
 
 

class 文件中的 interfaces_count 和 interfaces

 
 
紧接着 super_class 的是 interfaces_count, 表示当前类所实现的接口的数量或者当前接口所继承的超接口的数量。 注意, 只有当前类直接实现的接口才会被统计, 如果当前类继承了另一个类, 而另一个类又实现了一个接口, 那么这个接口不会统计在当前类的 interfaces_count 中。 在 interfaces_count 后面是 interfaces, 他可以看做是一个数组, 其中的每个数组项是一个索引, 指向常量池中的一个 CONSTANT_Class_info, 这个 CONSTANT_Class_info 又会引用常量池中的一个 CONSTANT_Utf8_info , 这个 CONSTANT_Utf8_info 中存放着有当前类型直接实现或继承的接口的全限定名。 当前类型实现或继承了几个接口, 在 interfaces 数组中就会有几个数项与之相对应。 
 
下面看代码示例:
 
  1. package com.jg.zhang;
  2.  
  3. public class Plane implements IFlyable, Cloneable{
  4.  
  5. @Override
  6. public void fly() {
  7.  
  8. }
  9. }
 

Plane 类实现了一个自定义的 IFlyable 接口, 还实现了一个 JDK 中的 Cloneable 接口, 那么它的常量池中会有如下信息:
 
 
  1. Constant pool:
  2.  
  3. .........
  4. .........
  5.  
  6. #5 = Class #6 // com/jg/zhang/IFlyable
  7. #6 = Utf8 com/jg/zhang/IFlyable
  8. #7 = Class #8 // java/lang/Cloneable
  9. #8 = Utf8 java/lang/Cloneable
  10.  
  11. .........
  12. .........
 

这四项数据就是当前的 Plane 类所实现的接口的信息。 第五项和第六项描述了 Plane 所实现的 IFlyable 接口, 第七项和第八项描述了 Plane 所实现的接口 Cloneable 接口。 下面是示意图(其中虚线范围内表示常量池的区域):
 

 

 

总结

 
在本篇博客中, 继续讲解了 class 文件中常量池以下的部分。 主要讲解了三个部分, 分别是 this_class , super_class , interfaces_count 和 interfaces 。 这三个数据项分别描述了当前类(就是当前 class 文件所在的类), 当前类所继承的超类, 和当前类所实现的接口(如果当前 class 文件代表的是一个接口, 那么 interfaces_count 和 interfaces 描述的是当前接口所继承的超接口)。
 
这几个数据项都持有指向常量池的索引。 真实的信息都是存放在常量池中的, 只不过常量池中的这些信息会被 this_class , super_class , interfaces_count 和 interfaces 引用。 
 
通过本篇博客我们可以知道源文件中的当前类, 当前类的超类以及当前类的超接口在 class 文件中是如何被描述的。 在下一篇博客中, 将会讲解源文件中的定义的字段, 声明的方法在 class 文件中是如何描述的。 

本专栏列前面的一系列博客, 对 Class 文件中的一部分数据项进行了介绍。 本文将会继续介绍 class 文件中未讲解的信息。 先回顾一下上面一篇文章。 在上一篇博客中, 我们介绍了:

 

  • this_class    对当前类的描述
  • super_class    对当前类的超类的描述
  • interfaces_count    当前类直接实现的接口的数量或当前接口直接继承的接口的数量
  • interfaces  对当前类或当前接口直接实现或继承的所有接口的描述

 

 

详细信息请移步至上一篇博客 深入理解 Java Class 文件格式(六)。 更多关于 Java Class 文件和 JVM 的文章请关注我的专栏深入理解 Java 语言 。 

 

下面继续介绍 class 文件中的其他信息。

 

 

class 文件中的 fields_count 和 fields

 
fields_count 描述的是当前的类中定义的字段的个数, 注意, 这里包括静态字段, 但不包括从父类继承的字段。 如果当前 class 文件是由一个接口生成的, 那么这里的 fields_count 描述的是接口中定义的字段, 我们知道, 接口中定义的字段默认都是静态的。此外要说明的是, 编译器可能会自动生成字段, 也就是说, class 文件中的字段的数量可能多于源文件中定义的字段的数量。 举例来说, 编译器会为内部类增加一个字段, 这个字段是指向外围类的对象的引用。
 
位于 fields_count 下面的数据叫做 fields, 可以把它看做一个数组, 数组中的每一项是一个 field_info 。这个数组中一共有 fields_count 个 field_info , 每个 field_info 都是对一个字段的描述。 下面我们详细讲解 field_info 的结构。 每个 field_info 的结构如下:
 

(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

name_index 下面的两个字节叫做 descriptor_index , 它同样是一个指向常量池的索引, 它描述的是当前字段的描述符。 这个索引指向常量池中的一个 CONSTANT_Utf8_info 数据项。 这个 CONSTANT_Utf8_info 数据项中存放的字符串就是当前字段的描述符(关于字段描述符, 在前面的博客中已经有过详细的讲解, 如果不明白, 请参考前面的博客:深入理解 Java Class 文件格式(二))。 
 

(4)attributes_count 和 attributes

descriptor_index 下面是 attributes_count 和 attributes 。 这是对当前字段所具有的属性的描述。 这里的属性和源文件中的属性不是同一个概念, 在源文件测层面中, 属性是字段的另一种叫法, 希望读者不要疑惑。读者也不要轻视 class 文件中的属性, 这些属性可以描述很多的信息。 我们会在后面的文章中进行介绍。 
 
attributes_count 表示这个字段有几个属性。attributes 可以看成一个数组, 数组中的每一项都是一个 attribute_info , 每个 attribute_info 表示一个属性, 数组中一共有 attributes_count 个属性。可以出现在 filed_info 中的属性有三种, 分别是 ConstantValue, Deprecated, 和 Synthetic。 这些属性会在后面的文章中进行介绍。
 
 
下面我们以代码的形式进行解释, 源码如下:
 
 
  1. package com.jg.zhang;
  2.  
  3. public class Programer extends Person{
  4.  
  5.  
  6. private Computer computer;
  7.  
  8. public Programer(Computer computer){
  9. this.computer = computer;
  10. }
  11.  
  12. public void doWork(){
  13. computer.calculate();
  14. }
  15. }
 
 
反编译之后, 常量池中会有如下信息(这里省略了大部分无关信息):
 
 
  1. Constant pool:
  2.  
  3. .........
  4. .........
  5.  
  6. #5 = Utf8 computer
  7. #6 = Utf8 Lcom/jg/zhang/Computer;
  8.  
  9. .........
  10. .........
  11.  
  12. {
  13.  
  14. private com.jg.zhang.Computer computer;
  15. flags: ACC_PRIVATE
  16.  
  17. .........
  18. .........
  19.  
  20. }
 

从反编译的结果可以看出, 源文件中定义了一个 Computer 类型的字段 computer, 并且是 private 的。 然后常量池中有这个字段的字段名和描述符。 其中常量池第五项的 CONSTANT_Utf8_info 是字段名, 第六项的 CONSTANT_Utf8_info 是该字段的描述符。这里有一点需要说明, 在反编译 Programer.class 时,由于 computer 是私有的, 要加 - private 选项, 否则的话, 虽然常量池中有字段引用信息, 但是不会输出字段信息, 即下面这两行不会输出 :
 
  1. private com.jg.zhang.Computer computer;
  2. flags: ACC_PRIVATE
 
如果在 javap 中加入 - private 选项, 那么就会有上面两行的输出。 使用的命令如下:
 
javap -c -v -private -classpath . com.jg.zhang.Programer
 根据反编译的结果,可以下面给出示意图, 该图说明了与 computer 相对应的 field_info 是不合引用常量池的 ( 其中虚线范围内表示常量池):
 


 

class 文件中的 methods_count 和 methods

 
fields 下面的信息是 methods_count 和 methods 。 methods_count 描述的是当前的类中定义的方法的个数, 注意, 这里包括静态方法, 但不包括从父类继承的方法。 如果当前 class 文件是由一个接口生成的, 那么这里的 methods_count 描述的是接口中定义的抽象方法的数量, 我们知道, 接口中定义的方法默认都是公有的。此外需要说明的是, 编译器可能会在编译时向 class 文件增加额外的方法, 也就是说, class 文件中的方法的数量可能多于源文件中由用户定义的方法。 举例来说: 如果当前类没有定义构造方法, 那么编译器会增加一个无参数的构造函数 <init>; 如果当前类或接口中定义了静态变量, 并且使用初始化表达式为其赋值, 或者定义了 static 静态代码块, 那么编译器在编译的时候会默认增加一个静态初始化方法 < clinit> 。 
 
位于 methods_count 下面的数据叫做 methods , 可以把它看做一个数组, 数组中的每一项是一个 method_info 。这个数组中一共有 methods_count 个 method_info , 每个 method_info 都是对一个方法的描述。 下面我们详细讲解 method_info 的结构。 每个 method_info 的结构如下, 几乎和 field_info 的结构是一样的:
 

(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

name_index 下面的两个字节叫做 descriptor_index , 它同样是一个指向常量池的索引, 它描述的是当前方法的描述符。 这个索引指向常量池中的一个 CONSTANT_Utf8_info 数据项。 这个 CONSTANT_Utf8_info 数据项中存放的字符串就是当前方法的描述符(关于方法描述符, 在前面的博客中已经有过详细的讲解, 如果不明白, 请参考前面的博客: 深入理解 Java Class 文件格式(二))。 
 

(4)attributes_count 和 attributes

descriptor_index 下面是 attributes_count 和 attributes 。 这是对当前方法所具有的属性的描述。 这里的属性和源文件中的属性不是同一个概念, 在源文件测层面中, 属性是字段的另一种叫法, 希望读者不要疑惑。读者也不要轻视 class 文件中的属性, 这些属性可以描述很多的信息。 我们会在后面的文章中进行介绍。 
 
attributes_count 表示这个字段有几个属性。attributes 可以看成一个数组, 数组中的每一项都是一个 attribute_info , 每个 attribute_info 表示一个属性, 数组中一共有 attributes_count 个属性。可以出现在 method_info 中的属性有三种, 分别是 Code, Deprecated, Exceptions 和 Synthetic。 在这几个属性中, 尤其是 Code 和 Exceptions 非常重要, 这两个属性对于在 class 文件中完整描述一个方法起着至关重要的作用, 其中 Code 属性中存放方法的字节面指令,Exceptions 属性是对方法声明中抛出的异常的描述 。 这两属性以及其他一些属性, 会在下一篇文章中详细介绍, 敬请关注。
 
 
介绍完了每个 method_info 的结构, 下面我们以代码来说明, 还是使用上面的源码:
 
 
  1. package com.jg.zhang;
  2.  
  3. public class Programer extends Person{
  4.  
  5.  
  6. private Computer computer;
  7.  
  8. public Programer(Computer computer){
  9. this.computer = computer;
  10. }
  11.  
  12. public void doWork(){
  13. computer.calculate();
  14. }
  15. }
 

反编译之后, 常量池中会有如下信息(这里省略了大部分无关信息):
 
 
  1. Constant pool:
  2.  
  3. .........
  4.  
  5. #7 = Utf8 <init>
  6. #8 = Utf8 (Lcom/jg/zhang/Computer;)V
  7.  
  8. .........
  9.  
  10. #12 = Utf8 ()V
  11.  
  12. .........
  13.  
  14. #19 = Utf8 doWork
  15.  
  16. {
  17.  
  18. .........
  19.  
  20. public com.jg.zhang.Programer(com.jg.zhang.Computer);
  21. flags: ACC_PUBLIC
  22.  
  23. .........
  24.  
  25. public void doWork();
  26. flags: ACC_PUBLIC
  27.  
  28. .........
  29. }
 

由反编译结果可以看出, 该类中定义了两个方法, 其中一个是构造方法, 一个是 doWork 方法, 且这两个方法都是 public 的。 这两个方法的描述信息都存放在常量池。 其中第 7 项的 CONSTANT_Utf8_info 为构造方法的方法名, 第 8 项的 CONSTANT_Utf8_info 为构造方法的方法描述符, 第 19 项的 CONSTANT_Utf8_info 为 doWork 方法的方法名, 第 12 项的 CONSTANT_Utf8_info 为 doWork 方法的方法描述符。 
 
根据常量池中的信息, 可以得出如下的示意图, 该示意图形象的说明了 class 文件中的 method_info 是如何引用常量池中的数据项来描述当前类中定义的方法的。 图中虚线范围内表示常量池所在的区域:
 

 

总结

 
到此为止, 我们就介绍完了 class 文件中的 fields 和 methods, 进行一下总结。 
 
 fields 是对当前类中定义的字段的描述, 其中每个字段使用一个 field_info 表示, fields 中有 fields_count 个 field_info。
 
methods 是对当前类或者接口中声明的方法的描述, 其中每个方法使用一个 method_info 表示, methods 中有 methods_count 个 method_info。 
 

在本专栏的第一篇文章 深入理解 Java 虚拟机到底是什么 中, 我们主要讲解了什么是虚拟机, 这篇博客是对 JVM 的一个概述。 在随后的几篇文章中,一直在讲解 class 文件格式。 在今天这篇博客中, 将会继续讲解 class 文件中的其他信息。 在本文中, 将会讲解 class 文件中的最后一部分, 属性(attributes) 。 这里的属性和源文件中的属性不是一个概念。 在源文件中, 我们把在类中定义的字段也叫做属性。 而 class 文件中的属性, 可以看做是存储一些额外信息的数据结构。 下面我们就来介绍属性。

 

class 文件中的 attributes_count 和 attributes

 
attributes_count 位于 class 文件中 methods 的下面。 它占两个字节, 存储的是一个整数值, 表示 class 文件中属性的个数。 
 
attributes_count 下面的是 attributes, 可以把它看做一个数组, 每个数组项是一个 attribute_info , 每个 attribute_info 表示一个属性。attributes 中有 attributes_count 个 attribute_info 。
 
需要说明的是, 属性会出现在多个地方, 不仅仅出现在顶层的 ClassFile 中, 也会出现在 class 文件中的数据项中, 如出现在 field_info 中, 用来描述特定字段的一些信息, 还可以出现在 method_info 中, 用来描述特定方法的一些信息。 (关于 field_info 和 method_info 已经在上面一篇博客中介绍过, 不明白的可以参考上面的博客: 深入理解 Java Class 文件格式(七)
 
属性(attribute_info)的大概格式是这样的:
 
其中 attribute_name_index 占两个字节, 它是一个指向常量池数据项的索引。 它指向一个 CONSTANT_Utf8_info , 这个 CONSTANT_Utf8_info 中存放的是当前属性的名字。
 
attribute_name_index 下面的四个字节叫做 attribute_length, 它表示当前属性的长度, 这个长度不包括前 6 个字节, 也就是说只包括属性真实信息(也就是 info)的长度。
 
attribute_length 下面的数据是 info, 它的长度由上面提到的 attribute_length 指定, 它存放的是真实的属性数据。
 
 
 
下面我们会依次介绍一些重要属性, 相对不是很重要的属性会一笔带过。
 
 

ClassFile 中的 SourceFile 属性

 
首先介绍一个比较简单的属性:SourceFile。 该属性出现在顶层的 class 文件中。 它描述了该类是从哪个源文件中编译来的, 注意, 描述的是源文件, 而不是类, 一个源文件中可以存在多个类。 它的格式如下:
前面说过, attribute_name_index 指向常量池中的一个 CONSTANT_Utf8_info , 这个 CONSTANT_Utf8_info 中存放的是这个属性的名字字符串, 即 “SourceFile” 。 
 
attribute_length 是属性信息的长度, 这里是 2, 因为这个属性的 info 就两个字节。
 
sourcefile_index 占两个字节, 这也是为什么 attribute_length 是 2 的原因。 sourcefile_index 指向常量池中的一个 CONSTANT_Utf8_info , 这个 CONSTANT_Utf8_info 中存放的是生成该类的源文件的文件名, 这里的文件名不包括路径部分。
 
下面举例说明, 示例代码:
 
 
  1. package com.jg.zhang;
  2.  
  3. public class Person {
  4.  
  5. int age;
  6.  
  7. int getAge(){
  8. return age;
  9. }
  10. }
 

反编译后的相关信息:
 
 
  1. public class com.jg.zhang.Person
  2.  
  3. SourceFile: "Person.java"
  4.  
  5. Constant pool:
  6.  
  7. .........
  8.  
  9. #20 = Utf8 SourceFile
  10. #21 = Utf8 Person.java
  11.  
  12. .........
 

反编译结果中的  SourceFile: "Person.java"  一行是 SourceFile 属性的简单表示形式。 可以把它看做一个可读的 attribute_info 。 下面常量池中的第 20 项的 CONSTANT_Utf8_info 是对这个属性的属性名(attribute_name_index)的描述 , 第 21 项的 CONSTANT_Utf8_info 是对源文件的文件名的描述。
 
下面是图例, 注意, 虚线范围内表示常量池区域:
 
 
 
 

 

ClassFile 中的 InnerClasses 属性

 
InnerClasses 是一个存在于顶层 class 文件中的属性, 它描述的是内部类和外围类的关系。  这是一个相对来说比较复杂的属性, 因为每个类可能有多个内部类, 而这些内部类中可能还有内部类, 多层嵌套。外围类中的 InnerClasses 属性必须描述它的所有内部类, 而内部类中的 InnerClasses 也必须描述它的外围类。 
 
由于这个属性相对较为复杂, 而对于我们理解 class 文件又不具有很大的意义, 所以我们只是简单的介绍一下。 如果想深入理解这个属性, 请参考 《深入 Java 虚拟机》 第 144 到 166 页。 
 
下面是这个属性的结构:
 
 
attribute_name_index 和 attribute_length 就不过多介绍了, 和上面介绍的是一样的。
 
number_of_classes 描述的是内部类的个数。
 
classes 可以看做是一个数组, 这个数组中的每一项是一个 inner_class_info, 而每个 inner_class_info 是对一个内部类的描述。每个 inner_class_info 的结构如下:
 
 
 

Synthetic 属性

 
Synthetic 属性可以出现在 filed_info 中, method_info 中和顶层的 ClassFile 中, 分别表示这个字段, 方法或类不是有用户代码生成的(即不存在与源文件中), 而是由编译器自动添加的。 例如, 编译器会为内部类增加一个字段, 该字段是对外部类对象的引用; 如果一个不定义构造方法, 那么编译器会自动添加一个无参数的构造方法 <init>, 如果定义了静态字段或静态代码块, 还会根据具体情况, 增加静态初始化方法 < clinit> 。 此外, 有些机制, 如动态代理, 会在运行时自动生成字节码文件, 由于这些类不是由源文件中编译来的, 所以这些类的 class 文件中会有一个 Synthetic 属性。 
 
它的结构如下:
 
 
可以看到, 它没有真正的属性数据 info, 它只是一个标志性的属性, 用来表示它所在的字段, 方法或类是由编译器自动添加的 。 
 
下面以实例代码来说明, 源码如下:
 
  1. package com.jg.zhang;
  2.  
  3. public class Person {
  4.  
  5. static{
  6. System.out.println("static");
  7.  
  8. }
  9.  
  10. int age;
  11.  
  12. int getAge(){
  13. return age;
  14. }
  15. }
 

反编译后的相关信息如下:
 
 
  1. {
  2. int age;
  3. flags:
  4.  
  5.  
  6. static {};
  7.  
  8. .........
  9.  
  10. public com.jg.zhang.Person();
  11.  
  12. .........
  13.  
  14. int getAge();
  15.  
  16. .........
  17. }
 

由反编译结果可以看出, 编译器自动生成了静态初始化方法和构造方法。 可能是因为 Synthetic 属性是可选的(也就是说某个版本的编译器可以选择不加入 Synthetic 属性) ,所以在反编译后的结果中没有发现 Synthetic 属性。  
 
 

ConstantValue 属性

 
ConstantValue 属性出现在 class 文件中的 field_info 中, 也就是说它是一个和字段相关的属性。 每个 field_info 中最多只能出现一个 ConstantValue 属性。 此外, 要注意的是, 必须是静态字段才可以有 ConstantValue 属性。 这个静态字段可以是 final 的, 也可以不是 final 的。 
 
这个属性为静态变量提供了另一种初始化的方式。 静态变量初始化的方式有两种, 一种就是现在要讲得 ConstantValue 属性, 另一种就是静态初始化方法 <clinit> 不同的编译器和虚拟机可以有不同的实现方式。 但是如果虚拟机决定使用 ConstantValue 属性为静态变量赋值, 那么为这个变量的赋值动作, 必须位于执行 < clinit > 方法之前。 
 
此外, 只有基本数据类型或 String 类型的静态变量才可以存在 ConstantValue 属性, 原因在下面会有说明。 
 
下面介绍它的结构:
 
 
attribute_name_index 和 attribute_length 就不过多介绍了, 和上面介绍的是一样的。这里的 attribute_length 为 2 。 
 
位于 attribute_length 之下的是 constantvalue_index , 这是一个指向常量池中某个数据项的索引。这个常量池数据项中存放的就是当前字段的值。
 
 这个常量池中的数据项,根据 field_info 描述的字段的不同, 可以是不同类型的数据项, 如果当前字段是 byte, short, char, int, boolean 类型, 那么这个被指向的常量池数据项就会是一个 CONSTANT_Integer_info , 如果当前字段是一个 long 类型的字段, 那么这个被指向的常量池数据项就会是一个 CONSTANT_Long_info 。 如果当前字段是是一个 String 类型的字段 , 那么这个被指向的常量池数据项就是一个 CONSTANT_String_info 。 这里有一点需要说明, 虽然 java 语言支持 byte, short, char, boolean 类型, 但是 JVM 却不支持这几种类型, 表现在 class 文件中就是, class 文件中的常量池中没有和这几个数据类型相对应的数据项, 这几中类型都被 JVM 在执行时当做 int 来对待, 表现在 class 文件中就是, 这几种类型都对应常量池中的 CONSTANT_Integer_info 数据项。 
 
这也说明了, 为什么只有基本数据类型和 String 类型的静态常量才会存在 ConstantValue 属性 。 因为 constantvalue_index 只是一个指向常量池的索引, 而其他引用类型的常量不会存在于常量池中。
 
下面以实例来说明, 实例代码如下:
 
  1. package com.jg.zhang;
  2.  
  3. public class Person {
  4.  
  5. static final int a = 1;
  6.  
  7. int age;
  8.  
  9. int getAge(){
  10. return age;
  11. }
  12. }
 

反编译后的相关结果如下:
 
  1. ......
  2.  
  3. Constant pool:
  4.  
  5. #7 = Utf8 ConstantValue
  6. #8 = Integer 1
  7.  
  8.  
  9. {
  10. static final int a;
  11. flags: ACC_STATIC, ACC_FINAL
  12. ConstantValue: int 1
  13.  
  14. .........
  15. }
 

可以看到, 源文件中的 a 字段, 是 static final 的, 所以编译器为这个字段的 filed_info 生成了 ConstantValue 属性。 这个属性的示意图如下所示, 注意, 虚线范围内表示常量池区域:
 
 
 
 

Deprecated 属性

Deprecated 属性可以存在于 filed_info 中, method_info 中和顶层的 ClassFile 中, 分别表示这个字段, 方法或类已经过时。 这个属性用来支持源文件中的 @deprecated 注解。 也就是说, 如果在源文件中为一个字段, 方法或类标注了 @deprecated 注解, 那么编译器就会在 class 文件中为这个字段, 方法或类生成一个 Deprecated 属性 。
 
Deprecated 属性的格式如下:
 
 
和上面的属性一样, attribute_name_index 属性指向一个常量池中的 CONSTANT_Utf8_info 。 这个 CONSTANT_Utf8_info 中存放着该属性的名字 “Deprecated” 。 
 
attribute_length 永远为 0 , 因为这个属性只是一个标志信息, 用来表示字段, 方法, 类已经过时, 而不具有任何实质性的属性信息。
 
下面以代码示例来说明, 代码如下:
 
 
  1. package com.jg.zhang;
  2.  
  3. public class Person {
  4.  
  5. int age;
  6.  
  7. @Deprecated
  8. int getAge(){
  9. return age;
  10. }
  11. }
 

在 getAge 方法上使用了 @deprecated 。 下面是反编译之后的相关信息:
 
 
  1. ......
  2.  
  3. Constant pool:
  4. ......
  5.  
  6. #18 = Utf8 Deprecated
  7.  
  8. ......
  9.  
  10. {
  11.  
  12. ......
  13.  
  14. int getAge();
  15. flags:
  16. Deprecated: true
  17.  
  18. ......
  19.  
  20. }
 

可以看到, 在 getAge 方法相关的信息中, 有一行 Deprecated: true , 这说明编译器在 getAge 方法的 method_info 中加入了 Deprecated 属性。 常量池第 18 项的 CONSTANT_Utf8_info 中存放的是 Deprecated 属性的属性名 “Deprecated” 。 
 
下面是示意图, 虚线范围内表示常量池区域:
 
 
 

总结

 
本文就到此为止。 在本文中, 主要讲解了 class 文件中的一些属性。 这些属性可以出现在 class 文件中的对个地方, 用来描述一些其他信息。 
 
在下一篇博客中, 会继续讲解其他属性。 下一篇博客要讲解的属性相对比较重要, 因为这些属性主要是和方法相关的。 到目前为止, 我们已经讲解了 class 文件中的大部分信息, 包括常量池, this_class, super_class, field_info, method_info 等等。 虽然 method_info 是对一个方法的描述, 但是目前我们知道的而关于 method_info 的信息, 只描述了方法的方法名, 描述符等签名信息。 但是方法中还包括很多重要信息, 比如字节码指令, 异常处理块, 方法声明抛出的异常 等。 这些重要信息在 class 文件中是如何描述的呢? 下一篇博客将会揭晓答案, 敬请关注。 

经过前八篇关于 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 属性

 
code 属性是方法的一个最重要的属性。 因为它里面存放的是方法的字节码指令, 除此之外还存放了和操作数栈,局部变量相关的信息。 所有不是抽象的方法, 都必须在 method_info 中的 attributes 中有一个 Code 属性。下面是 Code 属性的结构, 为了更直观的展示 Code 属性和 method_info 的包含关系, 特意画出了 method_info:
 
 
 
下面依次介绍 code 属性中的各个部分。
 
和上一篇博客中介绍的其他属性一样,attribute_name_index 指向常量池中的一个 CONSTANT_Utf8_info , 这个 CONSTANT_Utf8_info 中存放的是当前属性的名字 “Code” 。
 
attribute_length 给出了当前 Code 属性的长度(不包括前六字节)。
 
max_stack 指定当前方法被执行引擎执行的时候, 在栈帧中需要分配的操作数栈的大小。
 
max_locals 指定当前方法被执行引擎执行的时候, 在栈帧中需要分配的局部表量表的大小。注意, 这个数字并不是局部变量的个数, 因为根据局部变量的作用域不同, 在执行到一个局部变量以外时, 下一个局部变量可以重用上一个局部变量的空间(每个局部变量在局部变量表中占用一个或两个 Slot)。 方法中的局部变量包括方法的参数, 方法的默认参数 this, 方法体中定义的变量, catch 语句中的异常对象。 关于执行引擎的相关内容会在后面的博客中讲到。
 
code_length 指定该方法的字节码的长度, class 文件中每条字节码占一个字节。
 
code 存放字节码指令本身, 它的长度是 code_length 个字节。
 
exception_table_length 指定异常表的大小
 
exception_table 就是所谓的异常表, 它是对方法体中 try-catch_finally 的描述。 exception_table 可以看做是一个数组, 每个数组项是一个 exception_info 结构, 一般来说每个 catch 块对应一个 exception_info,编译器也可能会为当前方法生成一些 exception_info。 exception_info 的结构如下(为了直观的显示 exception_info, exception_table 和 Code 属性的关系, 画出了 Code 属性,的话读者就会更清楚各个数据项之间的位置关系和包含关系):
 
 
下面讲解 exception_info 中的各个字段的意思。
 
start_pc 是从字节码(Code 属性中的 code 部分)起始处到当前异常处理器起始处的偏移量。
 
end_pc 是从字节码起始处到当前异常处理器末尾的偏移量。
 
handler_pc 是指当前异常处理器用来处理异常(即 catch 块)的第一条指令相对于字节码开始处的偏移量。
 
catch_type 是一个常量池索引, 指向常量池中的一个 CONSTANT_Class_info 数据项, 该数据项描述了 catch 块中的异常的类型信息。这个类型必须是 java.lang.Throwable 的或其子类。
 
所以可以总结, 一个异常处理器(exception_info)的意思是: 如果偏移量从 start_pc 到 end_pc 之间的字节码出现了 catch_type 描述的类型的异常, 那么就跳转到偏移量为 handler_pc 的字节码处去执行。如果 catch_type 为 0, 就代表不引用任何常量池项(再回顾一下, 常量池中的项是从 1 开始计的), 那么这个 exception_info 用于实现 finally 子句。
 
我们一直在介绍 Code 属性, 只不过刚才进行了一个小插曲, 介绍了 Code 属性中的 exception_table 中的 exception_info 的详细信息。 下面我们继续介绍 Code 属性中的其他信息, 希望读者不要被绕晕了 : )
 
attributes_count 表示当前 Code 属性中存在的其他属性的个数。 现在我们知道, class 中的属性, 不仅会出现在顶层的 class 中, 会存在 field_info 中, 会存在 method_info 中, 甚至还会出现在属性中。 
 
attributes 可以看做是一个数组, 里面存放了 Code 属性中的其他属性。 Code 属性中可以出现的其他属性有 LineNumberTable 和 LocalVariableTable 。 这两个属性会在下面介绍。
 
 
 

LineNumberTable 属性

 
LineNumberTable 属性存在于 Code 属性中, 它建立了字节码偏移量到源代码行号之间的联系。 这个属性是可选的, 编译器可以选择不生成该属性。下面是该属性的结构(同样给出了全局的位置关系,LineNumberTable 在图的右下角部分):
 
 
由于这个属性并不是重点, 我们在此简单的讲述。 
 
每个 LineNumberTable 中的 line_number_table 部分, 可以看做是一个数组, 数组的每项是一个 line_number_info , 每个 line_number_info 结构描述了一条字节码和源码行号的对应关系。 其中 start_pc 是这个 line_number_info 描述的字节码指令的偏移量, line_number 是这个 line_number_info 描述的字节码指令对应的源码中的行号。可以看出, 方法中的每条字节码都对应一个 line_number_info , 这些 line_number_info 中的 line_number 可以指向相同的行号, 因为一行源码可以编译出多条字节码。
 
 
 

LocalVariableTable 属性 

 
LocalVariableTable 属性建立了方法中的局部变量与源代码中的局部变量之间的对应关系。 这个属性存在于 Code 属性中。 这个属性是可选的, 编译器可以选择不生成这个属性。该属性的结构如下:(同样给出了全局的位置关系图,LocalVariableTable 在该图的右下角 )
 
 
由于这个属性相对不那么重要, 这里只是大概讲解一下。
 
每个 LocalVariableTable 的 local_variable_table 部分可以看做是一个数组, 每个数组项是一个叫做 local_variable_info 的结构, 该结构描述了某个局部变量的变量名和描述符, 还有和源代码的对应关系。下面讲解 local_variable_info 的各个部分:
 
start_pc 是当前 local_variable_info 所对应的局部变量的作用域的起始字节码偏移量;
 
length 是当前 local_variable_info 所对应的局部变量的作用域的大小。 也就是从字节码偏移量 start_pc 到 start_pc+length 就是当前局部变量的作用域范围;
 
name_index 指向常量池中的一个 CONSTANT_Utf8_info, 该 CONSTANT_Utf8_info 描述了当前局部变量的变量名;
 
descriptor_index 指向常量池中的一个 CONSTANT_Utf8_info, 该 CONSTANT_Utf8_info 描述了当前局部变量的描述符;
 
index 描述了在该方法被执行时,当前局部变量在栈帧中局部变量表中的位置。 
 
由此可知, 方法中的每个局部变量都会对应一个 local_variable_info 。 
 
 

Exceptions 属性

 
首先需要说明, Exceptions 属性不是存在于 Code 属性中的, 它存在于 method_info 中的 attributes 中。 和 Code 属性是平级的。 这个属性描述的是方法声明的可能会抛出的异常, 也就是方法定义后面的 throws 声明的异常列表, 请不要和上面提到的异常处理器混淆。 异常处理器描述了方法的字节码如何处理异常, 而 Exceptions 属性描述方法可能会抛出哪些以异常。下面讲解 Exceptions 属性的结构(左下角为 Exceptions 属性):
 
 
下面讲解 Exceptions 属性中的信息。 
 
attribute_name_index 和 attribute_length 就不多说了, 和其他属性是一样的。 
 
number_of_exceptions 是该方法要抛出的异常的个数。 
 
exceptions_index_table 可以看做一个数组, 这个数组中的每一项占两个字节, 这两个字节是对常量池的索引, 它指向一个常量池中的 CONSTANT_Class_info。 这个 CONSTANT_Class_info 描述了一个被抛出的异常的类型。 
 
 

总结

到此为止, 和方法相关的属性就介绍完了。 这篇博客讲解的内容相对比较复杂。 下面以一个实例进行验证, 实例代码:
 
 
  1. package com.jg.zhang;
  2.  
  3. public class Test {
  4.  
  5. public void test() throws Exception{
  6.  
  7. int localVar = 0;
  8.  
  9. try{
  10.  
  11. Class.forName("com.jg.zhang.Person");
  12.  
  13. }catch(ClassNotFoundException e){
  14.  
  15. throw e;
  16. }finally{
  17. System.out.println(localVar);
  18. }
  19.  
  20. }
  21. }
 

反编译后的 test 方法部分(省略了常量池等信息):
 
 
  1. public void test() throws java.lang.Exception;
  2. flags: ACC_PUBLIC
  3. Exceptions:
  4. throws java.lang.Exception
  5. Code:
  6. stack=2, locals=4, args_size=1
  7. 0: iconst_0
  8. 1: istore_1
  9. 2: ldc #18 // String com.jg.zhang.Person
  10. 4: invokestatic #20 // Method java/lang/Class.forName:(Ljava/lang/String;)Ljava/lang/Class;
  11. 7: pop
  12. 8: goto 24
  13. 11: astore_2
  14. 12: aload_2
  15. 13: athrow
  16. 14: astore_3
  17. 15: getstatic #26 // Field java/lang/System.out:Ljava/io/PrintStream;
  18. 18: iload_1
  19. 19: invokevirtual #32 // Method java/io/PrintStream.println:(I)V
  20. 22: aload_3
  21. 23: athrow
  22. 24: getstatic #26 // Field java/lang/System.out:Ljava/io/PrintStream;
  23. 27: iload_1
  24. 28: invokevirtual #32 // Method java/io/PrintStream.println:(I)V
  25. 31: return
  26. Exception table:
  27. from to target type
  28. 2 8 11 Class java/lang/ClassNotFoundException
  29. 2 14 14 any
  30. LineNumberTable:
  31. line 7: 0
  32. line 11: 2
  33. line 13: 8
  34. line 15: 12
  35. line 16: 14
  36. line 17: 15
  37. line 18: 22
  38. line 17: 24
  39. line 20: 31
  40. LocalVariableTable:
  41. Start Length Slot Name Signature
  42. 0 32 0 this Lcom/jg/zhang/Test;
  43. 2 30 1 localVar I
  44. 12 2 2 e Ljava/lang/ClassNotFoundException;
 


结合上面的讲解和图解, 再分析反编译的结果, 就一目了然了: 所有的结果是一个 method_info, method_info 开始处是访问标志信息。 然后是 method_info 的 Exceptions 属性 , Exceptions 属性属性下面是 Code 属性, Code 属性中又包括字节码, 异常处理器 ,LineNumberTable 属性和 LocalVariableTable 属性。 
 
由于这篇博客讲解的内容大多和方法有关, 所以会直接或间接的和 method_info 有联系, 最后给出一张全局图, 这样的话, 读者就比较明确, 一个完整的方法, 是如何在 class 文件中描述的,由于考虑到复杂性, 这些属性或其他数据项中, 对常量池的引用均未画出:
 
 
 
posted @ 2024-06-26 14:17  CharyGao  阅读(99)  评论(0编辑  收藏  举报