Java基础4:类加载过程

来源:互联网 发布:京东杜爽离职知乎 编辑:程序博客网 时间:2024/06/05 09:02

前言:我们在开发中,经常可以遇见java.lang.ClassNotFoundExcetpion这个异常,对于这个异常,它实质涉及到了java技术体系中的类加载。Java的类加载机制是技术体系中比较核心的部分,虽然它和我们直接打交道不多,但是对其背后的机理有一定理解有助于我们排查程序中出现的类加载失败等bug。


一、类加载过程定义

Java类从加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。它们开始的顺序如下图所示:


其中类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持 Java 语言的运行时绑定。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。


二、类加载过程详介

Java类加载过程即类装载器把一个类装入Java虚拟机中,总体来说包含以下过程:

   加载 -> 链接(验证+准备+解析)->初始化(使用前的准备)

1、加载

⑴通过一个类的全限定名来获取定义此类的二进制字节流。

⑵将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

⑶在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。

注意:第一步获取二进制字节流有很多形式,因为它并没有限定二进制流从哪里来,那么我们可以用系统的类加载器,也可以用自己的方式写加载器来控制字节流的获取:

①从class文件来->一般的文件加载

②从zip包中来->加载jar中的类

③从网络中来->Applet

获取二进制流获取完成后会按照jvm所需的方式保存在方法区中,同时会在Java堆中实例化一个java.lang.Class对象与方法区中的数据关联起来。

2、链接:验证+准备+解析

⑴验证:检查加载类的正确性

⑵准备:为静态变量分配内存地址,并将其初始化为默认值

⑶解析:将符号引用转为直接引用

第一步:验证
验证又可以细分为几个步骤: 文件格式验证->元数据验证->字节码验证->符号引用验证

文件格式验证:验证字节流是否符合Class文件格式的规范并 验证其版本是否能被当前的jvm版本所处理。ok没问题后,字节流就可以进入内存的方法区进行保存了。后面的3个校验都是在方法区进行的。

元数据验证:对字节码描述的信息进行语义化分析,保证其描述的内容符合java语言的语法规范。

字节码检验:最复杂,对方法体的内容进行检验,保证其在运行时不会作出什么出格的事来。

符号引用验证:来验证一些引用的真实性与可行性,比如代码里面引了其他类,这里就要去检测一下那些来究竟是否存在;或者说代码中访问了其他类的一些属性,这里就对那些属性的可以访问行进行了检验。(这一步将为后面的解析工作打下基础)

验证的目的:确保class文件的字节流信息符合jvm的规范。假如jvm不对这些数据进行校验的话,可能一些有害的字节流会让jvm完全崩溃。

验证阶段很重要,但也不是必要的,假如说一些代码被反复使用并验证过可靠性了,实施阶段就可以尝试用-Xverify:none参数来关闭大部分的类验证措施,以简短类加载时间。

第二步:准备

这阶段会为类变量(静态变量)分配内存并设置初始默认值,这些内存在方法区中进行分配。注意这一步只会给那些静态变量设置一个初始的值,而那些实例变量是在实例化对象时进行分配的。
例如:

   public static int value=123; //此时value的值为0,不是123。

   private int i = 123; //此时,i还未进行初始化,因为这句代码还不能执行。

第三步:解析
虚拟机将常量池中的符号引用替换为直接引用的过程。

符号引用就是class文件中:CONSTANT_Class_info、CONSTANT_Field_info、CONSTANT_Method_info等类型的常量


符号引用和直接引用的概念:

①符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。

②直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。

3、初始化:激活类的静态变量和静态代码块,初始化Java代码

初始化阶段是类加载的最后一个阶段,前面的几个类加载阶段中,除了在加载阶段可以自定义类加载器以外,其它操作都由JVM主导。到了初始化阶段,才开始真正执行类中定义的Java程序代码。

要对类进行初始化 ,代码上可以理解为:为要初始化的类中的所有静态成员都赋予初始值、对类中所有静态块都执行一次,并且是按代码编写顺序执行。
如下代码:输出的是1。如果①和②顺序调换,则输出的是123。

public class Main { public static void main(String[] args){ System.out.println(Super.i); }}class Super{ //① static{ i = 123; } //② protected static int i = 1;}

(1)类什么时候才被初始化 

  1)创建类的实例,也就是new一个对象 
  2)访问某个类或接口的静态变量,或者对该静态变量赋值 
  3)调用类的静态方法 
  4)反射(Class.forName(“com.lyj.load”)) 
  5)初始化一个类的子类(会首先初始化子类的父类) 
  6)JVM启动时标明的启动类,即文件名和类名相同的那个类 

(2)类的初始化顺序 

1)如果这个类还没有被加载和链接,那先进行加载和链接 
2)假如这个类存在直接父类,并且这个类还没有被初始化(注意:在一个类加载器中,类只能初始化一次),那就初始化直接的父类(不适用于接口) 
3)假如类中存在初始化语句(如static变量和static块),那就依次执行这些初始化语句。 
4)总的来说,初始化顺序依次是:(静态变量、静态初始化块)–>(变量、初始化块)–> 构造器;如果有父类,则顺序是:父类static方法 –> 子类static方法 –> 父类构造方法- -> 子类构造方法 


三、类加载器

1、ClassLoader通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器。等到程序运行时,JVM先初始化,在JVM初始化的过程中,JVM生成几个ClassLoader,JVM调用指定的ClassLoader去加载.class文件等各类路径、文件的类。

程序运行时类的加载实际过程:

①JDK执行指令去寻找jre目录,寻找jvm.dll,并初始化JVM;产生一个Bootstrap Loader(启动类加载器);②Bootstrap Loader自动加载Extended Loader(标准扩展类加载器),并将其父Loader设为Bootstrap Loader。③Bootstrap Loader自动加载AppClass Loader(系统类加载器),并将其父Loader设为Extended Loader。

④最后由AppClass Loader加载Java类。

2、JVM的类加载是通过ClassLoader及其子类来完成的,类的层次关系和加载顺序可以由下图来描述:
3、加载器介绍 
1)BootstrapClassLoader(启动类加载器):负责加载java核心类库,无法被java程序直接引用  
2)ExtensionClassLoader(标准扩展类加载器):负责加载 Java 平台的扩展功能库  
3)AppClassLoader(系统类加载器):
根据Java 应用的类路径(classpath)来加载指定的jar包和Java 类。一般来说,Java 应用的类都是由它来完成加载的。
4)CustomClassLoader(自定义加载器)通过继承java.lang.ClassLoader类的方式实现 
  属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现。

4、类加载器的顺序 

1)加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,保证此类只所有ClassLoader加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。 
2)在加载类时,每个类加载器会将加载任务上交给其父,如果其父找不到,再由自己去加载。 
3)Bootstrap Loader(启动类加载器)是最顶级的类加载器了,其父加载器为null。


五、总结

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆中创建一个这个类的java.lang.Class对象,用来封装类在方法区的数据。类的加载的最终产品是位于堆中的Class对象,其封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。

注意:ClassLoader的loadClass(String className);方法只会加载并编译某类,并不会对其执行初始化


补充:

Java绑定:绑定指的是把一个方法的调用与方法所在的类(方法主体)关联起来,对Java来说,绑定分为静态绑定和动态绑定。

①静态绑定:即前期绑定。在程序执行前方法已经被绑定,此时由编译器或其它连接程序实现。针对Java,简单的可以理解为程序编译期的绑定。Java 当中的方法只有 final,static,private 和构造方法是前期绑定的。

②动态绑定:即晚期绑定,也叫运行时绑定。在运行时根据具体对象的类型进行绑定。在Java中,几乎所有的方法都是后期绑定的。


六、参考资料

Java中类的加载过程介绍

java类加载过程--简书





原创粉丝点击