Write Once , Run AnyWhere
java如何实现跨平台?
为啥C程序不能跨平台?
hello.cpp
#include<stdio.h>
int main()
{
printf("Hello Worldn");
return 0;
}
下面是这个c程序的执行过程
- 预处理会将hello.cpp文件中通过include引入的头文件插入到程序文本中,得到hello.i文件
- 编译器将hello.i中的C代码翻译成汇编指令,得到文件hello.s
- 汇编器将hello.s文件中的汇编指令翻译成机器指令,并且将其成一种可重定位目标程序的格式,然后将结果保存在目标文件中hello.o
- 在程序中我们使用printf函数,而这个函数存在于名字为printf.o的单独的预编译好的目标文件中,链接器负责将这个目标文件中机器指令与hello.o文件中的机器指令合并。结果得到一个可执行的目标文件,它被加载入内存后,就可以被执行了!
看到这里有没有发现,我们编写的C程序最终是直接被转换成机器码保存在文件中,然后被加载到内存中CPU取指执行。这就意味如果我将在intel架构下编译的C执行文件拿到AMD架构的计算机上面执行,就会出错;因为机器指令是CPU直接识别的指令,而按照intel指令集生成的机器指令你让AMD处理器去取指执行它,肯定会出错呀!
跨平台的java
从上面C程序的执行过程我们也看出,C程序之所以不跨平台是它可执行文件中的机器指令按照当前系统上CPU的指令集生成的。那么要实现跨平台,我们是不是可以设立一个规范,程序不直接编译为机器指令,而是编译成这个规范规定的形式保存在本地,然后在程序执行时由一个媒介根据不同的CPU指令集将这个规范转换为机器指令运行。
这里我所说的规范就是class文件格式,而媒介就是JVM或者也可以说是字节码指令引擎。下图是java源程序编译执行的大概过程,hello.java最终编译成hello.class文件保存在本地,然后被JVM加载入内存翻译或者是编译执行。
可以看出classs文件就是java实现平台无关性的关键,下面让我们一起揭开class文件的神秘面纱吧!!!
class文件
一图胜过千言
class文件的数据类型
根据《java虚拟机规范》的规定,Class文件格式采用一种类似于C语言的伪结构来存储数据,这种伪结构中只有俩种数据类型 : 无符号数和表
- 无符号数属于基本的数据类型,以u1,u2,u4,u8来分别代表1个字节,2个字节,4个字节,8个字节的无符号数,无符号数可以用来表示数字,UTF8编码的字符串,引用等。
- 表是由多个无符号数或者是其他表作为数据项组成的符合数据类型,比如常量池表中的表项就是各种表,所有表的命名以_info结尾
class文件结构
在cmd中输入javap -verbose class文件路径查看class文件内容(非二进制)。
使用sublime打开class文件可以查看二进制内容。
魔数(magic)
class文件中的头四个字节被称为魔数,它的作用就是标识这个文件是一个可以被JVM接受的文件。Class文件的魔数是 “OXCAFEBABE”,一个具有浪漫气息的值。
用sublime打开一个class文件看一下
cafe babe 0000 0034 0118 0a00 3c00 890a
看一下反编译结果
minor version: 0
major version: 52
class文件的版本号
紧接着魔数的4个字节存储的就是class文件的版本号:其中第5,6个字节标识次版本号,第7,8个字节标识主版本号。java实现良好的向后兼容性也依赖于class中的版本号,《java虚拟机规范》规定即使class文件的格式没有发生变化,JVM也必须拒绝执行超过其版本号的Class文件。
Java的主版本号从45开始,从上面我贴出的部分class文件内容可以看出我本机是JDK8即主版本号是52
Java次版本号在Java2前被使用过,在java2到java12中均未被使用,全部固定是零。直到JDK12中重新使用来公测一些新的特性。
不知道大家在初学java的时候,有没有经历辛辛苦苦在网上下载了jar包导入后console却疯狂输出啥 unsupport version 52.0之类的堆栈异常,还记得我当时疯狂问度娘,下载了一大堆jar包一个一个的试,解决后也是迷迷糊糊的。后来学习JVM相关内容的时候才恍然大悟(笨的离谱)
常量池
紧接着主次版本号后面就是常量池入口,常量池可以认为是class文件中的资源仓库,因为class文件中的其他项目几乎都和它有关联。在常量池前面有一个计数器,(constant_pool_count)来表示常量池中有多少个数据项。
常量池中主要存放俩大常量:字面量和符号引用
字面量:字面量就相当于是常量,例如String s="sheledon"中的"sheledon"就是一个字符串常量,还有被关键字final修饰的常量值等
符号引用:符号引用是编译原理方面的概念,它主要的意义就是通过符号无歧义的定位一个目标。在常量池表中主要包括以下内容
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
- 被模块导出或者开放的包
- 方法句柄和方法类型
下面看一下常量池的内容
public class Test extends Random implements Serializable {
private int a;
private static int b;
public static void main(String[] args) {
show();
}
public static void show(){
System.out.println("欢迎来到小白的知识空间");
}
}
tip:源文件中的类继承关系纯粹就是为了演示常量池中的内容
常量池
Constant pool:
#1 = Methodref #7.#26 // java/util/Random."<init>":()V
#2 = Methodref #6.#27 // cn/sheledon/test/Test.show:()V
#3 = Fieldref #28.#29 // java/lang/System.out:Ljava/io/PrintStream;
#4 = String #30 // 欢迎来到小白的知识空间
#5 = Methodref #31.#32 // java/io/PrintStream.println:(Ljava/lang/String;)V
#6 = Class #33 // cn/sheledon/test/Test
#7 = Class #34 // java/util/Random
#8 = Class #35 // java/io/Serializable
#9 = Utf8 a
#10 = Utf8 I
#11 = Utf8 b
#12 = Utf8 <init>
#13 = Utf8 ()V
#14 = Utf8 Code
#15 = Utf8 LineNumberTable
#16 = Utf8 LocalVariableTable
#17 = Utf8 this
#18 = Utf8 Lcn/sheledon/test/Test;
#19 = Utf8 main
#20 = Utf8 ([Ljava/lang/String;)V
#21 = Utf8 args
#22 = Utf8 [Ljava/lang/String;
#23 = Utf8 show
#24 = Utf8 SourceFile
#25 = Utf8 Test.java
#26 = NameAndType #12:#13 // "<init>":()V
#27 = NameAndType #23:#13 // show:()V
#28 = Class #36 // java/lang/System
#29 = NameAndType #37:#38 // out:Ljava/io/PrintStream;
#30 = Utf8 欢迎来到小白的知识空间
#31 = Class #39 // java/io/PrintStream
#32 = NameAndType #40:#41 // println:(Ljava/lang/String;)V
#33 = Utf8 cn/sheledon/test/Test
#34 = Utf8 java/util/Random
#35 = Utf8 java/io/Serializable
#36 = Utf8 java/lang/System
#37 = Utf8 out
#38 = Utf8 Ljava/io/PrintStream;
#39 = Utf8 java/io/PrintStream
#40 = Utf8 println
#41 = Utf8 (Ljava/lang/String;)V
下面带大家分析一下上面的常量池表,我们不用二进制来分析,用反编译更加的直观。
#6 = Class #33 // cn/sheledon/test/Test #7 = Class #34 // java/util/Random #8 = Class #35 // java/io/Serializable
Class是CONSTANT_Class_info型常量,它表示的是类或者是接口的符号引用,它的结构如下
类型 名称 | 数量 | 描述 | |
---|---|---|---|
u1 tag | 1 | 标志位区分常量类型值为7 | |
u2 name_index | 1 | 常量池索引值 |
tag是一个标志位,用于区分常量类型;name_index则是一个常量池的索引值,指向常量池中的一个CONSTANT_Utf8_info类型的常量。上面索引为6的class中的name_index是33。
#33 = Utf8 cn/sheledon/test/Test
Utf8表示这是一个CONSTANT_Utf8_info型的常量,结构如下
类型 | 名称 | 数量 | 描述 |
---|---|---|---|
u1 | tag | 1 | 标志位区分常量类型值为8 |
u2 | length | 1 | UTF8编码字符串占用字节数 |
u1 | bytes | length | 字符串的字节 |
由于java源码中方法和字段的名称在编译后都是由CONSTANT_Utf8_info来表示的,所以utf8_info常量的最大长度也就是方法和字段名称的最大长度。
#1 = Methodref #7.#26 // java/util/Random."<init>":()V
Methodref表示这是一个CONSTANT_Methodref_info型的常量,结构如下
类型 | 名称 | 描述 |
---|---|---|
u1 | tag | 标志位区分常量类型值为10 |
u2 | index | 指向声明方法的类描述符CONSTANT_Class_info的索引 |
u2 | index | 指向简单名称以及类型描述符CONSTANT_NameAndType_info的索引 |
#3 = Fieldref #28.#29 //java/lang/System.out:Ljava/io/PrintStream;
Fieldref表示这是一个CONSTANT_Fieldref_info型的常量,结构如下
类型 | 名称 | 描述 |
---|---|---|
u1 | tag | 标志位区分常量类型值为9 |
u2 | index | 指向声明字段的类或者是接口的描述符CONSTANT_Class_info的索引 |
u2 | index | 指向字段描述符CONSTANT_NameAndType_info的索引 |
#26 = NameAndType #12:#13 // "<init>":()V #27 = NameAndType #23:#13 // show:()V #32 = NameAndType #40:#41 // println:(Ljava/lang/String;)V
NameAndType表示这是一个CONSTANT_NameAndType_info型的常量,结构如下
类型 | 名称 | 描述 |
---|---|---|
tag | tag | 标志位区分常量类型值为12 |
index | index | 指向一个字符串常量的索引,表示字段或方法简单名称 |
index | index | 指向一个字符串常量的索引,表示字段或方法描述符 |
介绍一下方法和字段的简单名称和描述符的概念
简单名称:方法和字段的简单名称就是名称(呃呃呃),例如这个类中的show()方法和静态类变量b的简单名称就是"show"和"b"
描述符;方法和字段的描述符是用来描述字段的数据类型,方法的参数列表和放回值的,根据描述符规则,基本数据类型以及void用一个大写字符代表,例如B代表byte,C代表char,V代表void;而引用类型则用大写字母L+类的全限定名; 对于数组来,每一个维度前面要加'['描述
字段举例:
int age; //简单名称:age , 描述符 :I
String gender; //简单名称:gengder, 描述符: Ljava/lang/String
int [] times; //简单名称:times , 描述符: [I
String[][] colors; //描述符 : [[Ljava/lang/String
方法举例
描述符先参数列表后返回值
void show() {...} //简单名称: show , 描述符 : V
public static String valueOf(char data[], int offset, int count){...} //描述符: ([CII)Ljava/lang/String
访问标志
在常量池结束后,紧接着就是2个字节表示访问标志(access_flags),这个标志用于识别一些类或者是接口的信息,例如这个Class是接口还是类,是否是public,是否定义为abstract等.
class文件内容:
flags: ACC_PUBLIC, ACC_SUPER
标志名称标志值含义ACC_PUBLIC0x0001是否为public类型ACC_FINAL0x0010是否被声明finalACC_INTERFACE0x0200标识接口ACC_ABSTRACT0x0400标识为abstract
类索引,父类索引,接口索引集合
class文件根据这三项数据来确定该类的继承关系. 类索引用于确定这个类的全限定名;父类索引用于确定这个类的父类的全限定名,除了java.lang.Object外所有的类都有父类;接口索引集合用于确定这个class实现的接口,前面会有一个计数器来指明集合的大小.
字段表集合
字段表集合用户描述接口或者是类中声明的变量,这里的变量指的是类变量和实例变量.集合中的每一个表描述了一个变量所包含的信息,例如它的作用域,是否可序列化,可变性,常量值等
结构如下
类型 | 名称 | 描述 |
---|---|---|
u2 | access_flags | 访问标志 |
u2 | name_index | 指向常量池的索引,字段的简单名称 |
u2 | descriptor_index | 指向常量池的索引,字段的描述符 |
u2 | attribute_count | 属性表集合计数器 |
attribute_info | attributes | 属性表集合 |
access_flags取值如下
标志名称 | 含义 | |
---|---|---|
ACC_PUBLIC | public字段 | |
ACC_PRIVATE | private字段 | |
ACC_PROTECTED | protected字段 | |
ACC_STATIC | static字段 | |
ACC_FINAL | final常量 | |
ACC_VOLATILE | volatile修饰保障可见性和有序性 | |
ACC_TRANSIENT | transient修饰,不被序列化 | |
ACC_SYNTHETIC | 标识该字段是否由编译器产生 | |
ACC_ENUM | 是否是枚举类型 |
属性表相关内容见下文
需要注意的是字段表集合中并不包含从父类或者是父接口中继承的字段,这些字段在创建对象的时候会被分配空间存放在对象的实例数据中
方法表集合
方法表和字段表结构相同,只不过是在表中的一些表项的取值有所差异,比如访问标志中增加了 ACC_SYNCHRONIZED,ACC_NATIVE等.方法的定义通过access_flags,name_index, descriptor_index描述.而方法的方法体在由javac编译成字节码指令后添加到了方法属性表中的Code属性中.
如果子类没有重写父类的方法,那么父类中的方法就不会出现在子类的字段表集合中,但是可能出现由编译器自动添加的方法,例如类构造方法和实例构造方法.在初学java的时候,老师都会和我们说,如果不显示的在类中编写类的构造方法,那么在创建对象的时候就会调用默认构造方法,这里的默认构造方法就是javac编译器在编译的时候自动为我们添加的.
属性表相关内容见下文
属性表集合
属性表相当于是class文件中的附加信息,方法表和字段表都可以通过携带自己的属性表来描述某些场景的专有信息
下面对属性表中的部分属性做一下简单的介绍
属性名称 | 使用位置 | 含义 |
---|---|---|
Code | 方法表 | 方法体中代码编译后的字节码指令 |
ConstantValue | 字段表 | final常量的值 |
Deprecated | 类,方法表,字段表 | 被废弃的类,方法,字段 |
Exceptions | 方法表 | 方法抛出的异常表 |
InnerClasss | 类文件 | 内部类列表 |
LineNumberTable | Code属性 | java源码行号和字节码指令的对应关系 |
LocalVariabeTbale | Code属性 | 方法的局部变量描述 |
SourceFile | 类文件 | class源文件的记录 |
属性表的结构
类型 | 名称 | 数量 | 描述 |
---|---|---|---|
u2 | attribute_name_index | 1 | 指向常量池索字符串索引,标识属性名称 |
u4 | attribute_length | 1 | 标识属性值所占的长度 |
info | attribute_length | 属性值,不同属性自定义 |
Code属性
java方法的方法体中的代码在经javac编译为字节码后保存在code属性中,而抽象方法,接口中的非default和非static方法没有code属性
结构如下
类型 | 名称 | 数量 | 描述 |
---|---|---|---|
u2 | attribute_name_index | 1 | 常量池中属性名称索引 |
u4 | attribute_length | 1 | 属性值长度 |
u2 | max_stack | 1 | 栈帧中的操作栈的最大深度 |
u2 | max_locals | 1 | 栈帧中局部变量表的最大大小 |
u4 | code_length | 1 | 字节码的长度 |
u1 | code | code_length | 字节码 |
u2 | exception_table_length | 1 | 异常表的长度 |
exception_info | exception_table | exception_table_length | 异常表 |
u2 | attributes_count | 1 | 属性表集合计数器 |
attribute_info | attributes | attributes_count |
看一下下面这个main方法的class文件内容,源码中添加行号来说明LineNumberTable属性
7:public static void main(String[] args) {
8: try {
9: Thread.sleep(1000);
10: } catch (InterruptedException e) {
11: e.printStackTrace();
12: }
13:}
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V //方法描述符
flags: ACC_PUBLIC, ACC_STATIC //方法访问标志
Code: //字节码
stack=2, locals=2, args_size=1 //操作数栈最大深度 为2,局部变量大小为2
0: ldc2_w #2 // long 1000l
3: invokestatic #4 // Method java/lang/Thread.sleep:(J)V
6: goto 14
9: astore_1
10: aload_1
11: invokevirtual #6 // Method java/lang/InterruptedException.printStackTrace:()V
14: return
Exception table: //异常表
from to target type
0 6 9 Class java/lang/InterruptedException
LineNumberTable: //指示源码与字节码行号
line 9: 0 //源码第9行对应字节码0
line 12: 6 //源码第12行对应字节码6 return
line 10: 9
line 11: 10
line 13: 14
LocalVariableTable: //局部变量属性
Start Length Slot Name Signature
10 4 1 e Ljava/lang/InterruptedException;
0 15 0 args [Ljava/lang/String;
StackMapTable: number_of_entries = 2
frame_type = 73 /* same_locals_1_stack_item */
stack = [ class java/lang/InterruptedException ]
frame_type = 4 /* same */
下面我着重解释一下异常表
这个异常表是指方法内部try catch会出现的异常,而属性Excetions指明这个方法可能抛出的异常
Exception table: //异常表
from to target type
0 6 9 Class java/lang/InterruptedException
上面内容就是在执行索引从0到6的字节码过程中出现了java/lang/InterruptedException异常,那么就跳转到索引为9的字节码处执行.
Exceptions属性
与Code属性平级,列举方法中可能爬出的异常
结构如下
类型 | 名称 | 数量 | |
---|---|---|---|
u2 | attribute_name_index | 1 | |
u4 | attribute_length | 1 | |
u2 | number_of_exceptions | 1 | |
u2 | exception_index_table | number_of_exceptions |
实例
Exceptions:
throws java.lang.InterruptedException, java.lang.ClassNotFoundException
LineNumberTable 属性
LineNumberTable用来描述java源码中行号与字节码行号(偏移量)之间的关系.它的作用主要就是在堆栈异常中显示出错的源码行号还有在调试程序的时候按照行号设置断点
实例看上面main方法中的注释
LocalVariableTable属性
该属性描述栈帧中局部变量表与源码定义变量的关系
结构如下
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | local_variable_table_length | 1 |
local_variable_info | local_variable_table | local_variable_table_length |
local_variable_info代表源码中的局部变量,结构如下
类型 | 名称 | 数量 |
---|---|---|
u2 | start_pc | 1 |
u2 | length | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | index | 1 |
前面讲java虚拟机栈栈帧中的局部变量表说过,为了节省内存空间,局部变量表中的变量槽是根据局部变量的作用域而复用的,而这里的start_pc代表局部变量生命周期开始的字节码偏移量,length代表其作用覆盖的长度.俩者结合起来就是这个局部变量在字节码之中的作用域范围.
最后
以上内容参考 《深入理解java虚拟机》,《深入理解计算机系统》,站在巨人的肩膀上面我们才可以看的更高更远.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。