java虚拟机 第七章 虚拟机类加载机制

类加载机制

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

在java中,类型的加载、链接、初始化过程是在程序运行期间完成的,这种策略虽然会令类加载时稍微增长一些性能开销,可是会为java应用程序提供高度的灵活性。


类的整个生命周期包括:加载、链接(验证、准备、解析)、初始化、使用、卸载。类的加载过程必须严格的按照这个顺序执行,而解析阶段不必定:它在某些状况下能够在初始化以后运行,这是为了支持java的运行时绑定。


何时须要开始类加载的第一个阶段,java虚拟机规范没有明确的约束。可是对于初始化阶段,虚拟机规范严格规定了5种状况必须对当即对类进行‘初始化’(加载、链接要再次以前开始)
  1. 遇到new、getstatic、putstatic、invokestatic这四条字节码指令时;
  2. 使用java.lang.reflect包的方法对类进行反射调用时;
  3. 当初始化一个类的时候,若是发现其父类还没初始化时,则须要先触发父类的初始化;
  4. 当虚拟机启动时,用户须要指定一个要执行的主类,虚拟机会先初始化这个主类;
  5. 当使用动态语言支持时,若是一个java.lang.invoke.MethodHandle实例最后的解析结果时ref_getstatic、ref_putstatic、ref_invokestatic的方法句柄,而且这个句柄对应的类没有初始化过,则须要触发其初始化。
这五种方式,称为对类的主动引用,除此以外的全部引用类的方式都不会触发初始化。
  1. 经过子类引用父类中的静态字段只会触发父类的初始化
  2. 数组的初始化不会触发类的初始化阶段,会触发一个由虚拟机自动生成类的初始化,直接继承Object,由newarray字节码指令触发,这个类封装了数组元素的访问方法
  3. 引用类中的static final字段不会触发类的初始化
  4. 接口和类的初始化有所不一样,编译器会为接口生成<clinit>类构造器,用户初始化接口中定义的成员变量。真正的区别时:当一个子接口初始化的时候不会要求初始化其父接口,只有在使用到父接口的时候才会进行初始化。

类加载第一阶段:加载

  1. 经过一个类的全限定名来获取次类的二进制字节流
  2. 将这个二进制流表明的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成这个类的java.lang.Class对象,做为方法区中这个类的各类数据访问入口。
第1条没有指明这个二进制流要从一个class文件中获取,在此基础上创建了许多举足轻重的技术:
  • 从ZIP包中读取,最终成为jar,ear,war格式的基础;
  • 从网络中获取,典型的应用就是Applet;
  • 运行时计算生成,最多的就是动态代理技术;
  • 从其余文件中获取,如jsp应用,有jsp文件生成对应的class类;
  • 从数据库中读取,相对少见
相对于类加载过程当中其余阶段,一个非数组类的加载阶段(准确的说,时加载阶段中获取二进制流的动做)是开发人员可控性最强的,由于加载阶段既可使用系统提供的引导类加载器来完成,也能够由用户自定义的类加载器去完成,开发人员能够经过定义本身的类加载器去控制字节流的拉取方式(即重写一个类加载器的loadClass方法)。


数组类不须要经过类加载器建立,由java虚拟机直接建立,可是数组的类型元素最终是要经过类加载器建立,一个数组类的建立过程要遵循如下规则:
  1. 若是数组的的组件类型是引用类型(如Integer[]),那就递归采用类的加载过程去加载这个组件类型,数组C将在加载该组件类型的类加载器的类名称空间上被标识。
  2. 若是数组的组件类型不是引用类型(如int[]),java虚拟机会把类标记为与引导类加载器关联。
  3. 数组类的可见性与它的组件类型的可见性一致,若是组件类型不是引用类型,那数组的可见性将默认为public。

对于hotSpot虚拟机而言,class对象比较特殊,虽然是对象,可是却存放在方法区中(hotSpot虚拟机的方法区其实也在堆中,称为永久代)数据库


类加载第二阶段:链接(验证、准备、解析)

加载阶段与链接阶段的部份内容(如一部分字节码文件的格式验证动做)多是交叉进行的,加载阶段未结束,链接阶段可能已经开始。
  1. 验证是链接阶段的第一步,这一阶段的目的是为了确保class文件的字节流中包含的信息符合当前虚拟机的要求,并不会危害虚拟机自身的安全。总体上看,验证阶段大体上会分红4个阶段的验证动做:文件格式验证、元数据验证、字节码验证、符号引用验证。
  2. 准备阶段是正式为类变量分配内存并设置类变量初始值的阶段, 这些变量使用的内存最终都会在方法区中分配。
  3. 解析阶段时虚拟机将常量池中的符号引用替换为直接引用的过程。


符号引用:符号引用一组符号来描述要引用的目标,符号引用能够是任何形式的字面量,只要能无歧义的定位到目标便可。
直接引用:直接引用能够是直接执行目标的指针、相对偏移量或句柄。若是有了直接引用那么内存中必定存在该对象。


虚拟机规范中并未规定解析阶段发生的具体时间,因此虚拟机实现能够根据须要来判断是在类被加载器加载时就对常量池中的符号引用进行解析,仍是等到一个符号引用将要被使用前才去解析。


除invokedynamic指令外,虚拟机实现能够对第一次解析的结果进行缓存(在运行时常量池记录直接引用,并把符号引用标识为已解析状态),若是成功一直成功,若是失败一直失败。
解析动做主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符。


类加载第三阶段:初始化

初始化:类初始化阶段是加载过程的最后一步,真正开始执行类中定义的java程序代码。 初始化阶段是执行类构造器<clinit>() 方法的过程
  1. <clinit>()是编译器自动收集类变量的赋值动做和静态代码块中语句合并产生的,收集顺序按照代码出现的顺序,在前面的静态代码块能够赋值不能访问。
  2. <clinit>()不用显示调用父类构造器,虚拟机会保证在子类构造器调用以前,调用父类构造器,所以在虚拟机中第一个被执行的是Object类构造器。
  3. 因为父类<clinit>()方法先调用,意味着父类中的静态代码块优先于子类。
  4. <clinit>()方法不是必需的。
  5. 接口能够存在赋值语句,也会生成<clinit>()方法,接口初始化时父接口中的<clinit>()方法不会被执行,接口的实现类初始化时也不会执行接口的<clinit>()方法。
  6. 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的加锁和同步,多个线程去初始化一个类,只有会一个线程去执行<clinit>()方法。
类加载阶段中的“经过一个类的全限定名来获取描述此类的二进制字节流”是在java虚拟机外部实现的,实现这个动做的代码模块称为“类加载器”。


对于任意一个类,都须要由加载它的加载器和这个类自己一同确立其在java虚拟机中的惟一性,每个类加载器都有其独立的命名空间。


双亲委派模型:从java虚拟机的角度来说,只存在两种不一样的类加载器:一种是启动类加载器,这个类加载器使用C++语言实现,时虚拟机自身的一部分;另外一种就是全部其余类的类加载器,这些加载器都由java语言实现,独立于虚拟机外部,而且全都继承自抽象类ClassLoader。从开发人员的角度可划分: 启动类加载器、扩展类加载器、应用程序类加载器、自定义类加载器
双亲委派模型要求除了顶层的启动类加载器外,其他的类都应有本身的父类加载器(非强制要求)。
工做过程:若是一个类加载器收到了类加载的请求,它首先不会本身去尝试加载这个类,而是把这个请求委派给父类加载器,最终请求传送到最顶层的启动类加载器,若是父类在它的搜索范围内没有找到所需的类,反馈本身没法加载这个类,子类才会本身尝试加载这个类。好处是java类随着它的类加载具有了一种带优先级的层次关系。
相关文章
相关标签/搜索