Java ClassLoader机制及初始化步骤

Java类加载及变量初始化过程

Java虚拟机是如何将编译好的class文件加载成为Java类型?加载之后如何初始化?静态变量、静态代码块的初始化顺序以及继承中子类和父类的初始化顺序?
在学习Java的过程中经常会遇到这种问题,最初baidu、Google一下能搞清顺序,但不明白其内部流程,因而过段时间就会忘记。最近涉及到静态内部类单例模式和普通单例模式的对比,始终想不通类加载和变量初始化的机制,因而查找资料,整理一下。

Java类加载机制

类加载是通过ClassLoader来实现的,由于不同的JVM实现不同,本文仅限于常用的Hotspot JVM实现。

JDK默认的ClassLoader

JDK默认提供了一下三种类加载器:

  1. Bootstrp loader

    Bootstrp加载器是Java虚拟机的引导加载器,在Java虚拟机启动后初始化的,负责加载Java的核心类库,包括%JAVA_HOME%/jre/lib,-Xbootclasspath参数路径以及%JAVA_HOME%/lib/classes中的类库,由C++编写

  2. ExtClassLoader

    由Bootstrp加载,并且其父加载器为Bootstrp,负责加载Java的扩展类库,包括%JAVA_HOME%/jre/lib/ext下的类库,由Java编写,sun.misc.Launcher$ExtClassLoader

  3. AppClassLoader

    由Bootstrp加载,并且其父加载器为ExtClassLoader,负责加载应用以及应用classpath下的类库,是Java应用程序默认的加载器,由Java编写,sun.misc.Launcher$AppClassLoader

双亲委托模型

Java的ClassLoader都采用双亲委托机制,即为每个类加载器都指定一个父加载器,并在当前加载器中保存父加载器的引用。双亲委托机制的主要作用是为了防止类的重复加载,采用双亲委托机制加载类时的步骤如下:

  1. 当前ClassLoader首先从自己已经加载的类中查询是否已经加载了该类,如果已经加载了则直接返回该类。

    每个类加载器都有自己的加载缓存,当一个类被加载之后就会放入缓存,私以为就是PermGen Space

  2. 当前ClassLoader没有找到该类,则委托其父加载器去加载,父加载器采用同样的策略,一直到Bootstrp加载器。

  3. 当所有的父加载器都没有加载时,返回委托加载的源头即最初的加载器,由其根据文件路径去找到相应的jar包进行加载。

那么问题来了,Java虚拟机如何确定两个类是不是同一个类呢。一方面通过类的全限定名,另一方面是通过ClassLoader名。即完全相同的两个class文件,由不同的ClassLoader进行加载,在Java虚拟机中会被认为是两个不同的类,当然由于双亲委托机制的存在,这种情况基本不会存在,不然就会出现在不知情的情况下个人自定义的String代替JDK基本String的情况。

不遵循双亲委托的情景

上面说了双亲委托机制是为了实现不同的ClassLoader之间加载类的交互问题,被大家公用的类就交由父加载器进行加载,但是Java中也确实存在父加载器中需要用到子加载器中加载的类的情况。
Java中有一个SPI(Service Provider Interface)标准,使用了SPI的库,诸如JDBC、JNDI等,我们都知道JDBC需要第三方提供的驱动才可以,而包含驱动的jar放在个人应用的classpath下。JDBC本身的api是JDK提供的一部分,它已经被Bootstrp加载了,那么第三方厂商提供的实现类怎么加载呢?这里Java引入了线程上下文加载的概念,线程类加载器默认从父线程继承,如果没有指定的话就是APPClassLoader,这样的话当加载第三方驱动时就可以通过线程的上下文类加载器来加载。

Java虚拟机加载class文件的过程

虚拟机将class文件加载到内存,通过校验、解析和初始化,最终形成Java类型。加载、校验、准备、解析、初始化这5个步骤的顺序是确定的,但是在Java标准规范中并没有强制规定什么时候开始加载,但是却规定了几种情况必须初始化。

  1. 遇到new、getstatic、putstatic、invokestatic这4条字节码指令,如果类没有进行过初始化,则触发初始化。
  2. 使用Java.lang.reflect包的方法进行反射调用时,如果没有初始化则先进行初始化。
  3. 初始化一个类时如果发现父类没有初始化,则先初始化父类。

加载、校验、解析

加载就是通过类的全限定名,获取类的二进制字节流,然后将此字节流转换为方法去的数据结构,在内存中生成一个代表此类的Class对象的过程。验证 是为了为了确保Class文件中的字节流符合虚拟机的要求,并且不会危害虚拟机的安全。解析是虚拟机将常量池中的符号引用转换为直接引用的过程。class文件采用一种类似C语言的结构体的伪结构来存储我们编码的java类的各种信息。其中,class文件中常量池(constant_pool)是一个类似表格的仓库,里面存储了我们编写的java类的类和接口的全限定名,字段的名称和描述符,方法的名称和描述符。在java虚拟机将class文件加载到虚拟机内存之后,class类文件中的常量池信息以及其他的数据会被保存到java虚拟机内存的方法区。我们知道class文件的常量池存放的是java类的全名,接口的全名和字段名称描述符,方法的名称和描述符等信息,这些数据加载到jvm内存的方法区之后,被称做是符号引用。而把这些类的全限定名,方法描述符等转化为jvm可以直接获取的jvm内存地址,指针等的过程,就是解析。

准备

在Java虚拟机加载了class文件并且验证完毕之后,就会正式给类变量分配内存空间并设置变量的初始值。这些类变量所使用的的内存都会保存在方法区(PermGen Space)中,这里说的类变量也就是通过static修饰的静态变量。比如在public static int value=123;中,在执行准备阶段时会给value分配内存并设置初始值为0,而不是现象中的123.

初始化阶段

类初始化阶段是类加载的最后阶段,在这个阶段才会真正意义上的执行类中定义的Java代码。Java虚拟机是怎样完成初始化的呢?这要从编译讲起。在编译阶段,编译器会自动收集类中的所有静态变量和静态代码块(static{}块)并将其合并,编译器的收集顺序是按照他们在类中的定义顺序。收集完成之后会编译Java类中的static{}方法,Java虚拟机则会保证一个类中的static{}块在多线程或单线程环境下都正确的执行,并且只执行一次。在执行过程中便完成了static变量的初始化。当我们的Java代码中没有显示的声明static代码块,而只是定义了静态变量的话,编译器会默认给我们生成一个static{}方法。

如果出现了继承的情况,虚拟机会保证在子类的static{}代码块执行之前,父类的static已经执行完毕。

参考博客地址

Java类加载及变量初始化过程 https://my.oschina.net/kalo/blog/323078

Java Classloader机制解析 https://my.oschina.net/aminqiao/blog/262601

你可能感兴趣的:(Java ClassLoader机制及初始化步骤)