Write Once , Run AnyWhere

java如何实现跨平台?

为啥C程序不能跨平台?

hello.cpp

#include<stdio.h>
int main()
{
 printf("Hello Worldn");
 return 0; 
} 

下面是这个c程序的执行过程

image

  1. 预处理会将hello.cpp文件中通过include引入的头文件插入到程序文本中,得到hello.i文件
  2. 编译器将hello.i中的C代码翻译成汇编指令,得到文件hello.s
  3. 汇编器将hello.s文件中的汇编指令翻译成机器指令,并且将其成一种可重定位目标程序的格式,然后将结果保存在目标文件中hello.o
  4. 在程序中我们使用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加载入内存翻译或者是编译执行。

image

可以看出classs文件就是java实现平台无关性的关键,下面让我们一起揭开class文件的神秘面纱吧!!!

class文件

一图胜过千言

image

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 tag1标志位区分常量类型值为7
u2 name_index1常量池索引值

tag是一个标志位,用于区分常量类型;name_index则是一个常量池的索引值,指向常量池中的一个CONSTANT_Utf8_info类型的常量。上面索引为6的class中的name_index是33。

#33 = Utf8 cn/sheledon/test/Test

Utf8表示这是一个CONSTANT_Utf8_info型的常量,结构如下

类型名称数量描述
u1tag1标志位区分常量类型值为8
u2length1UTF8编码字符串占用字节数
u1byteslength字符串的字节

由于java源码中方法和字段的名称在编译后都是由CONSTANT_Utf8_info来表示的,所以utf8_info常量的最大长度也就是方法和字段名称的最大长度。

#1 = Methodref #7.#26 // java/util/Random."<init>":()V

Methodref表示这是一个CONSTANT_Methodref_info型的常量,结构如下

类型名称描述
u1tag标志位区分常量类型值为10
u2index指向声明方法的类描述符CONSTANT_Class_info的索引
u2index指向简单名称以及类型描述符CONSTANT_NameAndType_info的索引
#3 = Fieldref #28.#29 //java/lang/System.out:Ljava/io/PrintStream;

Fieldref表示这是一个CONSTANT_Fieldref_info型的常量,结构如下

类型名称描述
u1tag标志位区分常量类型值为9
u2index指向声明字段的类或者是接口的描述符CONSTANT_Class_info的索引
u2index指向字段描述符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型的常量,结构如下

类型名称描述
tagtag标志位区分常量类型值为12
indexindex指向一个字符串常量的索引,表示字段或方法简单名称
indexindex指向一个字符串常量的索引,表示字段或方法描述符

介绍一下方法和字段的简单名称和描述符的概念

简单名称:方法和字段的简单名称就是名称(呃呃呃),例如这个类中的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实现的接口,前面会有一个计数器来指明集合的大小.


字段表集合

字段表集合用户描述接口或者是类中声明的变量,这里的变量指的是类变量和实例变量.集合中的每一个表描述了一个变量所包含的信息,例如它的作用域,是否可序列化,可变性,常量值等

结构如下

类型名称描述
u2access_flags访问标志
u2name_index指向常量池的索引,字段的简单名称
u2descriptor_index指向常量池的索引,字段的描述符
u2attribute_count属性表集合计数器
attribute_infoattributes属性表集合

access_flags取值如下

标志名称含义
ACC_PUBLICpublic字段
ACC_PRIVATEprivate字段
ACC_PROTECTEDprotected字段
ACC_STATICstatic字段
ACC_FINALfinal常量
ACC_VOLATILEvolatile修饰保障可见性和有序性
ACC_TRANSIENTtransient修饰,不被序列化
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类文件内部类列表
LineNumberTableCode属性java源码行号和字节码指令的对应关系
LocalVariabeTbaleCode属性方法的局部变量描述
SourceFile类文件class源文件的记录

属性表的结构

类型名称数量描述
u2attribute_name_index1指向常量池索字符串索引,标识属性名称
u4attribute_length1标识属性值所占的长度
infoattribute_length属性值,不同属性自定义
Code属性

java方法的方法体中的代码在经javac编译为字节码后保存在code属性中,而抽象方法,接口中的非default和非static方法没有code属性

结构如下

类型名称数量描述
u2attribute_name_index1常量池中属性名称索引
u4attribute_length1属性值长度
u2max_stack1栈帧中的操作栈的最大深度
u2max_locals1栈帧中局部变量表的最大大小
u4code_length1字节码的长度
u1codecode_length字节码
u2exception_table_length1异常表的长度
exception_infoexception_tableexception_table_length异常表
u2attributes_count1属性表集合计数器
attribute_infoattributesattributes_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属性平级,列举方法中可能爬出的异常

结构如下

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2number_of_exceptions1
u2exception_index_tablenumber_of_exceptions

实例

Exceptions:
 throws java.lang.InterruptedException, java.lang.ClassNotFoundException
LineNumberTable 属性

LineNumberTable用来描述java源码中行号与字节码行号(偏移量)之间的关系.它的作用主要就是在堆栈异常中显示出错的源码行号还有在调试程序的时候按照行号设置断点

实例看上面main方法中的注释

LocalVariableTable属性

该属性描述栈帧中局部变量表与源码定义变量的关系

结构如下

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2local_variable_table_length1
local_variable_infolocal_variable_tablelocal_variable_table_length

local_variable_info代表源码中的局部变量,结构如下

类型名称数量
u2start_pc1
u2length1
u2name_index1
u2descriptor_index1
u2index1

前面讲java虚拟机栈栈帧中的局部变量表说过,为了节省内存空间,局部变量表中的变量槽是根据局部变量的作用域而复用的,而这里的start_pc代表局部变量生命周期开始的字节码偏移量,length代表其作用覆盖的长度.俩者结合起来就是这个局部变量在字节码之中的作用域范围.


最后

以上内容参考 《深入理解java虚拟机》,《深入理解计算机系统》,站在巨人的肩膀上面我们才可以看的更高更远.


小白不想当码农
13 声望3 粉丝