博客备案了一个月,中途也有各种工作的事情,现在继续更新。 前面从比较远的角度介绍了JVM的一些小常识, 如果没看懂也没关系,毕竟学习的过程就是 先感性认识,后才能升华到理性认识。所谓书读百遍其义自见也是类似的道理。

这次谈谈Java的平台无关性的基石: Class文件。

1. 简介

机器是只认机器码的,所谓的机器码, 就是机器认的一堆有特殊意义的二进制指令。 这些二进制指令的语义是由具体机器CPU的指令集架构(Instruction Set Architecture)来规定的。这些指令集都是和具体的CPU系列是强相关的,比如x86的指令集换到arm上就玩不转。这对程序的可移植性和维护性都带来了很大的挑战,同一份逻辑要散落在各个平台, 很肯能就漏改了某一处逻辑。

所以过去那个年代有人提出了write once, run anywhere的口号,然后java诞生了。java的跨平台性是靠jvm来提供的。 JVM是一层用来屏蔽平台无关性的抽象, JVM本身是和平台强相关的, JVM作为虚拟的机器,也抽象出了一套自己的“指令集”. 但是这些指令又不是机器码一样能直接执行,还需要一些额外的信息,例如常量,栈帧,异常等信息, 以满足java等高级语言的语法和语义 这些信息就都存在class文件里。

事实上,在java发展之初,设计者就考虑过让其他语言运行在jvm,<<Java虚拟机规范>><<Java语言规范>>是分开发布的,第一版jvm规范也说过: in the future, we will consider bounded extensions to the java virtual machine to provide better support for other languages.

class可能的设计思路

class文件作为实现平台无关性的基石,猜测要做到以下几点

  • 语义: 能完整的表达出Java的语义
  • 紧凑: 早期的计算机内存很小,性能很差,class文件需要尽量占少的空间, 尽可能小
  • 安全性: 既要保证java的语法语义不被破坏,又要确保错误的格式得到虚拟机正确的处理
  • 可扩展: java在发展,class也要跟上

2. class文件的结构

基于以上原则,class文件是以二进制形式存储,并严格的规范了各个字段的语义, 兼容性和检查方法 每个class文件都只代表一个类或者接口,java中一个类的信息主要有字段(fields)方法(method), 还有继承自哪个类,实现了哪些接口等等 class文件也对应的给出了存储的结构.

jvm8规范给出的结构如下:

// u代表unsigned byte, 无符号字节, 占8位
ClassFile {
    u4             magic;
    u2             minor_version;
    u2             major_version;
    u2             constant_pool_count;
    cp_info        constant_pool[constant_pool_count-1];
    u2             access_flags;
    u2             this_class;
    u2             super_class;
    u2             interfaces_count;
    u2             interfaces[interfaces_count];
    u2             fields_count;
    field_info     fields[fields_count];
    u2             methods_count;
    method_info    methods[methods_count];
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

其中magic代表文件类型,任何文件的前4个字节都是magic number, java的魔数是0xCAFEBABE, 咖啡宝贝, 程序员式的浪漫:) minor_versionmajor_version代表的是class的版本号,虚拟机会根据版本号做一些兼容性判断,比如1.4的虚拟机不能执行1.8的字节码 后面的this_class,super_class, interfaces, fields, methods存的就是当前类名,父类名,实现的接口们,字段和方法大全,都是诸位 javaer耳熟能详的东西。

常量池

上面有一块比较重要,在虚拟机规范里甚至单独提出来作为运行时内存区域的一部分, 这部分就是运行时常量池 常量池数据是源自class文件中的常量池的,也就是上面结构里的constant_pool, java源码里的方法签名,裸字符串,数字, 方法引用, 字段引用等都会被编译后放到 class文件的这个结构里。

举个栗子:

public class HelloWorld implements java.io.Serializable{
  private String str = "Hello";

  @Deprecated
  public void testMethod() {
  }
}

javap -v HelloWorld反编译之后可以得到

Constant pool:
   #1 = Methodref          #5.#19         // java/lang/Object."<init>":()V
   #2 = String             #20            // Hello
   #3 = Fieldref           #4.#21         // HelloWorld.str:Ljava/lang/String;
   #4 = Class              #22            // HelloWorld
   #5 = Class              #23            // java/lang/Object
   #6 = Class              #24            // java/io/Serializable
   #7 = Utf8               str
   #8 = Utf8               Ljava/lang/String;
   #9 = Utf8               <init>
  #10 = Utf8               ()V
  #11 = Utf8               Code
  #12 = Utf8               LineNumberTable
  #13 = Utf8               testMethod
  #14 = Utf8               Deprecated
  #15 = Utf8               RuntimeVisibleAnnotations
  #16 = Utf8               Ljava/lang/Deprecated;
  #17 = Utf8               SourceFile
  #18 = Utf8               HelloWorld.java
  #19 = NameAndType        #9:#10         // "<init>":()V
  #20 = Utf8               Hello
  #21 = NameAndType        #7:#8          // str:Ljava/lang/String;
  #22 = Utf8               HelloWorld
  #23 = Utf8               java/lang/Object
  #24 = Utf8               java/io/Serializable

方法名testMethod, 字段名str 等都被编译成了utf8属性,也就是字符串常量 方法签名变成了()V, 左边代表参数类型,右边代表返回值,V就是void, J代表Long(L已经被数组用掉了),其他参考jvm8规范 其中还有别的信息,比如在源码里没有体现的attributes名称,例如 LineNumberTable, Code

属性表(Attributes)

属性表是存储信息量最大的一块结构, 里面包含了你的方法编译后生成的字节指令(Code), 注解信息, 异常表(Exceptions), 栈映射表(StackMapTable)等,还有很多调试信息,比如LineNumberTable就是源码行号表

没错,你们框架常用的注解信息,就是存在属性表里,上面的field_info和method_info都包含属性信息,所以你可以对field和method进行注解

3. 总结

本节主要介绍了class文件的历史,设计和结构, 这些是jvm平台无关性的基石,众所周知的java玩再深,就是玩字节码诡计,class文件就是字节码诡计的基石 在运行时,编译时,类装载时去动态加功能, 都是字节码诡计的杰作。常用的AspectJ框架就是如此。

了解class文件的结构对后续的jvm学习有很大帮助,但是不可避免的又比较枯燥,笔者推荐是自己写个解析器去按照规范解析下, 纸上得来终觉浅,learning by inventing的路子是比较好的。

最后,如果你不知道怎么解析,参考java-simple-class-reader