JVM拾遗(2): 平台无关性基石之Class解析

博客备案了一个月,中途也有各种工作的事情,现在继续更新。
前面从比较远的角度介绍了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规范给出的结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 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文件的这个结构里。

举个栗子:

1
2
3
4
5
6
7
public class HelloWorld implements java.io.Serializable{
private String str = "Hello";

@Deprecated
public void testMethod() {
}
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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