3.虚拟机类加载机制

Java虚拟机的类加载机制

1.Java虚拟机的特点

1.1语言无关性

Java虚拟机并不仅仅支持java语言,可以支持JRuby,JPython,Scala等语言

1.2平台无关性

Java天生就是为了摆脱操作系统的束缚而产生的,提出了一个"Write Once,Run Anywhere"的口号

总结:

  • Java系的语言通过自己的编译器把源代码编译为字节码存放在class文件中,虚拟机通过加载字节码实现虚拟机的语言无关性
  • 字节码的功能远远比java语言强大,因为java不能提供的功能,其他的Java系语言可能可以提供,而字节码必须对所有的Java系语言提供底层的技术支持
  • Java的平台无关性是有条件的,比如JDK1.6打的Jar包在JDK1.8的虚拟机上是不能执行的(向下兼容)

1.3字节码与机器码

C系语言编译之后得到本地机器码,执行与操作系统的机器指令集有关

JAVA系编译后得到机器码,存放在Class文件中,其执行与操作系统的指令集无关

2.Class文件的格式--简单说明

  • 2.1 魔数(CAFEBABY)---标示该类文件可以被JAVA虚拟机识别
  • 2.2 次版本号、主版本号--标示Class文件可以被虚拟机加载的版本(备注:向下兼容,某1.7Class文件可以在1.6上运行,但是不能在1.8上运行)
  • 2.3 常量池
      • a.常量数量
      • b.字面量---文本字符串         符号引用---类、接口、字段、方法名称
          • 备注:C++在编译时会有动态连接的过程,连接就是提供本文件引用其他文件方法变量的内存入口
          •   JAVA编译时期不会进行动态连接,在运行时才会获取变量方法的内存地址(准确的是类加载的解析阶段)
      • c.一共有14种常量结构
  • 2.4 访问标示----类接口层次的访问信息(Class or interface or @interface or enum、public or private or protected)
  • 2.5 类索引、父类索引、接口索引---指向常量池
  • 2.6 字段集合---类的变量、不包含方法的局部变量(public or private or protected、volatile(易变) or final(不变)等)+属性表
  • 2.7 方法表-----描述方法(基本信息、是否为final、返回值)+属性表+CODE
  • 2.8 属性表-----字段:类型      方法:返回值类型
  • 2.9 CODE-----方法代码

3.Java虚拟机的类加载机制

3.1概述

类加载包含如下的几个步骤:

1.加载 2.验证 3.准备 4.解析 5.初始化 6.使用 7.卸载

  • 解析不一定初始化之前,可能会在初始化之后,这是为了配合java的后期绑定(动态绑定)
  • java动态绑定—一般的函数、不包括被static和final修饰的函数
  • 1.前期绑定:编译器在程序执行之前就知道,调用哪个函数体
  • 2.后期绑定:编译器不知道参数(接口)调用的是哪个对象的函数

3.2 类的初始化时机

类的加载时机是交给虚拟机进行把握的,对于类的初始化有严格的要求的,因此我们先介绍类的初始化的时机。

如下的几种情况会进行虚拟机的类初始化

  • 使用new创建的类的对象,使用类的静态变量(不包括final,static)和调用类的静态函数
  • 利用反射机制创建对象时--Class.newInstance
  • 初始化一个类的时候,如果发现其父类还没有被初始化,应该进行父类的初始化---创建子类对象时会先调用父类的构造函数。
  • 虚拟机初始化的时候,优先初始化含有main函数的主类
  • 与java的动态语言特性相关----先不介绍

除了上边的5种情况,其他的情况都不会引起类的初始化,我们称为被动引用  

  • 1.子类调用父类的静态变量或者静态函数不会引起子类的初始化,只会引起父类的初始化
  • 2.定义了类的对象数组,但是没有进行初始化——————————————Father[ ] arrays=new Father[10];是不会调用Father类的构造函数的
  • 3.使用类中的常量,在编译期间会把类的常量加载到方法区的常量池中,所以不会触发类的初始化

3.2.1 接口的初始化

  接口初始化时发现接口的父接口没有进行初始化时是不会进行初始化的,只有到父接口被使用的时候才会进行初始化

3.3 加载-----加载是类加载的一个子过程

非数组类的加载----利用类加载器(类必须和类的加载器有一一对应的关系,可以使用用户自己定义的类加载器,也可以使用系统提供的引导加载器)

  1. 根据类名获取相对应的二进制字节码------不一定从类的Class文件中获取,加载完之后会把类进行初始化的
  2. 将二进制字节码中的类的相关静态变量提取出来,放到虚拟机的方法区的运行时数据结构中
  3. 在内存中生成一个该类的Class对象,提供类的方法去中的运行时数据结构的访问入口------比较特殊不一定在堆中进行创建,一般放在方法区中

数组类的加载

  • 数据是由虚拟机直接产生的,数据存放的类型(数据减去一个维度)是由类加载器产生的
  • 数组的类型的创建过程如下:
      • 类型是引用类型---递归调用该类型的类加载器创建数组中的元素 
      • 类型不是引用类型-使用引导加载器进行创建,eg:int[]

3.4 类的连接过程

连接过程:验证、准备、解析

3.4.1验证过程-------引用自参考文献

验证过程的目的是为了确保Class文件字节流中包含的信息符合当前虚拟机的要求并且不会危害到虚拟机自身的安全

  • 文件格式验证------词法分析
  • 验证字节流是否符合class文件格式的规范,并且能被当前虚拟机处理,如是否以魔数0xCAFEBABE开头、主次版本号是否在当前虚拟机处理范围内、常量池是否有不支持的常量类型等。只有经过格式验证的字节流,才会存储到方法区的数据结构,剩余3个验证都基于方法区的数据进行。
  • 元数据验证----数据类型的语法分析
  • 对字节码描述的数据进行语义分析,以保证符合Java语言规范,如是否继承了final修饰的类、是否实现了父类的抽象方法、是否覆盖了父类的final方法或final字段等。
  • 字节码验证----方法体的语法分析
  • 对类的方法体进行语义分析,确保在方法运行时不会有危害虚拟机的事件发生,如保证操作数栈的数据类型和指令代码序列的匹配、保证跳转指令的正确性、保证类型转换的有效性等。(但是无法保证绝对的安全)
  • 符号引用验证
  • 为了确保后续的解析动作能够正常执行,对符号引用进行验证,如通过字符串描述的全限定名是都能找到对应的类、在指定类中是否存在符合方法的字段描述符等。

符号引用:位于常量池的Constant_Class_info,Constant_Filed_info,Constant_Methodref_info中,通过其中的索引可以找到常量表中对应的类、方法、字段信息

直接引用:目标对象的指针,或者说是操作句柄,与内存的布局相关,一般一个符号引用解析出来的直接引用一般是不一样的,一个对象有直接引用说明它已经存在于内存中了

解析过程就是把符号引用转换为直接引用的过程,我觉得就是引用的方法、变量真正的内存入口

3.4.2准备过程----为static变量进行初始化为0

  • 在准备阶段,为类变量(static修饰)在方法区中分配内存并设置初始值。
  • 准备阶段完成后,var 值为0,而不是100,Reference的话会被初始化为null,在初始化阶段,才会把100赋值给val,并为引用设定
  • 但是有个特殊情况:ConstantValue(常量)属性,在准备阶段虚拟机会根据ConstantValue属性将常量赋值为100。------这个其实是常量,跟一般的静态变量是不一样的。

3.4.3解析过程

  • 虚拟机中符号引用转换为直接引用的阶段,解析时连接阶段最重要的阶段

3.5 初始化阶段

1.执行类构造器的<clinit>方法

clinit方法是编译器自动收集类中的所有类变量和静态语句块(static{}),对于静态变量来说只能使用定义在它之前的静态变量,对于定义在它后边的静态变量可以赋值但是不能使用。

static{
     i=0;//OK
     System.out.println(i);//ERROR  
}
static int i=3;

clinit函数与类的构造函数不一样(init),他不用显示的调用父类的构造器函数,虚拟机会保证在执行类的clinit函数之前,其父类的clinit函数已经被执行过了,也就是说虚拟机一开始就会执行Object类的clinit函数,一个类的clinit函数只会被执行一次。

clinit函数就是类的static语句的集合

4.类加载器

4.1类加载器的分类

  • 1.启动类加载器---程序员不能直接使用,负责加载放在<JAVAHOME>lib下的类文件
  • 2.扩展类加载器---开发者可以直接使用类加载器<JAVA_HOME>libext下的类文件
  • 3.应用程序类加载器---加载用户类路径上所指定的类库

4.2类的双亲委派模型

类加载器之间的机构如下

a.启动类加载器 <---- b.扩展类加载器 <---- c.应用程序加载器<---- d.自定义类加载器

他们之间的父子关系并不是通过继承实现的,而是使用复用关系来父类加载器的代码。

双亲委派模型工作过程:

当一个类加载器接收到类加载请求时,首先不会自己去加载这个类,而是把任务向上级,也就是委派自己的父类进行加载,一般来说所有的类加载任务都会提交到启动类加载器中,除非父类反馈这个类加载任务我完成不了(在它的搜索范围内没有找到这个类),才会尝试自己去加载。

优点

1.Java中的类随着它的类加载器一起具备了一种带有优先级的层次关系,比如:自己编写了一个Object类,使用用户类加载器进行加载,系统中就会有多个Object类,java中最基础的行为也就不能保证。

备注:JAVA中的相同的class文件就是被不同的类加载器进行加载,最终也会得到两个不相同的类。

参考文献:

http://blog.jobbole.com/104947/

原文地址:https://www.cnblogs.com/yangyunnb/p/6049256.html