Java类加载过程

概述

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

java是一门静态语言,在Java中,类型的加载、连接和初始化过程都是在程序运行期间完成的,这种策略虽然会令类加载时稍微增加一些性能开销,但是能为Java程序提供高度的灵活性,Java天生可以动态拓展的语言特性就是依赖运行期动态加载动态链接这个特点实现的

 

类加载的时机

类被加载到虚拟机内存的生命生命周期:

                                      缺失

 

 

其中验证、准备、解析三个阶段统称为连接,加载、验证、准备、初始化、卸载5个阶段的顺序时确定的,类的加载过程必须按照这种顺序按部就班的开始,而解析阶段则不一定,他在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(动态绑定或晚期绑定)。这里要注意开始和进行或完成的区别,因为这些阶段通常都是互相交叉进行的,通常会在一个阶段执行的过程中调用、激活另一个阶段。

其实什么时候开始类加载过程的第一个阶段加载Java虚拟机规范并没有进行强制约束,但是对于初始化决断规定了五个有且仅有的条件:

  1. 使用new关键字实例化对象的时候、读取或设置一个类的静态字段的时候,以及调用另一个类的静态方法的时候
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先进行初始化
  3. 初始化一个类的时候,如果发现其父类没有进行初始化,则需要先触发父类的初始化
  4. 当虚拟机启动的时候,用户需要指定一个执行的主类(也就是包含main方法的类),虚拟机会先初始化这个主类。
  5. 使用动态语言支持时,如果java.lang.invoke.MethodHandler实例最后解析结果的方法句柄对应的类没有初始化,则需要先触发其初始化

 

这5个场景中的行为成为对一个类进行主动引用,除此之外,其余所有的引用类的方式都不会触发初始化,称为被动引用。

 

类加载的过程

Java虚拟机类加载的全过程为加载、验证、准备、解析、和初始化5个阶段

 

加载

加载是类加载过程的一个阶段,在加载阶段主要完成三个事件:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储数据转化为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的class对象,作为方法去这个类的各个数据的访问入口

这里可以看到Java虚拟机实现与具体应用的灵活度。 例如第一条“通过一个类的全限定名来获取定义此类的二进制字节流”,这里并没有指明要从哪里获取,怎样获取。这里可以在ZIP包中获取、从网络中获取、运算时计算生成(代理技术)、有其他文件生成(JSP应用)、从数据库生成

在类加载的阶段中,加载阶段是开发人员可控性最强的,因为加载阶段既可以用系统提供的引导类加载器来提供,也可以由自定义实现的加载器实现(即重写一个类加载器的loadClass方法),不过这说的是非数组类。

数组类则不同,因为数组类本身不需要类加载器去加载,它是由Java虚拟机直接创建的。但是如果数组的元素的类型是引用类型,则需要用类加载器去加载

加载阶段完成之后,虚拟机外部的的二进制流就按照虚拟机所需的格式存储在方法区之中,然后再内存中实例化一个java.lang.Class类的对象(并没有明确规定在堆中,对于HotSpot虚拟机而言,Class类对象比较特殊,他虽然是对象,但是存放在方法区之中),作为程序访问方法区中这些类型数据的接口。

 

验证

验证是连接阶段的第一步,这一阶段主要是为了确保Class文件的字节流包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全

Java本身是相对安全的语言,使用纯粹的Java代码无法做到诸如访问数组边界以外的数据、将一个对象转型为它为实现的类型、跳转到不存在的代码行之类的事,如果这样做了,编译器将拒绝编译。但是前面讲到,Java虚拟机并没有规定Class文件的来源;所以虚拟机不能对其完全信任。验证阶段包括4个阶段的检验动作:文件格式检验、元数据验证、字节码验证、符号引用验证。

 

准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中分配。 这时候进行内存分配仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时跟着对象一起分配在堆内存中。并且,这里说的设置初始值并不是赋值,而是分配内存之后的各类型的默认值。默认值可以通过类字段的字段属性表中添加ConstantValue属性来设置,编译时Javac将会为value生成ConstanValue属性,在准备阶段虚拟机就会根据ConstantValue设置的值为变量赋值。

 

解析

解析阶段就是将常量池中的符号引用替换为直接引用的过程。

符号引用:符号引用是以一组符号来描述所引用的目标,用来定位到目标

直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能直接定位到目标的句柄

Java虚拟机并没有规定解析阶段发生的具体时间,可以根据需求来判断到底是在类被类加载加载时就对常量池中的符号引用进行解析还是等到一个符号引用将要被使用前去才解析它。

对同一个符号引用进行多次解析是很正常的事情,除了invokedaynamic之外,虚拟机可以对一次解析的结果进行缓存(在运行时常量池直接记录引用,并把常量标识为已解析状态)从而避免解析动作重复进行。并且这个缓存解析会影响到后面的解析。即第一次解析失败了后面解析会一直失败,反之一样。

invokedaynamic不成立是因为invokedaynamic指令目的本来就用于动态语言支持,都说到动态了,那肯定就是动态加载,必须等到程序实际运行到这条指令时才会进行解析动作。相对的,其余的解析动作都是静态的,可以在刚刚完成加载阶段,还没有开始执行代码时就进行解析。

 

初始化

类初始化阶段时类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作都是由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码。

初始化阶段时执行类构造器<clinit>()方法的过程。

  • <clinit>()方法是由编译器自动收集类中的所有变量的赋值动作和静态语句块合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的 ,静态语句块只能访问到定义在它前面的变量,定义在静态语句之后的变量,静态语句可以赋值,但不能访问。
  • <clinit>()方法与类的构造函数不同,它不需要显示的调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行前,父类的<clinit>()方法已经执行
  • 由于父类<clinit>()方法先执行,也就意味着父类的静态语句块要优于子类的的赋值操作,所以下面代码执行结果时2而不是1
static class Parent{

public static int A = 1;

static {

A = 2;

}

}

static class Sub extends Parent{

public static itn B = A;

}

public static void main(String[] args){

System.out.println(Sub.B);

}

 

  • <clinit>()方法对于类或接口并不是必需的,如果类或接口中没有静态语句块,也没有对变量的赋值操作,那么就可以不为这个类生成<clinit>()方法
  • 接口不可以使用静态语句块,但是仍然有对变量的赋值操作,因此接口和类一样都会生成<clinit>()方法。但接口与类不同的的是,执行<clinit>()方法时不需要先执行父接口的<clinit>()方法。只有父接口定义的变量使用时父接口才会初始化。并且,接口的实现类在初始化时也不会执行父类的<clinit>()方法。
  • 虚拟机会保证一个类<clinit>()方法在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个类去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,知道活动线程执行<clinit>()方法完毕。

本篇章总结参考自《深入了解Java虚拟机》一书,写的不好欢迎指点

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值