深入理解Java虚拟机类加载机制

时间:2021-07-11 10:39:39

1.类加载时机

        对于类加载的第一个阶段—--加载,虚拟机没有强制的约束,但是对于初始化阶段,虚拟机强制规定有且只有以下的5中情况必须开始初始化,当然,加载、验证、准备阶段在初始化前就已经开始。

  ①使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰静态字段除外)的时候,以及调用一个类的静态方法的时候。

  ②对类进行反射调用的时候

  ③当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。

  ④当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。

  ⑤当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

  上面的5种场景中的行为称为对一个类的主动引用。除此之外的其他引用,虚拟机都不会触发类的初始化,称为被动引用

  被动引用的例子:

/*
 *通过子类引用父类的静态字段,不会导致子类初始化
 **/
public class SuperClass {
    static {
        System.out.println("SuperClass init!");
    }
    public static int value = 123;
}
public class SubClass extends SuperClass{
    static{
        System.out.println("SubClass init!");
    }
}
/**
*非主动使用类字段演示
**/
public class NotInitialization{
    public static void main(String[]args){
        System.out.println(SubClass.value);
    }
}

结果:只会输出SuperClass init!

/**
*被动使用类字段演示二:
*通过数组定义来引用类,不会触发此类的初始化
**/
public class NotInitialization{
    public static void main(String[]args){
        SuperClass[]sca=new SuperClass[10];
    }
}

结果:不会输出SuperClass init!

/**
*非主动使用类字段演示
**/
public class NotInitialization{
    public static void main(String[]args){
        System.out.println(ConstClass.HELLOWORLD);
    }
}

结果:没有输出“ConstClass init!”

接口加载过程:只与类加载过程中的第三条不同,当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化。

2. 类加载过程

         2.1加载:

    加载阶段虚拟机要完成三件事:

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

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

    ③在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

        2.2 连接:

    2.2.1验证:

      验证阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。虚拟机主要做以下工作:

      ①文件格式验证:例如:以魔数0xCAFEBABE开头

      ②元数据验证:例如:是否有父类

      ③字节码验证:对类的方法体验证

      ④符号引用验证:例如:通过全限定名是否能找到对应的类

    2.2.2 准备:

       准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。如:

      public static int value=123;

      准备阶段只会把类变量的初始值(也就是0)赋值给value,而真正到初始化阶段才把123赋值给value。

      注意:如果是final修饰的字符串,虚拟机会在准备阶段就对变量做初始化。如下:

      public static final int value=123;

      编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为123。

    2.2.3 解析:

      解析阶段解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行解析。

  2.3初始化:

      在此之前的所有动作都是虚拟机来主导的,到了初始化阶段才真正执行类中定义的Java代码。也就是说由程序员写的用来初始化类变量或其他资源的代码在初始化阶段才得以执行。

      事实上,初始化过程就是是执行类构造器<clinit>()方法的过程,<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的。

      静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。如下代码:

//非法向前引用变量:
public class Test{
    static{
        i=0;//给变量赋值可以正常编译通过
        System.out.print(i);//这句编译器会提示"非法向前引用"
    }
    static int i=1;
}

3.类加载器

  3.1类与类加载器:

         通过一个类的全限定名(如:反射)来获取描述此类的二进制字节流类加载器,这个动作的代码模块称为类加载器。

   判断两个类是否相等:

   ①类名是否相同,包括包名;

   ②是否由同一个类加载器加载,只有在两个类由同一个类加载器加载的前提下才有意义,否则即使这两个类都来源于同一个class文件,而加载这个class文件的类加载器不同,那么这两个类也是不相等的。

   虚拟机提供了3种类加载器,引导(Bootstrap)类加载器、扩展(Extension)类加载器、系统(System)类加载器(也称应用类加载器)。

   启动类加载器:<JAVA_HOME>/lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包,出于安全考虑,只加载包名为java、javax、sun等开头的类。

   扩展类加载器:加载<JAVA_HOME>/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库

   应用程序类加载器:也称它为系统类加载器。加载系统类路径java -classpath或-D java.class.path 指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用系统类加载器。

      3.2 双亲委派模式:

深入理解Java虚拟机类加载机制

           当类加载器加载一个类的时候,它先委托其父类加载器加载这个类,同样的,父类加载器也委托其父类加载器加载这个类,一直到顶层的启动类加载器,启动类加载器默认是没有父类的,当启动类加载器无法加载此类时,就会委派给其子类加载器,同样的,一直委派下去,直到有子类加载器能成功加载此类,否则就报错。这就是双亲委派模式。

    使用双亲委派模式的优势:

          ①避免类的重复加载:当父类已经加载了这个类,子类就没有必要再加载一次。

          ②防止核心api库被篡改:当从网络中传过来一个java.lang.Object类时,类加载器通过双亲委派模式委派到启动类加载器,启动类加载器发现这个类已经加载了,就不会再去加载传过来的这个类,而是直接返回已经加载的类。

          破坏双亲委派模式:

    ①使用线程上下文类加载器,当基础类要调用回用户的代码,这时由于双亲委派模式的存在,启动类加载器并不能委派给子类加载器去加载类,此时就要用到线程上下文类加载器来解决。常见有JNDI、JDBC、JCE、JAXB和JBI等。

    以jdbc为例:<JAVA_HOME>/lib下已经封装好了jdbc的接口,由各个不同的数据库厂商按照接口的规范来编写这些接口的实现类,但是这些实现类通常都是放在classpath路径下,启动类加载器需要调用这些接口的实现类,但它并不能加载classpath路径下的类,classpath路径下的类只能由系统类加载器加载,又由于双亲委派模式的存在,只能由子类加载器委托父类加载器加载类,不能由父类加载器委派给子类加载器加载类。于是便出了一个折中的办法,当启动类加载器需要调用这些接口的实现类时,它委托线程上下文类加载器去加载这些实现类。默认情况下,线程上下文类加载器就是系统类加载器。

    ②代码热替换(HotSwap)、模块热部署(Hot Deployment):

    意思就是希望应用程序能像我们的计算机外设那样,接上鼠标、U盘,不用重启机器就能立即使用,鼠标有问题或要升级就换个鼠标,不用停机也不用重启。具体应用例子有Tomcat下的项目的自动发布,jsp修改后无需重启服务器等。

4.Tomcat分析

深入理解Java虚拟机类加载机制

  Common类加载器:加载tomcat路径下的lib目录下class文件

  Webapp类加载器:加载tomcat路径下的/WebApp/WEB-INF/*下class文件

  Jsp类加载器:它的加载范围仅仅是这个JSP文件所编译出来的那一个Class,它出现的目的就是为了被丢弃:当服务器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能。

  WebApp类加载器和Jsp类加载器通常会存在多个实例,每一个Web应用程序对应一个WebApp类加载器,每一个JSP文件对应一个Jsp类加载器。