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规范给出的结构如下:
// 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_version
和major_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
- 原文作者:Chris Wang
- 原文链接:https://www.sound2gd.wang/post/jvm%E6%8B%BE%E9%81%972-%E5%B9%B3%E5%8F%B0%E6%97%A0%E5%85%B3%E6%80%A7%E5%9F%BA%E7%9F%B3%E4%B9%8Bclass%E8%A7%A3%E6%9E%90/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议. 进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。