JAVA类加载探索

虚拟机类加载过程

Java虚拟机把描述类的数据从class文件加载到内存,并且对数据进行校验、转换解析和初始化,形成可以被虚拟机直接使用的java类型,这个过程称为类的加载过程,类的加载、连接、初始化都是在运行过程进行的

类加载的过程

  • 加载

此下三步也成为连接阶段

  • 验证

  • 准备

  • 解析(有可能是在初始化阶段开始之后才开始)

  • 初始化

  • 使用

  • 卸载

一.初始化(在此之前一定要执行前四个操作)

必须初始化的时机!!!有且仅有

  • 遇到newgetstaticputstaticinvokestatic四个指令时,如果没有被初始化则需要先触发初始化阶段,而产生这四个指令的场景是:

    • 使用new关键字实例化对象

    • 读取或设置一个类型里的static对象(被final修饰的在编译的时候已经被放入常量池)

    • 调用一个类型的静态方法

  • 使用reflect进行反射的时候,要反射产生的类型没有初始化的时候要触发初始化

  • 初始化类型的时候,如果父类没有被初始化,先初始化父类 //接口初始化不用初始化父类

  • 虚拟机启动先初始化带有main()方法的类

  • 使用JDK7加入的动态语言支持,如果一个java.lang.invoke.MethodHandle实例解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有初始化的时候先进行初始化。

  • 接口定义了JDK8新加入的默认方法(被default关键字修饰的接口方法)时,如果这个接口的实现类发生了初始化,接口在其之前要初始化。

所有引用类型的方式都不会出发初始化

1.子类的初始化与否取决于虚拟机的实现

对于静态字段,只有直接定义这个字段的类才会被初始化

 public class SuperClass {
 
     static {
         System.out.println("Super Class init");
    }
 
     public static int value = 123;
 }
 
 class SubClass extends SuperClass{
 
     static {
         System.out.println("Sub Class init");
    }
 }
 
 class NotInitiation{
     public static void main(String[] args) {
         System.out.println(SubClass.value);
    }
 }
 
 
 out:
 Super Class init
 123
     

2.这种情况不会造成类的初始化,但是jvm会触发一个名为[L(全限定名)的类的初始化工作,数组中应有的方法都储存在里面。

 class NotInitiation{
     public static void main(String[] args) {
         SuperClass[] sca = new SuperClass[10];
    }
 }
 
 out:
 no out

3.final值的初始化阶段在编译成class文件阶段就已经实现(生成contentValue表储存在class文件的常量池中),然后通过常量传播将final的值储存进其他class文件的常量池中,对该final值的引用实际上是对自身常量池中的一个值的引用

接口的父接口的初始化在使用的时候才会初始化(如引用父接口中的变量)

init / clinit

  1. init和clinit方法执行时机不同

init是对象构造器方法,也就是说在程序执行 new 一个对象调用该对象类的 constructor 方法时才会执行init方法,而clinit是类构造器方法,也就是在jvm进行类加载—–验证—-解析—–初始化,中的初始化阶段jvm会调用clinit方法。

  1. init和clinit方法执行目的不同

init is the (or one of the) constructor(s) for the instance, and non-static field initialization. clinit are the static initialization blocks for the class, and static field initialization. 上面这两句是Stack Overflow上的解析,很清楚init是instance实例构造器,对非静态变量解析初始化,而clinit是class类构造器对静态变量,静态代码块进行初始化。

clinit

  1. <clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问如下代码

  2. 虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。 因此在虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object。由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作,如下代码中,字段B的值将会是2而不是1。

     static class Parent{
         public static int A=1
         static{
         A=2;}
         static class Sub extends Parent{
         public static int B=A;
        }
         public static void main(String[]args){
         System.out.println(Sub.B);
        }
     }
  3. 接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成<clinit>()方法。 但接口与类不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。 只有当父接口中定义的变量使用时,父接口才会初始化。 另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法。

二.加载

  1. 根据类的全限定名获得二进制文件,说明不限定二进制文件的获得过程,不一定要从class文件中获取,最常见的就是从jar包中获取

  2. 将该二进制文件的静态结构转化为方法区运行时所需的数据结构

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

特殊的类型:数组类型

数组类的加载是虚拟机在内存中动态构造出来的,但是数组的元素类型最终还是要靠加载器加载。

  1. 如果数组的组件类型是引用类型,那么会递归加载这个类,数组类会被标示在加载该组件类型的类加载器的类名称空间上(一个类型必须与类加载器一起确定唯一性)

  2. 如果不是引用类型,将数组类标记为与引导类加载器关联。

  3. 数组类的可访问性和他的组件类型的可访问性一样。

加载阶段完成后,会在方法区安置类型数据之后,在堆中生成一个类型实例作为程序访问方法区中的类型数据的外部接口。

 

三.验证

包括以下几个部分:

  1. 文件格式的验证:例如是否以魔数CAFAEBABE开头、主从版本是否可以接受等。经过该验证后就允许二进制字节流放入内存,此后的三个验证阶段都是基于方法区的储存结构进行。

  2. 元数据验证:

    • 除了Object类所有类都应该有父类

    • 类中的字段、方法等是否与父类冲突

  3. 字节码验证:通过数据流与控制流分析保证程序语义是合法的,符合逻辑的,即对class文件的code属性进行分析校验。优化:与javac编译器联合优化,在code属性表中增加StackMapTable描述方法体的所有基本块开始时的本地变量与操作栈应该有的状态,所以只需要检查这个StackMapTable就可,即在编译阶段就确定。

  4. 符号引用的校验:发生在将符号引用转化为直接引用。

 

四.准备

将类变量赋初值,即static变量(0),final变量(用户自定义)

 

五.解析

如果说D拥有C的访问权限,那么至少满足一个要求:

  1. C是public的,并且与D处于同一个模块中。

  2. C是public的,不与D在同一个模块,但是C的模块被允许访问D的模块。

  3. C不是public的但是D与C在同一个包中。

java虚拟机将符号引用替换为直接引用的过程

符号引用: 符号引用就是一组符号用来描述引用的目标,符号引用并不指向真实的内存地址

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

句柄:在java中我们在实例化完对象后,在对其进行操作时,用来去操作对象的就叫做句柄。他代表了当前对象的唯一一个标识,并不能代表当前对象的[内存)

 Tree t1 = new Tree(); ti就是句柄
 
 //引申一下 String相加时的**+**号调用了StringBuilder的append()方法,而底部实现是调用了new String(value, 0 , count)

解析动作主要针对 类、接口、类方法、字段、接口方法、方法类型、方法句柄、调用限定符这七类符号引用进行

  1. 类或接口的解析

    当前代码所处类D,把符号引用N解析为一个类或接口C的直接引用

    • 如果C不是一个数组类型,虚拟机把N的全限定名传递给D的类加载器去加载这个类C,在加载过程中可能会出现元数据验证、字节码验证等触发其他类的加载动作,如果加载过程出现失败则解析失败;

    • 如果C是一个数组类型,并且数组元素类型为对象,由虚拟机生成一个代表该数组维度和元素的数组对象。

    • 解析完成前要进行符号引用验证,确认D是否对C具有 访问权限,如果没有抛出异常。

  1. 字段解析

    • 对字段表内的class_index中的索引项进行解析(类解析),把字段所属的类或接口用C表示,那么

      • 如果C本身包含了简单名称与字段描述符都一样的字段,解析成功

      • 否则,如果C中实现了接口,会依照继承关系从下往上递归搜索各个接口和他的父接口,如果包含,解析成功

      • 否则,按照继承关系从下往上递归搜索其父类(obj除外,找到则改为直接引用,结束。

      • 失败,抛出java.lang.NoSuchFieldError异常

      返回还会对权限进行验证。所以是先接口后父类,具体的编译器在遇到父类与子类有同名的字段时会拒绝编译。

  2. 方法解析

    • 第一步与字段解析一样,首先解析方法表中的class_index,用C表示这个类

      • 若发现C是一个接口的话,抛出异常(类的方法引用和接口的方法引用常量的定义是分开的)

      • 在类C的方法表中查找

      • 在父类中查找

      • 在接口列表与父接口中查找,如果找到说明是抽象类,查找结束,抛出异常

      • 失败

  3. 接口方法解析

    • 第一步相同,解析成功用C表示这个接口

      • 如果发现C是一个类,那么解析结束,抛出异常

      • 在接口C中查找相匹配的方法

      • 在接口C的父接口中查找,还有Object中查找(接口的查找范围也包括Object)

      • 同样,对于多继承接口,编译器在出现相同名称的方法出现在不同父类的接口会编译失败

      • 查找失败

      最后还要查找访问控制权限。

    类加载器

    每一个类加载器都有拥有一个独立的类名称空间,只加载该空间中的类,即使两个类来源于同一个class文件,如果他们的类加载器不相同,那么equals(),isAssignableFron(),isInstance()都是返回不相同结果。

    类加载器的分类

    image.png

    • 第一个:启动类/引导类:Bootstrap ClassLoader

    这个类加载器使用C/C++语言实现的,嵌套在JVM内部,java程序无法直接操作这个类。

    它用来加载Java核心类库,如:JAVA_HOME/jre/lib/rt.jarresources.jarsun.boot.class.path路径下的包,用于提供jvm运行所需的包。

    并不是继承自java.lang.ClassLoader,它没有父类加载器

    它加载扩展类加载器应用程序类加载器,并成为他们的父类加载器

    出于安全考虑,启动类只加载包名为:java、javax、sun开头的类

    如果需要把加载类交给启动类加载,直接使用null代替即可。

    • 第二个:扩展类加载器:Extension ClassLoader

    Java语言编写,由

     sun.misc.Launcher$ExtClassLoader

    实现,我们可以用Java程序操作这个加载器

    派生继承自java.lang.ClassLoader,父类加载器为启动类加载器

    从系统属性:java.ext.dirs目录中加载类库,或者从JDK安装目录:jre/lib/ext目录下加载类库。加载用户类路径CLassPath下的类。我们就可以将我们自己的包放在以上目录下,就会自动加载进来了。

    • 第三个:应用程序类加载器:Application Classloader

    Java语言编写,由

     sun.misc.Launcher$AppClassLoader

    实现。

    派生继承自java.lang.ClassLoader,父类加载器为启动类加载器

    它负责加载环境变量classpath或者系统属性java.class.path指定路径下的类库

    它是程序中默认的类加载器,我们Java程序中的类,都是由它加载完成的。

    我们可以通过ClassLoader#getSystemClassLoader()获取并操作这个加载器

    • 第四个:自定义加载器

    一般情况下,以上3种加载器能满足我们日常的开发工作,不满足时,我们还可以

     自定义加载器

    比如用网络加载Java类,为了保证传输中的安全性,采用了加密操作,那么以上3种加载器就无法加载这个类,这时候就需要自定义加载器

    自定义加载器实现步骤

    继承

     java.lang.ClassLoader

    类,重写findClass()方法

    如果没有太复杂的需求,可以直接继承URLClassLoader类,重写loadClass方法,具体可参考AppClassLoaderExtClassLoader

    获取ClassLoader几种方式

    它是一个抽象类,其后所有的类加载器继承自 ClassLoader(不包括启动类加载器)

     // 方式一:获取当前类的 ClassLoader
     clazz.getClassLoader()
     // 方式二:获取当前线程上下文的 ClassLoader
     Thread.currentThread().getContextClassLoader()
     // 方式三:获取系统的 ClassLoader
     ClassLoader.getSystemClassLoader()
     // 方式四:获取调用者的 ClassLoader
     DriverManager.getCallerClassLoader()

    类加载机制—双亲委派机制

    jvm对class文件采用的是按需加载的方式,当需要使用该类时,jvm才会将它的class文件加载到内存中产生class对象。

    在加载类的时候,是采用的双亲委派机制,即把请求交给父类处理的一种任务委派模式。

    image.png

    • 工作原理

    (1)如果一个类加载器接收到了类加载的请求,它自己不会先去加载,会把这个请求委托给父类加载器去执行。

    (2)如果父类还存在父类加载器,则继续向上委托,一直委托到启动类加载器:Bootstrap ClassLoader

    (3)如果父类加载器可以完成加载任务,就返回成功结果,如果父类加载失败,就由子类自己去尝试加载,如果子类加载失败就会抛出ClassNotFoundException异常,这就是双亲委派模式

     

       protected Class<?> loadClass(String name, boolean resolve)
             throws ClassNotFoundException
        {
             synchronized (getClassLoadingLock(name)) {
                 // First, check if the class has already been loaded
                 //如果类被加载过就返回
                 Class<?> c = findLoadedClass(name);
                 if (c == null) {
                     long t0 = System.nanoTime();
                     try {
                         //通常用组合的方式实现,即包括parent
                         if (parent != null) {
                             c = parent.loadClass(name, false);
                        } else {
                             c = findBootstrapClassOrNull(name);
                        }
                    } catch (ClassNotFoundException e) {
                         // ClassNotFoundException thrown if class not found
                         // from the non-null parent class loader
                    }
     
                     if (c == null) {
                         // If still not found, then invoke findClass in order
                         // to find the class.
                         long t1 = System.nanoTime();
                         c = findClass(name);
     
                         // this is the defining class loader; record the stats
                         PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                         PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                         PerfCounter.getFindClasses().increment();
                    }
                }
                 
                 if (resolve) {
                     resolveClass(c);
                }
                 return c;
            }
        }

     

    • 第三方包加载方式:反向委派机制

    在Java应用中存在着很多服务提供者接口(Service Provider Interface,SPI),这些接口允许第三方为它们提供实现,如常见的 SPI 有 JDBC、JNDI等,这些 SPI 的接口属于 Java 核心库,一般存在rt.jar包中,由Bootstrap类加载器加载。而Bootstrap类加载器无法直接加载SPI的实现类,同时由于双亲委派模式的存在,Bootstrap类加载器也无法反向委托AppClassLoader加载器SPI的实现类。在这种情况下,我们就需要一种特殊的类加载器来加载第三方的类库,而线程上下文类加载器(双亲委派模型的破坏者)就是很好的选择。

    从图可知rt.jar核心包是有Bootstrap类加载器加载的,其内包含SPI核心接口类,由于SPI中的类经常需要调用外部实现类的方法,而jdbc.jar包含外部实现类(jdbc.jar存在于classpath路径)无法通过Bootstrap类加载器加载,因此只能委派线程上下文类加载器把jdbc.jar中的实现类加载到内存以便SPI相关类使用。显然这种线程上下文类加载器的加载方式破坏了“双亲委派模型”,它在执行过程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器,当然这也使得Java类加载器变得更加灵活。

    image.png

    JDK6提供了java.util.ServiceLoader类,以META—INF/services中的配置信息,加上责任链的模式才有了相对合理的解决方式。

    • 沙箱安全机制

    自定义 String 类,但是在加载自定义 String 类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载 JDK 自带的文件(rt.jar 包中的 javalangString.class),报错信息说没有 main 方法就是因为加载的 rt.jar 包中的 String 类。这样可以保证对 Java 核心源代码的保护,这就是沙箱安全机制

    JAVA模块化系统(JPMS)

    java模块定义包括

    1. 依赖其他模块的列表

    2. 到处包的列表,也就是其他模块使用的列表

    3. 开放包的列表,也即可反射访问模块的列表

    4. 使用的服务列表

    5. 提供服务的实现列表

    6.  

     

 

 

 

posted @ 2022-03-16 21:45  码虫垒起代码之城  阅读(56)  评论(0编辑  收藏  举报