JVM总结(3)Class文件,类加载机制、编译过程

Java编译器先把Java代码编译为存储字节码的Class文件,再通过Class文件进行类加载。

Class类文件的结构

Java编译器可以把Java代码编译为存储字节码的Class文件

Class文件格式采用一种类似C语言结构体的伪结构来存储数据。这种伪结构中只有两种数据类型:无符号数和表。整个Class文件本质上就是一张表。

无符号数:属于基本数据类型,以u1、u2、u4分别代表1个字节、2个字节、4个字节。

表:由多个无符号数或其他表作为数据项构成的复合数据类型。


2、Class类文件结构详解:

  • 魔数:每个Class文件的头4个字节称为魔数,唯一的作用是确定这个文件是否能被虚拟机接受。如GIF\JPEG等在文件头都存有魔数。
  • 版本号:紧接着魔数的4个字节是Class文件的版本号。
  • 常量池:接着版本号的是常量池入口
  • 访问标志:接着常量池的是访问标志,标志着这个Class是类还是接口、是否为public等。
  • 类索引、父类索引与接口索引集合:之后接着这三个。
  • 字段表集合:接着是字段集合,用于描述接口或者类中声明的变量。
  • 方法表集合
  • 属性表集合:

一句话解释

虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是JVM的类加载机制。

类加载时期

  • 在Java语言里,类的加载、连接和初始化过程都是在程序运行期间完成的。

  • Java语言运行期加载类的特性,为Java应用程序提供了高度的灵活性,比如一个本地程序可以在运行时从网络或其他地方加载一个二进制流作为程序代码的一部分。

类的生命周期


  • 类从被加载到虚拟机内存中开始,到卸载出内存为止,整个生命周期如上图。
  • 其中加载、验证、准备、初始化和卸载这5个阶段的顺序时确定的。
  • 解析阶段则不一定,可能在初始化之后才开始。

类与接口加载时的区别

只有一点,当一个类在初始化的时候,要求其父类全部都已经初始化过了,但一个接口在初始化的时候,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化。

3、类加载的详细过程

加载
  • 通过一个类的全限定名来获取定义此类的二进制字节流,JVM把这个阶段的动作放在了虚拟机外部的“类加载器”中实现。未指明从哪里获取,因此有各种花样,比如从JAR包、WAR包,或者网络,或者运行时计算生成等等。
  • 将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构。
  • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。即对象类型数据(非对象实例数据)存在方法区。
验证

  • 验证的目的是确保Class文件的字节流中包含的信息不会危害虚拟机自身的安全,直接决定了Java虚拟机是否能承受恶意代码的攻击。
  • 验证阶段分为4个:文件格式验证,元数据验证,字节码验证,符合引用验证
准备

准备阶段为类变量在方法区中分配内存并设置类变量的零值

  • 这里只包含类变量(即被static修饰的变量),而不是实例变量
  • 实例变量会在对象实例化时随着对象一起分配在Java堆中
  • 比如 public static int value =123;在准备阶段过后value=0,只有在初始化阶段之后,value才等于123.
解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程
  • 符号引用也是描述所引用目标的,但引用的目标并不一定以及加载在内存中
  • 直接引用是指针、句柄这种,直接引用的目标必定以及在内存中存在。
初始化

初始化时类加载的最后一步,根据程序员的主观去初始化类变量和其他资源

4、类加载器ClassLoader

  • 虚拟机把类加载阶段中的通过一个类的全限定名来获取定义此类的二进制字节流这个动作放在了虚拟机外部的“类加载器”中实现。
  • 对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性
  • 比较两个类是否“相等”,只有在这个两个类是由同一个类加载器加载的前提下才有意义。

Java类的加载过程

Java类的加载过程应用了双亲委派模型


双亲委派模型

绝大部分Java程序都会用到以下3种系统提供的类加载器

  • 启动类加载器
  • 扩展类加载器
  • 应用程序类加载器

工作流程

某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

好处

Java类随着它的类加载器一起具备了一种带有优先级的层次关系。

为什么需要双亲委派模型:

  • 例如类java.lang.Object,它存在在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的Bootstrap ClassLoader进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,那系统中将会出现多个不同的Object类,程序将混乱。
  • 如果不采用双亲委派模型,那么由各个类加载器自己去加载的话,那么系统中会存在多种不同的Object类。

Java代码的编译过程

代码编译是由Javac编译器来完成,流程如上图所示。

Javac的任务就是将Java源代码编译成Java字节码,也就是JVM能够识别的二进制代码,从表面看是将.java文件转化为.class文件。而实际上是将Java源代码转化成一连串二进制数字,这些二进制数字是有格式的,只有JVM能够真确的识别他们到底代表什么意思。

具体流程:

  • 词法分析:读取源代码,一个字节一个字节的读进来,找出这些词法中我们定义的语言关键词如:if、else、while等,识别哪些if是合法的哪些是不合法的。这个步骤就是词法分析过程。
  • 语法分析:就是对词法分析中得到的token流进行语法分析,这一步就是检查这些关键词组合在一起是不是符合Java语言规范。如if的后面是不是紧跟着一个布尔型判断表达式。
  • 语义分析:语法分析完成之后也就不存在语法问题了,语义分析的主要工作就是把一些难懂的,复杂的语法转化成更简单的语法。比如将foreach转化为for循环。
  • 字节码生成:将会根据经过注释的抽象语法树生成字节码,也就是将一个数据结构转化为另外一个数据结构,结果就是生成符合java虚拟机规范的字节码。