前言:

在前几篇文章中:
Java虚拟机——字节码、机器码和JVMJava虚拟机——类加载机制和类加载器Java虚拟机—堆、栈、运行时数据区
我们大概介绍了JVM、字节码、类加载器和JVM运行时数据区的概念,现在让我们进入JVM的重要部分—.class文件的结构。所以本篇文章的主题主要包含以下2个部分:
1.Java语言的平台无关性和JVM的语言无关性
2.字节码.class文件的结构


1.Java语言的平台无关性和JVM的语言无关性
Java语言的平台无关性
《深入理解Java虚拟机-第二版》中第6章开头就写到:
代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步。
为什么这么说呢?作者下面也解释的很明白。因为在虚拟机出现之前,程序要想正确运行在计算机上,首先要将代码编译成二进制本地机器码,而这个过程是和电脑的操作系统OS、CPU指令集强相关的,所以可能代码只能在某种特定的平台下运行,而换一个平台或操作系统就无法正确运行了。随着虚拟机的出现,直接将程序编译成机器码,已经不再是唯一的选择了。越来越多的程序语言选择了与操作系统和机器指令集无关的、平台中立的格式作为程序编译后的存储格式。Java就是这样一种语言。“一次编写,到处运行”于是成立Java的宣传口号。
正是虚拟机和字节码(ByteCode)构成了平台无关性的基石,从而实现“一次编写,到处运行”
Java虚拟机将.java文件编译成字节码,而.class字节码文件经过JVM转化为当前平台下的机器码后再进行程序执行。这样,程序猿就无需重复编写代码来适应不同平台了,而是一套代码处处运行,至于字节码怎样转化成对应平台下的机器码,那就是Java虚拟机的事情了。
JVM的语言无关性
Java语言通过JVM虚拟机和字节码(ByteCode)实现了平台无关性,那么语言无关性又是什么意思?其实,在Java虚拟机设计之初,作者非常前瞻性的说过:
“In the future,we will consider bounded extensions to the Java virtual machine to provide better support for other languages” 在未来,我们会对java虚拟机进行适当的拓展,以便更好的支持其他语言运行于JVM之上。
时至今日,商业机构和开源机构以及在Java语言之外发展出一大批在Java虚拟机之上运行的语言,如Groovy,JRuby,Jython,Scala等等。这些语言通过各自的编译器编译成为.class文件,从而可以被JVM所执行。
Java Class文件结构 - 图1
所以,由于Java虚拟机设计之初的定位,以及字节码(ByteCode)的存在,使得JVM可以执行不同语言下的字节码.class文件,从而构成了语言无关性的基础。或许在未来,语言无关性的优势会赶超Java平台无关性的优势。。。

2.字节码.class文件的结构

根据Java虚拟机的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构只有2种数据类型:无符号数和表。
当然无论是无符号数还是表,Class文件都是以8位(8bit),一个字节为单位存储的,各个数据项目紧密无间隔排列的二进制流。当数据项长度超过8位时,按照高位在前(Big Endian)的方式分隔成若干个8位字节存储。
无符号数用u表示后面跟1、2、4、8代表1个字节、2个字节、4个字节、8个字节。无符号数用来描述数字、索引引用、数量值、字符串值。
表则是由无符号数或者其他表作为数据复合而成的数据类型,所有表都习惯以_info结尾。
整个Class文件实质上就是一张表,其中的数据项由各个子表和无符号数构成。Class文件的格式如下:
Java Class文件结构 - 图2
此处需要注意的是,由于class文件没有任何分隔符号,所有.class文件中所有的数据项(表或无符号数)都是按照图表中的顺序依次排列好的,所以我们可以在.class文件中依照字节的顺序来查看对应数据项的详细信息。
这里我们以一个.class文件为例,看看其具体的字节信息,源码如下:

  1. package JustCoding.Practise;
  2. public class ConstantPool {
  3. private static String a = "Class";
  4. public int VERSION = 100;
  5. private static void test1(String s){
  6. String b = "Method ";
  7. String c = b + s;
  8. System.out.println("合并后的字符串:"+c);
  9. }
  10. public static void main(String[] args){
  11. test1(a);
  12. }
  13. }

用vim打开其.class文件查看其16进制文件如下:
Java Class文件结构 - 图3

魔数magic

如上图所示,是ConstantPool.class文件的16进制表示,前4个字节为ca fe ba be,这个即为表6-1中的magic,magic译为“魔数”,在.class文件的头4个字节,它的唯一作用是确定这个文件是否是能够被虚拟机识别的class文件,其值是固定的为0xCAFEBABE(咖啡宝贝),这个也是Java语言中一段有意思的“黑”历史了,哈哈

版本声明major_version、minor_version

紧挨着魔数后的第5、6两个字节存储的是minor_version,即Java的次版本号,第7、8两个字节是major_version主版本号。可以看见这里此版本号为0x0000,主版本号为0x0034。
每个Java版本都有对应的主、次版本号可以查询。例子中的0x0034对应10进制的52,表示JDK的主版本号为1.8。

常量池计数项constant_pool_count

版本声明后,是一个2个字节的无符号数u2用于标志常量池容量,此处0x0040,等于10进制下的64,表明常量池中有63项常量。
(此处有个小设计,容量计数是从1开始而不是从0开始,故64-1=63)

常量池表constant_pool

接着就到了常量池表cp_info。此处常量池表就是之前文章Java虚拟机—堆、栈、运行时数据区中提到的,方法区中的运行时常量池。
5.1运行时常量池运行时常量池(Runtime Constant Pool)是.class文件中每一个类或接口的常量池表(constant pool table)的运行时表示形式,属于方法区的一部分。每一个运行时常量池都在Java虚拟机的方法区中分配,在加载类和接口道虚拟机后,就创建对应的运行时常量池。常量池的作用是:存放编译器生成的各种字面量和符号引用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建或运行时解析、翻译到具体的内存地址之中。字面量(Literal),通俗理解就是Java中的常量,如文本字符串、声明为final的常量值等。符号引用(Symbolic References)则是属于编译原理中的概念,包括了下面三类常量:1.类和接口的全限定名2.字段的名称和描述符3.方法的名称和描述符
Java Class文件结构 - 图4
常量池可以理解为class文件中的资源仓库,它是class文件结构中与其他项目关联最多的数据类型,也是占用class空间最大的一个数据项。
因为常量池中常量的数量不是固定的,所以需要2字节的无符号u2(constant_pool_count)代表常量池容量计数值(此处有个小设计,容量计数是从1开始而不是从0开始)。
常量池中的每一项常量都是一个表。每个常量项表中第一位是一个u1类型的标志位,用于标志常量的类型,具体各个常量表如下图所示(目前有14种类型的常量,表中只列了11项):
Java Class文件结构 - 图5
图片引用自:http://www.sohu.com/a/131458551_504186
到此,我们来看一下用javap -v ConstantPool.class反编译一下.class文件来看看字节码的组成情况:

  1. Classfile xxx/.../ConstantPool.class
  2. Last modified 2018920日; size 1063 bytes
  3. MD5 checksum 024d748f4dc1776164f6c3e8e19cf95b
  4. Compiled from "ConstantPool.java"
  5. public class JustCoding.Practise.ConstantPool
  6. minor version: 0
  7. major version: 52
  8. flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  9. this_class: #14 // JustCoding/Practise/ConstantPool
  10. super_class: #15 // java/lang/Object
  11. interfaces: 0, fields: 2, methods: 4, attributes: 1
  12. Constant pool:
  13. #1 = Methodref #15.#39 // java/lang/Object."<init>":()V
  14. #2 = Fieldref #14.#40 // JustCoding/Practise/ConstantPool.VERSION:I
  15. #3 = String #41 // Method
  16. #4 = Class #42 // java/lang/StringBuilder
  17. #5 = Methodref #4.#39 // java/lang/StringBuilder."<init>":()V
  18. #6 = Methodref #4.#43 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  19. #7 = Methodref #4.#44 // java/lang/StringBuilder.toString:()Ljava/lang/String;
  20. #8 = Fieldref #45.#46 // java/lang/System.out:Ljava/io/PrintStream;
  21. #9 = String #47 // 合并后的字符串:
  22. #10 = Methodref #48.#49 // java/io/PrintStream.println:(Ljava/lang/String;)V
  23. #11 = Fieldref #14.#50 // JustCoding/Practise/ConstantPool.a:Ljava/lang/String;
  24. #12 = Methodref #14.#51 // JustCoding/Practise/ConstantPool.test1:(Ljava/lang/String;)V
  25. #13 = String #52 // Class
  26. #14 = Class #53 // JustCoding/Practise/ConstantPool
  27. #15 = Class #54 // java/lang/Object
  28. #16 = Utf8 a
  29. #17 = Utf8 Ljava/lang/String;
  30. #18 = Utf8 VERSION
  31. #19 = Utf8 I
  32. #20 = Utf8 <init>
  33. #21 = Utf8 ()V
  34. #22 = Utf8 Code
  35. #23 = Utf8 LineNumberTable
  36. #24 = Utf8 LocalVariableTable
  37. #25 = Utf8 this
  38. #26 = Utf8 LJustCoding/Practise/ConstantPool;
  39. #27 = Utf8 test1
  40. #28 = Utf8 (Ljava/lang/String;)V
  41. #29 = Utf8 s
  42. #30 = Utf8 b
  43. #31 = Utf8 c
  44. #32 = Utf8 main
  45. #33 = Utf8 ([Ljava/lang/String;)V
  46. #34 = Utf8 args
  47. #35 = Utf8 [Ljava/lang/String;
  48. #36 = Utf8 <clinit>
  49. #37 = Utf8 SourceFile
  50. #38 = Utf8 ConstantPool.java
  51. #39 = NameAndType #20:#21 // "<init>":()V
  52. #40 = NameAndType #18:#19 // VERSION:I
  53. #41 = Utf8 Method
  54. #42 = Utf8 java/lang/StringBuilder
  55. #43 = NameAndType #55:#56 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  56. #44 = NameAndType #57:#58 // toString:()Ljava/lang/String;
  57. #45 = Class #59 // java/lang/System
  58. #46 = NameAndType #60:#61 // out:Ljava/io/PrintStream;
  59. #47 = Utf8 合并后的字符串:
  60. #48 = Class #62 // java/io/PrintStream
  61. #49 = NameAndType #63:#28 // println:(Ljava/lang/String;)V
  62. #50 = NameAndType #16:#17 // a:Ljava/lang/String;
  63. #51 = NameAndType #27:#28 // test1:(Ljava/lang/String;)V
  64. #52 = Utf8 Class
  65. #53 = Utf8 JustCoding/Practise/ConstantPool
  66. #54 = Utf8 java/lang/Object
  67. #55 = Utf8 append
  68. #56 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
  69. #57 = Utf8 toString
  70. #58 = Utf8 ()Ljava/lang/String;
  71. #59 = Utf8 java/lang/System
  72. #60 = Utf8 out
  73. #61 = Utf8 Ljava/io/PrintStream;
  74. #62 = Utf8 java/io/PrintStream
  75. #63 = Utf8 println
  76. {
  77. public int VERSION;
  78. descriptor: I
  79. flags: (0x0001) ACC_PUBLIC
  80. public JustCoding.Practise.ConstantPool();
  81. descriptor: ()V
  82. flags: (0x0001) ACC_PUBLIC
  83. Code:
  84. stack=2, locals=1, args_size=1
  85. 0: aload_0
  86. 1: invokespecial #1 // Method java/lang/Object."<init>":()V
  87. 4: aload_0
  88. 5: bipush 100
  89. 7: putfield #2 // Field VERSION:I
  90. 10: return
  91. LineNumberTable:
  92. line 3: 0
  93. line 7: 4
  94. LocalVariableTable:
  95. Start Length Slot Name Signature
  96. 0 11 0 this LJustCoding/Practise/ConstantPool;
  97. public static void main(java.lang.String[]);
  98. descriptor: ([Ljava/lang/String;)V
  99. flags: (0x0009) ACC_PUBLIC, ACC_STATIC
  100. Code:
  101. stack=1, locals=1, args_size=1
  102. 0: getstatic #11 // Field a:Ljava/lang/String;
  103. 3: invokestatic #12 // Method test1:(Ljava/lang/String;)V
  104. 6: return
  105. LineNumberTable:
  106. line 15: 0
  107. line 16: 6
  108. LocalVariableTable:
  109. Start Length Slot Name Signature
  110. 0 7 0 args [Ljava/lang/String;
  111. static {};
  112. descriptor: ()V
  113. flags: (0x0008) ACC_STATIC
  114. Code:
  115. stack=1, locals=0, args_size=0
  116. 0: ldc #13 // String Class
  117. 2: putstatic #11 // Field a:Ljava/lang/String;
  118. 5: return
  119. LineNumberTable:
  120. line 5: 0
  121. }
  122. SourceFile: "ConstantPool.java"

可以看到,minor version: 0 major version: 52;Constant pool共有63项。和我们之前看16进制码时是一一对应的。

访问标志access_flags

在常量池表后面的两个字节代表访问标志,用于标志类或接口层次的访问信息。如:这个Class文件是类还是接口?是否是public?是否为抽象的abstract?是否为final的等。

类索引this_class、父类索引super_class和接口索引集合intefaces

类索引用于确定此类的全限定名称:JustCoding/Practise/ConstantPool,父类索引superclass用于确定这个类父类的全限定名:java/lang/Object。接口索引集合intefaces用来描述这个类实现了哪些接口。
类索引this__class、父类索引super_class都是一个u2类型的数据,接口索引集合包含一个u2类型的接口
计数项intefaces_count和若干个u2类型的数据集合。

字段表集合fields_count+field_info

字段表集合用于描述接口或类中声明的变量。字段filed包括类变量、实例变量,但不包括方法内部声明的局部变量。字段表结构如下:
Java Class文件结构 - 图6
字段表集合中第一项是access_flags,需要注意的是,这里的access_flags和之前类中的access_flags类似,是一个u2类型的数据,表示字段访问标记,可以设置9个标记位用于标记字段是否为:public,private,protected,static,final,volatile,transient,enum,是否由编译器自动产生。
然后是name_index和descriptor_index,他们分别代表字段的简单名称和字段OR方法的描述符。方法表集合用于存储此类或接口中包含的方法,表结构和字段表类似。简单名称是指没有类型修饰、没有参数修饰的字段OR方法名称。简单名称很好理解,在例子中有:a ,VERSION ,test1。描述符descriptor则稍微麻烦点,描述符的作用是用来描述字段的数据类型、方法参数列表和返回值。例子中描述符有:I , ()V,([Ljava/lang/String;)V这几个。
最后是属性表集合attributes_count+attribute_info,用于记录一些属性。

方法表集合methodscount+methodinfo

和字段表集合类似,此处需要注意的是,通过访问标志accessflags、名称索引nameindex、描述符索引descriptorindex来定义了方法,方法的实际代码存放在属性表attribute_info中的“Code”属性中。

属性表集合attributes_count+attribute_info

属性表在前面已经出现了多次,在class文件、字段表、方法表中都可以包含自己的属性表集合用于描述自己特定的属性。class文件中其他的数据项对顺序、长度和内容要求十分严格,而对属性表则相对宽松,不再要求属性表具有严格顺序,且只要不和已有属性重名,即可向属性表中写入自己定义的属性。JVM运行时会忽略掉它所不认识的属性。
Java虚拟机规范(Java8)中预定义了23种属性,按照不同分类,大致可分为3类:
Java Class文件结构 - 图7
Java Class文件结构 - 图8
熟悉了这些属性,学习了class文件结构,这时再用javap -v xxx.class命令来反编译一下字节码,看上去就清晰多了。所以下一篇文章我们就来对着反编译后的.class文件来继续学习和讲解JVM字节码指令,毕竟JVM指令才是整个JVM的核心。