JVM读书笔记之类虚拟机类加载机制

来源:互联网 发布:安卓 平板 软件 编辑:程序博客网 时间:2024/05/22 14:22

上一个博客我们简单的介绍了Java虚拟机(http://blog.csdn.net/ghostwarden/article/details/78249597),这次我们详细说明下类加载机制。

一.为什么需要类加载机制

     .java文件(不同的JVM语言后缀名不同)经过编译器编译成.class文件之后,如果要在JVM上运行,必须把描述类的数据从.class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。(注:我们都知道Java是面向对象的语言,每个.class文件对应一个类,在进行程序设计与开发时,要尽量保证类与类之间的低耦合与高内聚。)不同的数据类型加载到虚拟机的不同地方(JVM将其内存划分为五大部分),程序运行时,会不断地调用加载到JVM中的类或将未加载到JVM中的类加载然后调用。


二. Java类生命周期

      类的生命周期包括:加载,验证,准备,解析,初始化,使用和卸载这七个阶段。其中验证,准备和解析三个阶段称为连接。加载,验证,准备,初始化和卸载这五个阶段的开始顺序是确定的,类加载过程必须按照这种顺序按部就班的开始(注:是开始,不是运行,在它们的运行期,这些阶段可能是交叉进行的)。

                              

三. Java类加载与初始化

      对与类加载的时机,虚拟机规范中没有进行强制约束,这一点交给虚拟机的具体实现自由把握。但是对于初始化阶段,虚拟机严格规定六种情况必须进行初始化:

  • 遇到new(new关键字实例化对象的时候), getstatic,putstatic(读取或设置一个类的静态字段)或invokestatic(调用一个类的静态方法)4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。注意:对于静态字段,调用它只有直接定义这个字段的类才会被初始化,即使是这个类的子类也不会初始化
  • 访问某个类或接口的静态变量,或者对该静态变量赋值(注:如果访问编译期即可确定值得常量,不会导致类的初始化)
  • 调用类的静态方法
  • 使用java.lang,reflect包的方法对类进行发射调用的时候,如果类没有进行初始化,则需要先初始化。
  • 当初始化一个类时,如果发现其父类没有进行初始化,先触发其初始化。
  • 当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化这个主类(Main 线程)。
    以上这场景称为对类的主动引用。除此之外的引用方式都不会触发初始化,称为被动引用。看到这里我们不由的想问一个问题,类的初始化与加载之间是什么关系?
       类的加载是指将.class文件中类的数据加载到JVM内存之中;初始化指的是类或接口被Java程序首次主动调用时才会初始化。加载一个类的.class文件,不意味着该类被初始化,必须要经过加载,连接,初始化才能叫做类的初始化。我们也要注意初始化与实例化的区别!! 
     在很多Java面试题中会遇到类初始化的顺序问题,困扰了我很久,类初始化有以下规则:
  •  类从顶至底的顺序初始化,声明在顶部的字段遭遇底部的字段初始化
  • 超类早于子类初始化
  • 如果类的初始化是由于访问静态域而触发,那么只有声明静态域的类才被初始化,而不会触发超类或子类的初始化
  • 接口的初始化不会导致父类接口的初始化
  • 静态域的初始化是在类的静态初始化期间,非静态域的初始化时在类的实例创建期间。这意味这静态域初始化在非静态域之前
  • 非静态域通过构造器初始化,子类在做任何初始化之前构造器会隐含地调用父类的构造器,保证非静态或实例变量(父类)初始化早于子类

四. 类加载过程
1.加载 
         加载阶段,虚拟机需要完成以下三件事:
          1)通过一个类的全限定名来获取定义此类的二进制字节流
          2)将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构(包括类的完整有效名;直接父类的完整有效名;类的类型修饰符;类的直接接口;类型的常量池;域信息;方法信息;所有的静态变量);
          3)在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。
          注意:获取二进制字节流不一定要从Class文件中获取,可以从ZIP包中读取(比如从jar包和war包中读取),也可以在运行时计算生成(动态代理),也可以由其它文件生成(比如将JSP文件转换成对应的Class类)。
2.验证
         验证是连接阶段的第一步,为了确保class文件的字节流包含信息符合当前虚拟机的要求。虚拟机规范没有明确的定义检查什么,怎么检查,何时检查,不同的虚拟机有不同的实现,但大致上都会进行如下验证:
           1)文件格式验证,保证输入的字节流能够正确解析并存储于方法区内,格式上符合Java类型信息描述;
           2)元数据验证,进行语义分析,保证符合Java语言规范
           3)字节码验证,进行数据流和控制流校验分析,保证校验类的方法运行时不会做危害虚拟机的事
           4)符号引用验证,对类自身之外(常量池中的各种符号引用)的信息进行匹配性的校验。
3.准备
          正式为类变量(static变量,仅仅是对类变量进行内存分配,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中)分配内存并设置初始值得阶段,这些内存都在方法区中分配。通常情况下,初始值一般情况是对类型的零值赋值,例如:private static int i = 10:准备阶段就赋值为0,在初始化阶段赋值为10;private final static int i = 10:则在准备阶段就赋值为10。
4.解析
          解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。符号引用:是以一组符号来表述所引用的目标,可以是任何形式的字面量,只要使用时无歧义地定位到目标即可。符号引用与虚拟机的内存无关,引用目标不一定在内存中。直接引用:可以是直接指向目标的指针、相对偏移量或是直接能定位到目标的句柄,直接引用与虚拟机内存相关,如果进行了直接引用,则引用目标比在内存中。
          虚拟机没有规定发生解析具体时间,只要求在执行anewaray,checkcast,getfield,getstatic,instanceof,
Invokeinterface,invokespecial,invokestatic,invokevirtual,mulianewarray,new,putfield,putstatic这13个用于操作符引用的字节码指令之前,先对它们所使用的符号进行解析。
          解析动作主要是针对类或接口、字段、类方法和接口方法四类符号引用进行。
5.初始化
           类初始化是类加载过程的最后一步,前面的类加载过程中,除了加载阶段用程序可以通过自定义ClassLoader加载,其余动作完全由JVM控制。初始化阶段才真正开始执行类中自定义的Java字节码。在准备阶段,变量已经赋值过一次(按照JVM的规定),在初始化阶段,变量的赋值则是按照程序的主管计划进行赋值(执行类构造器<clinit>()过程)。
         <clinit>( )方法:
         1.<clinit>()方法是由编译器自动收集类中的所有变量赋值动作和静态语句块中的语句按照在程序中的出现顺序合并而成,
         2.与实例构造器<init>( )方法不同,不需要显式的调用父类实例构造器。虚拟机会保证父类的<clinit>( )方法提前执行;
         3.由于父类的<clinit>( )方法提前执行,因此父类中的静态语句块要优于子类变量赋值语句。
         4.<clinit>( )方法不是必须的,如果一个类中没有静态语句块,也没有对变量的赋值操作,则编译器不会为这个类产生<clinit>( )方法
         5.接口中不能使用静态语句块,但是可以有变量初始化赋值操作,但是接口与类的不同之处在于:执行接口的<clinit>( )方法不需要先执行父接口的<clinit>( )方法,只有父类接口中定义的变量被使用时,才会调用父类<clinit>( )方法。
         6.虚拟机会保证一个类的<clinit>( )方法在多线程环境下被正确的加锁与同步。

五. 类的加载方式与动态加载
       JVM加载类文件到内存有两种方式: 
  1. 隐式加载:所谓隐式加载就是不通过在代码里调用ClassLoader来加载需要的类,而是通过JVM来自动加载需要的类到内存的方式。例如,当我们在类中继承或者引用某个类时,JVM在解析这个类时发现引用的类不在内存中,那么就会自动将这些类加载到内存中。动态加载!!
  2. 显式加载:相反的显式加载就是我们在代码中通过调用ClassLoader类来加载个类一个类的方式,例如,调用this.getClass.getClassloader().loadClass(); Class.forName();我们自己实现ClassLoader的findClass()方法
      上面提到了Java有一个重要的特性-------类动态加载。
  1. Java程序在运行时并不一定被完整加载,只有当发现该类还没有加载时,才去本地或远程查找类的.class文件并验证和加载;
  2. 当程序创建了第一个对类的静态成员的引用(如类的静态变量、静态方法、构造方法——构造方法也是静态的)时,才会加载该类。
六. 类加载器
      类加载器(ClassLoader)是用来动态的加载class文件到虚拟机中,并转换成java.lang.class类的一个实例,每个这样的实例用来表示一个java类,我们可以根据Class的实例得到该类的信息,并通过实例的newInstance()方法创建出该类的一个对象,除此之外,ClassLoader还负责加载Java应用所需的资源,如图像文件和配置文件等。
      ClassLoader任务:1.Class加载到JVM(javac编译之后的java字节码,.class文件);2.审查每个类由谁加载,父类优先的等级加载机制;3.将Class字节码重新解析成JVM统一要求的对象格式。
      在第二步审查每个类由谁加载时,有非常重要的一点就是双亲委派模型。JVM平台提供三层ClassLoader,分别:
  1. BootstrapClassLoader,它主要加载JVM自身需要的类,如java.lang.*、java.uti.*等; 这些类位于$JAVA_HOME/jre/lib/rt.jar。这个ClassLoader完全是由 JVM自己控制的,需要加载哪个类,怎么加载由JVM自己控制,别人也访问不到这个类,所以这个ClassLoader是不遵守前面介绍的加载规则的,它仅仅是一个类的加载工具而已,既没有更高一级的父加载器,也没有子加载器。当JVM启动是BootstrapClassLoader随之启动,负责加载完核心类库后,并构造ExtensionClassLoader和AppClassLoader类加载器。
  2. ExtClassLoader,这个类加载器有点特殊,它是JVM自身的 一部分,但是它的血统也不是很纯正,它并不是JVM亲自实现的,我们可以理解为这个类加载器是加载一些扩展类。它服务的特定目标在System.getProperty("java.ext.dirs”)目录下。
  3. AppClassLoader,这个类加载器就是用于加载普通的POJO类,它的父类是ExtClassLoader。它服务的目标是广大普通类,所有在 System.getProperty("java.class.path”)目录下的类都可以被这个类加载器加载,这个目录就是我们经常用到的classpath。
  4. 我们还可以通过java.lang.ClassLoader子类自定义ClassLoader,这些自定义的类加载器统称为CustomerClassLoader。
      双亲委派模型就是基于以上四个ClassLoader进行工作的,其工作流程是:如果一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是把这个类请求委派给父类加载器去完成,每一层加载器都是如此,因此所有的加载请求最后都会被传递到顶层的启动类加载器,只有当父类加载器反馈自己无法完成这个加载请求时,子类加载器才会尝试自己去加载。因此判断加载的顺序是从底向上,加载的顺序是从顶向下(这一点很重要)。
                                                           
      那么为什么要用双亲委派模型,它有什么优点呢?Java类随着类加载器一起具备了层次优先级的层次关系,避免了类被重复加载,已经加载到内存中的类不会被再次加载,Java体系更具有稳定性。