Java虚拟机:Java二进制字节码的结构、加载

这篇文章的素材来自周志明的《深刻理解Java虚拟机》。java

做为Java开发人员,必定程度了解JVM虚拟机的的运做方式很是重要,本文就一些简单的虚拟机的相关概念和运做机制展开我本身的学习过程,是这个系列的第二篇。安全

咱们在文件里写入了java的源代码,源代码写就后存入磁盘,磁盘上的源代码通过javac命令的编译造成了二进制字节码造成了class文件,通过一番步骤后java虚拟机将这些二进制字节码按照必定的方式读入内存中的不一样区域造成了二进制字节码的活化状态,虚拟机使用字节码指定的命令执行这些指令,其间使用字节码中存储的数据,最终完成了任务。这个过程就是java虚拟机执行java二进制字节码的过程的简单归纳。能够以下图所示:数据结构

clipboard.png

这只是对这个过程的简单介绍,实际上其中的每一步都相当重要并且复杂,正是这些过程最终使得咱们编写的java源代码可以运行在虚拟机搭建的环境中。编辑器

java源代码转换的结果:编译所获得字节码的结构

java的二进制字节码是一个紧密链接的二进制数码,这个数码的结构以下,各个结构之间是无缝链接的,也所以首先于这种规则,java的二进制代码才不会产生二义性,即虚拟机在读区这些数码时能够惟一地解析出它所表达的意思。函数

clipboard.png

这个庞大的结构主要包含如下几个部分:工具

1.魔数和版本号

基本的信息用于肯定java二进制字节码的特征和加载可行特征。
魔数“CAFEBABE”用以肯定这段字节码是java字节码的开始,版本号用于肯定不一样版本的jdk编译了不一样版本的java源代码生成了不一样版本的二进制字节码,这个标记的另外的目的用于提示虚拟机高于当前版本的二进制字节码可能因为兼容性不能加载。学习

2.常量池

全部和程序相关的常量都将加入这个部分中,这个部分开头的常量数决定了常量池中常量的个数以使得虚拟机可以正确解析出哪些部分是常量池。后面的常量以表的形式呈现,“表”是字节码中一个特殊的复合型的数据结构,不一样类型的常量有不一样的标记tag以指示虚拟机以不一样的方式解析出常量的值。这样最终虚拟机将根据不一样类型的常量解析出常量池中的所有常量对应的值或索引。spa

常量分为字面量和符号引用两种,字面量即通常的基本类型的数据,好比整型、浮点型等,而符号引用则是那些须要进一步经过这个符号的值去寻找它真正引用的对象,好比CONSTANT_Fieldref_info类型的常量就是符号引用,必须经过这个字段名去寻找到它真正引用的字段。
以下是常量池中的常量类型,另外以CONSTANT_Utf8_info表为例说明了常量表中的结构:设计

clipboard.png

clipboard.png

3.访问标志

关乎类的访问权限的信息将会以位的不一样的形式展现在这里。
如下是访问标志的不一样位,若是有好几个访问标志,那么通常将它们作或运算将几个相关的位都展现出来。3d

clipboard.png

4.类索引、父类索引和接口索引

这些字节码向虚拟机提供了这个类的类名、父类的类名和接口名的索引值,这个索引值最终将能够从常量池中得到其对应的全限定名。

5.字段表集合

(成员变量的描述)这些字节码向虚拟机提供了这个类中包含的字段的个数和每一个字段的信息,每一个字段一样是用一个字段表来描述的,这个字段表里说明了这个字段的信息:字段的访问权限、名索引在常量池中找到它的名字、描述符说明了这个字段的类型,可能会附带的属性表则会进一步经过拓展的数据结构展现这个字段的其它属性,好比这个字段可能被赋的初值。
如下展现的是字段表的结构:

clipboard.png

6.方法表集合

(成员方法的描述)和字段表相似的,这些字节码向虚拟机提供了这个类中包含的方法的个数和每一个方法的信息,,每一个方法用一个方法表来描述:方法的访问权限、方法名的索引在常量池中找到这个方法的名字、描述符索引获得了这个方法的特征如返回值类型和参数,可能会附带的属性表则会进一步经过拓展的数据结构展现这个方法的其它属性,好比这个方法索引获得的Code属性存在的话那么说明这个方法的方法体是存在的,则接下去的字节码就是具体的方法体了,这个方法体由Code属性表来描述。Code属性表则是更深刻的一个数据结构了(字节码的数据结构就是以这样可拓展的方式一步步创建的,当简单的索引或字面量不足以描述的时候就会引入表,以结构化的方式来对所要描述的对象作进一步的阐释),在Code属性表里规定了“Code”常量索引以肯定这段字节码是方法体、Code属性长度、最大栈、局部变量空间、代码长、代码、异常数和异常表,还有可能带有其余可拓展的属性表。

如下是方法表的结构,针对方法表中的Code属性表能够看到它的更深一层的结构,方法表中还有其余的属性表可依据状况以供拓展,好比Exceptions属性表用以描述这个方法所要规定的可抛出异常。

clipboard.png

clipboard.png

7.类其它属性表

基于一样的拓展思想,总体结构最后也预留了一样的属性表来作拓展,包括源代码所在文件等信息均可以拓展在这个部分里。

这个部分咱们能够清楚地看出字节码设计者对于数据结构的可拓展性的追求,经过可拓展的属性表的定义,不少难以描述的结构能够更深一步的描述(这有点像是文件的结构:在一个文件难以描述的时候就用一个包含了不少文件的文件夹来共同描述),这种设计最终使java二进制字节码可以长期稳定的存在下来,由于新添加的特性只须要在特定的节点作一个拓展便可。

上面的部分除了辛苦地使用十六进制编辑器对class文件做分析以外还能够直接使用jdk提供的javap工具进行分析:javap -verbose * ,它将结构化的结果呈现出来:

jinhaoplus$ javap -verbose MyClass
Classfile /Users/jinhao/Desktop/MyClass.class
  Last modified 2015-10-11; size 288 bytes
  MD5 checksum 8235b2e50d3ca6704b44862387570773
  Compiled from "MyClass.java"
class MyClass
  minor version: 0
  major version: 52
  flags: ACC_SUPER
Constant pool:
   #1 = Methodref          #5.#17         // java/lang/Object."<init>":()V
   #2 = String             #18            // a
   #3 = Fieldref           #4.#19         // MyClass.x:Ljava/lang/String;
   #4 = Class              #20            // MyClass
   #5 = Class              #21            // java/lang/Object
   #6 = Utf8               x
   #7 = Utf8               Ljava/lang/String;
   #8 = Utf8               ConstantValue
   #9 = Utf8               y
  #10 = Utf8               C
  #11 = Utf8               <init>
  #12 = Utf8               ()V
  #13 = Utf8               Code
  #14 = Utf8               LineNumberTable
  #15 = Utf8               SourceFile
  #16 = Utf8               MyClass.java
  #17 = NameAndType        #11:#12        // "<init>":()V
  #18 = Utf8               a
  #19 = NameAndType        #6:#7          // x:Ljava/lang/String;
  #20 = Utf8               MyClass
  #21 = Utf8               java/lang/Object
{
  final java.lang.String x;
    descriptor: Ljava/lang/String;
    flags: ACC_FINAL
    ConstantValue: String a

  char y;
    descriptor: C
    flags:

  MyClass();
    descriptor: ()V
    flags:
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: ldc           #2                  // String a
         7: putfield      #3                  // Field x:Ljava/lang/String;
        10: return
      LineNumberTable:
        line 1: 0
        line 2: 4
}
SourceFile: "MyClass.java"

java字节码转换为活化的内存数据:类加载的过程

java的字节码通过了编译存在了磁盘上,那么把它从磁盘里请到内存里成为真正活化可用的内存对象是相当重要的,这个过程称之为类加载过程:Class Loading,这个加载过程结束后class文件里的二进制字节码将会成为内存不一样区域里的数据,虚拟机就能够按照原则将这些数据表明的指令执行完成任务。

clipboard.png

上图展现的就是这个过程的分步骤。下面是这几步的功能:

加载的任务:

加载就是把二进制字节码转为字节流,经过类的全限定名把对应class文件里的二进制字节码转换为虚拟机内存中方法区里规定的数据结构(也就是说最终虚拟机里的结构并非二进制字节码的那种紧密型)以完成以后的数据向内存中的分配,同时在堆内存中开辟区域以存放类的java.lang.Class对象以使得未来造成的方法区中的数据能有入口访问。

类加载器:加载的时候是根据全限定名去找到对应的二进制字节码,这个过程是由类加载器完成的,虽然是同一个二进制字节码文件,若是类加载器的选择不一样,那么出于安全考虑也不能断定加载出的类是彻底一致的,所以自定义加载器和系统的应用程序加载器对同一个类的加载结果是不同的,要断定这两个类是不同的。为了解决这个问题,类加载器被设计成了多层继承关系,从上向下分别是启动类加载器、拓展类加载器、应用程序加载器、自定义加载器,加载的时候层层向上代理给父加载器,最终将会使得启动类加载器执行最终的加载,以确保全部同名的类可以被同一个类加载器所加载。

验证的任务:

毕竟是文件里的字节码,没有办法保证字节码是不被修改的安全代码,即便保证了没被修改也不能保证代码编写者在知足编译成功以外没有犯下低级的语意错误,因此对字节码的验证工做相当重要,能够必定程度上保证字节码的安全性和正确性,虚拟机将从字节流的格式是否正确(是否知足class字节码的格式限制)、元数据语法是否正确(类的元数据信息是否符合java语言的语法要求)、字节码安全性是否保证(是否有跨越内存安全性的错误和隐患出现)、符号引用验证是否可以经过(符号引用是查看那些非类自身的其它类和这些类中的字段和方法是否真的存在,这个过程是解析时会触发的,解析的过程会去查看这些符号引用到的类的状况是否会出错)。

准备的任务:

准备是为了在方法区中为即将要分配内存的数据开始开辟在相应位置开辟内存空间,并将相应的字节流注入到这些空间中去,同时为字段赋初值。值得注意的是,除非字段带有Constant Value属性外,通常状况下赋初值的时候都会为字段赋零值。这个过程结束后方法区里就已经创建起来了类的基本数据结构,这其中包括常量池的常量。

解析的任务:

准备阶段结束后类变量就都带着初值在方法区中等待了,可是这个时候方法区中的常量池里的常量却只是一些字面量和符号引用,字面量是能够直接使用的,可是符号引用必须转换为直接引用(能够理解为这些引用真正指向的地址)才能使用,不然这些常量的字面量并不能指定活化在内存里的对象:好比常量池中有一个CONSTANT_Class_info类型的符号引用常量,这个类符号引用里存储的仅仅是类的全限定名的索引,找到全限定名以后也没有什么用处,由于没办法肯定符合这个全限定名的类在内存中加载的具体地址,所以必须将这个符号引用对应的类的直接引用(地址)找出来,也就是转换为直接引用。

因此解析的任务就是将常量池里的符号引用转换为直接引用,以使得方法区里的类、父类、接口、字段、方法可以经过自身的索引寻找常量池中的引用时直接定位到这些类、父类、接口、字段、方法的准确的内存地址。

另外须要注意的是,解析不必定非要在准备以后初始化以前进行,由于咱们能够看到这个阶段的主要任务是使用阶段才会用到的,若是程序中有动态绑定的需求时这时候是没有办法把符号引用准确转换为直接引用的。因此解析的阶段有时会在初始化以后甚至使用的过程当中才会再进行的。这样作的好处就是可以完成相对灵活的动态绑定。

初始化的任务:

初始化的过程实际上就是执行<clinit>类初始化函数的过程,这个函数执行的其实就是字段的赋值语句和静态代码块的执行,这步事后,全部的字段都将被初始化为程序中赋值语句和静态代码块要求的初值,而不是准备阶段的零值。

以上几个步骤就是类加载的全过程,在这个过程当中,class文件中的二进制字节码以二进制字节流的形式先按照方法区特定的数据结构重整并创建java.lang.Class对象于堆中,验证重整后的二进制字节流没有语法、语意和安全性的问题后虚拟机为即将加载的类在方法区中开辟内存空间,字节流注入开辟的方法区的内存空间并将各字段赋零值,常量池中的符号引用转换为有实际意义的直接引用以访问特定的地址,特定的字段被初始化为程序规定的初值,整个类成功加载到方法区中。

虚拟机规范制定了不少限制,这些限制是必须遵照的,不一样的厂商对虚拟机的规范有不一样的实现,可是面对限制都是同样的遵照。java虚拟机对于类加载的时机没有明确限制,可是对于类加载过程的初始化的时机却有明确的四个“有且仅有要求”:这些条件下必须对类进行初始化,鉴于类初始化位于类加载过程的最后,因此这个规定也能够大体理解为类加载的时机,这些时机称为“主动引用”:

  1. new新对象、读写静态字段、调用静态方法的时候必须初始化类:读写静态字段时只初始化这个静态字段所在的类,若是是父类的静态字段则只初始化父类而不初始化子类;另外若是是static final修饰的静态字段,那么在编译的时候就会将其写入常量池,这个时候即便读这个静态字段也不会加载类,由于只须要去常量池中取这个值就好。这两个策略的目的其实都是尽量地减小类加载的开销;

  2. 反射调用的时候初始化类;

  3. 初始化类的时候若是父类未初始化要初始化父类;

  4. 执行主类(执行的main函数所在类)要初始化。

相关文章
相关标签/搜索