Java虚拟机翻译.class文件的理解?

  咱们知道计算机是由晶体管、电路板等组装而成的电子设备,而这些电子设备其实只能识别0与1的信号。html

  那么问题来了,咱们在操做系统上编写的Java代码(由字母、数字等各类符号组成),打包后部署到服务器上,是如何被计算机所识别并运行的呢?另外,操做系统有不少种,包括Windows系统,Linux系统,Mac OS系统等,而咱们一样的Java代码,却能够不作任何处理在不一样的系统上正常运行,这又是为啥呢?java

  带着这些疑问,你将会在下面的介绍中获得答案!!!数组

一、Java虚拟机的两个特性

  在此系列博客中,咱们介绍到Java虚拟机的两个特性。安全

  对于Java语言,咱们经过编辑器编写的Java代码,后缀通常是.java。经过javac编译器编译后,会变成.class结尾的字节码文件,只有编译后的.class文件,才能在Java虚拟机上运行。(解压部署在服务器上的jar包,全是编译后的class文件)ruby

  再好比对于 JRuby 语言,经过编辑器编写的代码后缀是.rb。经过jrubyc 编译器编译后,也会变成 .class 结尾的字节码文件,而后也能在Java虚拟机上运行。服务器

  在好比已正式成为Android官方支持开发语言的Kotlin。也能够编译成.class字节码文件,而后在虚拟机上运行。并发

  咱们能够用下面这幅图来表示:oracle

   也就是说,无论你是什么语言,只要能经过某种手段生成合乎规范的.class字节码文件,其实就能够在Java虚拟机上运行,这就是语言无关性。jvm

  Write once, run everywhere(一次编写,处处运行)这是Java语言诞生之处就宣传的一个口号。Java语言之因此可以跨平台运行,其实就是由于Java虚拟机对各个平台的适配,在不一样的系统下安装不一样的Java虚拟机,咱们程序固然可以在不一样的系统上运行。编辑器

   对于文章开头提出的问题,一样的程序可以在不一样的系统上正常运行的缘由,就是由于咱们在不一样的系统上安装了不一样的Java虚拟机。

二、class 字节码文件介绍

  搞清楚了Java代码的跨平台原理,咱们接着来介绍为何编写的Java代码可以被计算机所识别。

  这实际上是上面所说的语言无关性这个特性重要文件——class字节码文件的功劳。

  Java全部的指令大概有 200 个左右,一个字节(8位)能够存储 256 种不一样的信息,咱们将一个这样的字节称为字节码(ByteCode)。

  而 class 文件即是一组以 8 位字节为基础单位流的二进制流,各个数据项目严格按照顺序紧凑地排列在 class 文件之中,中间没有添加任何分隔符,因此整个class 文件中存储的内容几乎都是程序运行的必要数据,没有任何冗余。当遇到须要占用 8 位字节以上空间的数据项时,则会按照高位在前的方式分割成若干个 8 位字节进行存储。

  好比,对于以下这段代码:

  咱们将生成的class 文件,经过十六进制编辑器打开(在IDEA中,能够下载HexView插件,安装完成后,选择这个class文件,右键 HexView)

   打开后的文件以下:(下面的介绍也都是以这张图为例)

   下面咱们会介绍这些十六进制分别表明什么意思。

  另外,为了更好的查看 Class 文件字节码结构,JDK 还为咱们提供了一个命令行工具 javap。使用语法以下:

  经过 javap -help 命令,能够查看相关参数做用:

   这些内容下面也会详细介绍。

  在介绍这些十六进制以前,咱们先介绍 Class 文件的数据类型。

  Class 文件采用一种相似于 C 语言结构体的伪结构来存储,这种伪结构只有两种数据类型:无符号数和表

  这是一种基本数据类型,以 u1,u2,u4,u8 来分别表明 1个字节、2个字节、4个字节、8个字节的无符号数,无符号数能够用来描述数字、索引引用、数量值或按照 UTF-8 编码构成的字符串值。

  表是由多个无符号数或其它表做为数据项所构成的复合数据类型,全部表都习惯行的以“_info”结尾。表用于描述有层次关系的复合结构数据。

  整个 Class 文件本质上就是一张表,结构以下:

   PS:须要说明的是,因为 Class 文件结构没有任何分隔符,因此不管是每一个数据项的的顺序仍是数量,都是严格限定的,哪一个字节表明什么含义,长度多少,前后顺序如何,都是不容许改变的。

  下面,咱们就来分别介绍这些数据项表明什么含义。  

  每一个 class 文件的头 4 个字节称为魔数(Magic Number),它的惟一做用是:标识该文件是一个Java类文件。若是没有识别到该标志,则说明该文件不是Java类文件或者文件已受损。

  其实不少文件存储标准中都使用魔数进行身份识别,好比图片gif或者jpeg,使用魔数而不是使用扩展名来进行识别主要是基于安全考虑,由于文件扩展名能够任意的改动。

五、Class 文件的版本号

  紧随魔数的 4 个字节存储的是 class 文件的版本号:第 5 和第 6 个字节是次版本号(Minor Version),第 7 和第 8 个字节是主版本号(Major Version)。

  Java的版本号是从 45 开始的,JDK1.1 以后的每一个 JDK 大版本发布主版本号向上加1(JDK1.0~JDK1.1使用了45.0~45.3的版本号),高版本的 JDK 能向下兼容之前版本的 Class 文件,但不能运行之后版本的 Class 文件,即便文件格式未发生变化。

  上图第五、六、七、8个字节为 00 00 00 34。其十进制值为 52,是JDK8的内部版本号。

  紧随主版本号的是常量池入口,是class文件中第一个出现的表类型数据项目,也是占用Class文件空间最大的项目之一,更是Class文件结构中与其它项目关联最多的数据类型。

  由于常量池中常量的数量是不固定的,因此在常量池的入口要放置一项 u2 类型的数据,表明常量池容量计数值(constant_pool_count)。

  PS:注意,常量池容量计数值是从 1 开始的,而不是从 0 开始。将 0 空出来,是为了知足后面某些指向常量池的索引值的数据在特定状况下须要表达“不引用任何一个常量池项目”的意思。

  Class 文件结构中,只有常量池的容量是从 1 开始的,其它的集合类型,都是从 0 开始的。

  看上图的十六进制文件,常量池容量计数值为:0x0025,即十进制 37。这就表示常量池中有 36 项常量,索引值分别为 1~36(经过上面javap命令生成字节码文件能够很明显看出来有36个)

  常量池主要存放两大类常量:

  一、字面量(Literal):字面量比较接近于 Java 语言层面的常量概念,好比 文本字符串、被声明为 final 的常量值等。

  二、符号引用(Symbolic References):符号引用属于编译原理方面的概念,包括下面三类常量:

    字段的名称和描述符(Descriptor)

    方法的名称和描述符。

  须要说明的是,Java代码在进行javac 编译的时候,并不像 C 和 C++ 那样有“链接”这一步骤,而是在虚拟机加载 Class 文件的时候进行动态链接。

  也就是说,在 Class 文件中不会保存各个方法和字段的最终内存布局信息,所以这些字段和方法的符号引用不通过转换的话是没法被虚拟机使用的。当虚拟机运行时,须要从常量池得到对应的符号引用,再在类建立时或运行时解析并翻译到具体的内存地址之中。关于类的建立和动态链接的内容,下篇博客会详细介绍。

  常量池中的每一项内容都是一个表,在JDK1.8中共有 14 种结构各不相同的表结构数据,每一个表结构第一位是一个 u1 类型的标志位(tag,取值为1 到 18,缺乏标志为 二、1三、1四、17 的数据类型)。表明当前这个常量属于哪一种常量类型。

  14 种常量类型所表明的具体含义以下:

   接着看十六进制文件,紧跟常量池数量的十六进制是0x0a,这是一个标志位,0x0a的十进制数是10,查看常量池的项目表接口,表示的类型是 CONSTANT_Methodref_info。

   也就是说,接下来的u2类型0x0006,其十进制值为6,紧跟后面的u2类型十六进制为0x0017,其十进制值为23,这都是两个索引值,分别指向第索引值为6的常量和索引值为23的常量。

  整个十六进制字节码就不一一进行推导了,下面是各个数据类型的结构:

   常量池结束后的两个字节表示访问标志(access_flags),这个标识用于识别一些类或接口层次的访问信息。

  包括:这个 Class 是类仍是接口;是否认义为 public 类型,是否认义为 abstract 类型;若是是类的话,是否被声明为 final 等。

  具体的标志位及标志含义以下:

   上表定义了 8 个标志位,可是咱们说访问标志是一个 u2 类型,一共有 32 个标志位可使用,没有定义的标志位一概为 0 。

八、类索引、父类索引和接口索引集合

  类索引、父类索引和接口索引按顺序排列在访问标志以后。

  类索引:用于肯定这个类的全限类名 ,是一个 u2 类型的数据。

  父类索引:用于肯定这个类的父类全限类名,也是一个 u2 类型的数据。由于Java是单继承的,除了 java.lang.Object 类之外,全部的类都有父类。因此,除了Object 类之外,全部Java类的父类索引都不为0.

  接口索引:用于描述这个类实现了哪些接口,是一组 u2 类型的数据集合,第一项为 u2 类型的接口计数器,表示实现接口的个数。若是没有实现任何接口,则为0。

   字段表(field_info):描述接口或类中声明的变量。(不包括方法内部声明的变量)

  ②、是类级变量仍是实例级变量(static修饰)

  ③、是否可变(final修饰)

  ④、并发可见性(volatile修饰,是否强制从主从读写)

  ⑤、是否可序列化(transient修饰)

  ⑥、字段数据类型(8种基本数据类型,对象,数组等引用类型)

  前面5个修饰符,都是布尔值,用标志位来表示;后面两个字段名称和类型,是没法固定的,只能引用常量池中的常量来表示。

  Class 文件存储格式中对方法的描述和字段的描述基本上是一致的。也是依次包括:

  方法访问标志以下(access_flags):

  在前面介绍的字段表集合、方法表集合中都包括了属性表集合(attributes),其实就是引用的这里。

  根据《Java虚拟机规范第二版》中,预约义了 9 项虚拟机实现应当可以识别的属性。

   对于每个属性,它的名称要从常量池中引用一个 CONSTANT_Utf8_info 类型的常量来表示,其属性值的结构则是彻底自定义的,只须要说明属性值所占用的位数长度便可。

Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最 终形成可以被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载机制。与那些在编译时需 要进行连接的语言不同,在Java语言里面,类型的加载、连接和初始化过程都是在程序运行期间完成 的,这种策略让Java语言进行提前编译会面临额外的困难,也会让类加载时稍微增加一些性能开销, 但是却为Java应用提供了极高的扩展性和灵活性,Java天生可以动态扩展的语言特性就是依赖运行期动 态加载和动态连接这个特点实现的。例如,编写一个面向接口的应用程序,可以等到运行时再指定其 实际的实现类,用户可以通过Java预置的或自定义类加载器,让某个本地的应用程序在运行时从网络 或其他地方上加载一个二进制流作为其程序代码的一部分。这种动态组装应用的方式目前已广泛应用 于Java程序之中,从最基础的Applet、JSP到相对复杂的OSGi技术,都依赖着Java语言运行期类加载才得以诞生。

一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载 (Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化 (Initialization)、使用(Using)和卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称 为连接(Linking)。


“加载”(Loading)阶段是整个“类加载”(Class Loading)过程中的一个阶段,希望读者没有混淆 这两个看起来很相似的名词。在加载阶段,Java虚拟机需要完成以下三件事情:
1)通过一个类的全限定名来获取定义此类的二进制字节流。
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3)在内存中生成一个代表这个类的.URLClassLoader,如果有程序直接 依赖了这种继承关系,或者依赖了URLClassLoader类的特定方法,那代码很可能会在JDK 9及更高版 本的JDK中崩溃。现在启动类加载器、平台类加载器、应用程序类加载器全都继承于

  • 最后,JDK 9中虽然仍然维持着三层类加载器和双亲委派的架构,但类加载的委派关系也发生了 变动。当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能 够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器 完成加载,也许这可以算是对双亲委派的第四次破坏。

JDK 9后的类加载器委派关系

我要回帖

更多关于 怎么向虚拟机里面导入文件 的文章

 

随机推荐