类加载子系统

类加载子系统的作用

类加载子系统负责从文件系统或者网络中加载class文件,class文件在文件开头有特定的文件标识即16进制CA FE BA BE。

加载后的Class类信息存放于一块成为方法区的内存空间。除了类信息之外,方法区还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)。

来一张经典的JVM内存结构图:其中类加载器的工作范围只限于下图的左半部分,不包含调用构造器实例化对象。

ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine(执行引擎)决定。

如果调用构造器实例化对象,则其实例存放在堆区。

类加载子系统功能细分

加载

  • 通过一个类的全限定明获取定义此类的二进制字节流;
  • 将这个字节流所代表的的静态存储结构转化为方法区的运行时数据;
  • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

链接

链接分为三块,即验证、准备、解析。

验证

目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。

主要包括四种验证,文件格式验证,源数据验证,字节码验证,符号引用验证。

准备

为类变量分配内存并且设置该类变量的默认初始值,即零值;

这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化;

类不会为实例变量分配初始化,实例变量是会随着对象一起分配到java堆中。

解析

将常量池内的符号引用转换为直接引用的过程

事实上,解析操作往往会伴随着jvm在执行完初始化之后再执行。

符号引用就是一组符号来描述所引用的目标。符号应用的字面量形式明确定义在《java虚拟机规范》的class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT_Class_info/CONSTANT_Fieldref_info、CONSTANT_Methodref_info等。

初始化

初始化阶段就是执行类构造器方法clinit()的过程。

clinit()即“class or interface initialization method”,注意它并不是指构造器init()。

此方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。 

我们注意到如果没有静态变量c,那么字节码文件中就不会有clinit方法。

构造器方法clinit()中指令按语句在源文件中出现的顺序执行。

虚拟机必须保证一个类的clinit()方法在多线程下被同步加锁。即一个类只需被clinit一次,之后该类的内部信息就被存储在方法区。

父子类初始化变量和方法的执行顺序(静态代码块和静态变量谁在前面就先执行谁,先父类后子类)

  1. 父类静态代变量
  2. 父类静态代码块
  3. 子类静态变量
  4. 子类静态代码块
  5. 父类非静态变量(父类实例成员变量)
  6. 父类构造函数
  7. 子类非静态变量(子类实例成员变量)
  8. 子类构造函数

类加载器分类

JVM支持两种类型的加载器,分别为引导类加载器C/C++实现(BootStrap ClassLoader)和自定义类加载器由Java实现。

从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。

注意上图中的加载器划分关系为包含关系,并不是继承关系。

按照这样的加载器的类型划分,在程序中我们最常见的类加载器是:引导类加载器BootStrapClassLoader、自定义类加载器(Extension Class Loader、System Class Loader、User-Defined ClassLoader)

类路径classpath会比lib目录的jar包优先加载,只有classes下不存在才会加载jar包下的。

自定义类与核心类库的加载器

对于用户自定义类来说:将使用系统类System Class Loader加载器中的AppClassLoader进行加载

java核心类库都是使用引导类加载器BootStrapClassLoader加载的。

/**
 * ClassLoader加载
 */
public class ClassLoaderTest {
    public static void main(String[] args) {
        //获取系统类加载器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2

        //获取其上层  扩展类加载器
        ClassLoader extClassLoader = systemClassLoader.getParent();
        System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@610455d6

        //获取其上层 获取不到引导类加载器
        ClassLoader bootStrapClassLoader = extClassLoader.getParent();
        System.out.println(bootStrapClassLoader);//null

        //对于用户自定义类来说:使用系统类加载器进行加载
        ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
        System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2

        //String 类使用引导类加载器进行加载的  -->java核心类库都是使用引导类加载器加载的
        ClassLoader classLoader1 = String.class.getClassLoader();
        System.out.println(classLoader1);//null获取不到间接证明了String 类使用引导类加载器进行加载的

    }
}

虚拟机自带的加载器

启动类加载器(引导类加载器,BootStrap ClassLoader)

  • 这个类加载使用C/C++语言实现的,嵌套在JVM内部。
  • 它用来加载java的核心库,用于提供JVM自身需要的类。
  • 它并不继承自java.lang.ClassLoader,没有父加载器。
  • 加载拓展类和应用程序类加载器,并指定为他们的父加载器,即ClassLoader。
  • 出于安全考虑,BootStrap启动类加载器只加载包名为java、javax、sun等开头的类

扩展类加载器(Extension ClassLoader)

  • java语言编写 ,由sun.misc.Launcher$ExtClassLoader实现。
  • 派生于ClassLoader类。
  • 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会由扩展类加载器自动加载。

应用程序类加载器(系统类加载器,AppClassLoader)

  • java语言编写, 由sun.misc.Launcher$AppClassLoader实现。
  • 派生于ClassLoader类。
  • 它负责加载环境变量classpath或系统属性 java.class.path指定路径下的类库。
  • 该类加载器是程序中默认的类加载器,一般来说,java应用的类都是由它来完成加载。
  • 通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器。

代码示例:

/**
 * 虚拟机自带加载器
 */
public class ClassLoaderTest1 {
    public static void main(String[] args) {
        System.out.println("********启动类加载器*********");
        URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
        //获取BootStrapClassLoader能够加载的api路径
        for (URL e:urls){
            System.out.println(e.toExternalForm());
        }

        //从上面的路径中随意选择一个类 看看他的类加载器是什么
        //Provider位于 /jdk1.8.0_171.jdk/Contents/Home/jre/lib/jsse.jar 下,引导类加载器加载它
        ClassLoader classLoader = Provider.class.getClassLoader();
        System.out.println(classLoader);//null

        System.out.println("********拓展类加载器********");
        String extDirs = System.getProperty("java.ext.dirs");
        for (String path : extDirs.split(";")){
            System.out.println(path);
        }
        //从上面的路径中随意选择一个类 看看他的类加载器是什么:拓展类加载器
        ClassLoader classLoader1 = CurveDB.class.getClassLoader();
        System.out.println(classLoader1);//sun.misc.Launcher$ExtClassLoader@4dc63996
    }
}

启动类加载器BootStrapClassLoader能够加载的api路径有:

java.time.、java.util.、java.nio.、java.lang.、java.text.、java.sql.、java.math.*等等都在rt.jar包下。

为什么要使用用户自定义类加载器

  • 隔离加载类
  • 修改类加载的方式
  • 拓展加载源
  • 防止源码泄漏

 

ClassLoader的常用方法及获取方法

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

获取ClassLoader的途径:

双亲委派机制

Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成的class对象。而且加载某个类的class文件时,java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式。

双亲委派机制工作原理

如图,虽然我们自定义了一个java.lang包下的String尝试覆盖核心类库中的String,但是由于双亲委派机制,启动加载器会加载java核心类库的String类(BootStrap启动类加载器只加载包名为java、javax、sun等开头的类),而核心类库中的String并没有main方法:

双亲委派机制的优势

避免类的重复加载

保护程序安全,防止核心API被随意修改

启动类加载器可以抢在标准扩展类装载器之前去装载类,而标准扩展类装载器可以抢在类路径加载器之前去装载那个类,类路径装载器又可以抢在自定义类加载器之前去加载它。所以Java虚拟机先从最可信的Java核心API查找类型,这是为了防止不可靠的类扮演被信任的类。试想一 下,网络上有个名叫 java.lang.Integer 的类,它是某个黑客为了想混进 java.lang 包所起的名字,实际上里面含有恶意代码,但是这种 伎俩在双亲模式加载体系结构下是行不通的,因为网络类加载器在加载它的时候,它首先调用双亲类加载器,这样一直向上委托,直到启动类加载器,而启动类加载器在核心Java API里发现了这个名字的类,所以它就直接加载Java核心API的java.lang.Integer类,然后将这个类返回,所以自始自终网络上的 java.lang.Integer 的类是不会被加载的。

保证核心API包的访问权限

但是如果这个代码不是去试图替换一个被信任的类(就是前面说的那种情况),而是想在一个被信任的包中插入一个全新的类型,情况会怎样呢?

比如一个名为 java.lang.Virus的类,经过双亲委托模式,最终类装载器试图从网络上下载这个类,因为网络类装载器的双亲们都没有这个类(当然没有了,因为是病毒嘛)。假设成功下载了这个类,那你肯定会想,Virus 和 lang下的其他类同在 java.lang 包下,暗示这个类是Java API的一部分,那么是不是也拥有修改java.lang包中数据的权限呢?答案当然不是,因为要取得访问和修改java.lang包中的权限,java.lang.Virus和java.lang下其他类必须是属于同一个运行时包的,什么是运行时包?运行时包是指由同一个类装载器装载的、属于同一个包的、多个类型的集合。考虑一下,java.lang.Virus和java.lang其他类是同一个类装载器装载的吗?不是的!java.lang.Virus是由网络类装载器装载的!

自定义类:java.lang.MeDSH(java.lang包需要访问权限,阻止我们用包名自定义类)

双亲委派机制在SPI中的应用

某个应用程序由双亲委派机制找到引导类加载器,首先调用rt.jar包中的SPI核心,但由于SPI核心当中有各种各样的接口需要被实现(这里指具体的服务提供商),这里我们以JDBC.jar为例,jdbc.jar可以为我们提供具体的实现。

那么这时我们需要反向委托,找到线程上下文类加载器去加载jdbc.jar。

线程上下文类加载器属于系统类加载器。

为什么说SPI打破双亲委派机制?

以JDBC加载驱动为例:在JDBC4.0之后支持SPI方式加载java.sql.Driver的实现类。SPI实现方式为,通过ServiceLoader.load(Driver.class)方法,去各自实现Driver接口的lib的META-INF/services/java.sql.Driver文件里找到实现类的名字,通过Thread.currentThread().getContextClassLoader()类加载器加载实现类并返回实例。

驱动加载的过程大致是在什么地方打破了双亲委派模型呢?

先看下如果不用Thread.currentThread().getContextClassLoader()加载器加载,整个流程会怎么样。

  • 从META-INF/services/java.sql.Driver文件得到实现类名字DriverA
  • Class.forName(“xx.xx.DriverA”)来加载实现类。Class.forName()方法默认使用当前类的ClassLoader,JDBC是在DriverManager类里调用Driver的,当前类也就是DriverManager,它的加载器是BootstrapClassLoader。
  • 用BootstrapClassLoader去加载非rt.jar包里的类xx.xx.DriverA,就会找不到。要加载xx.xx.DriverA需要用到AppClassLoader或其他自定义ClassLoader。

最终矛盾出现在,要在BootstrapClassLoader加载的类里,调用AppClassLoader去加载实现类。

这样就出现了一个问题:如何在父加载器加载的类中,去调用子加载器去加载类?

jdk提供了两种方式,Thread.currentThread().getContextClassLoader()和ClassLoader.getSystemClassLoader()一般都指向AppClassLoader,他们能加载classpath中的类。

SPI则用Thread.currentThread().getContextClassLoader()来加载实现类,实现在核心包里的基础类调用用户代码。 

tomcat如何打破双亲委派机制

通常,一个tomcat要加载几个应用程序呢? 当然是n多个应用程序, 加上我们使用的都是spring的框架, 那我们能保证所有的应用程序都是用spring4 或者spring5 么? 不可能, 它可能既有spring4的项目, 又有spring5的项目. 那么tomcat在加载spring4项目的war包是, 会不会和spring5项目的war包冲突呢? 因为spring4, 和 spring5中有很多类的类名是一样的, 但是实现不一样。如果都是交给父类加载器加载,那么肯定只能加载一份,也就是spring4和spring5的项目不能共存。而实际上的情况呢? 我们的tomcat可以加在各种各样类型的war包, 相互之间没有影响. 他是怎么做到的呢? 

因为tomcat打破了双亲委派机制, 下面我们就来看看tomcat是如何打破双亲委派机制的?

如上图, 上面的橙色部门还是和原来一样, 采用双亲委派机制;而黄色部分是tomcat第一部分自定义的类加载器, 这部分主要是加载tomcat包中的类, 这一部分依然采用的是双亲委派机制;而绿色部分是tomcat第二部分自定义类加载器,正是这一部分, 打破了类的双亲委派机制。

1、tomcat第一部分自定义类加载器(黄色部分)

这部分类加载器, 在tomcat7及以前是tomcat自定义的三个类加载器, 分别在不同文件加载的jar包,而到了tomcat8及以后,tomcat将这三个文件夹合并了, 合并成了一个lib包. 也就是我们现在看到的lib包。

我们来看看这三个类加载器的主要功能:

  • commonClassLoader:tomcat最基本的类加载器, 加载路径中的class可以被tomcat容器本身和各个webapp访问;
  • catalinaClassLoader:tomcat容器中私有的类加载器, 加载路径中的class对于webapp不可见;
  • sharedClassLoader:各个webapps共享的类加载器, 加载路径中的class对于所有的webapp都可见, 但是对于tomcat容器不可见。

这一部分类加载器, 依然采用的是双亲委派机制, 原因是, 它只有一份。如果有重复, 那么也是以这一份为准。

2、tomcat第二部分自定义类加载器(绿色部分)

绿色部分是java项目在打war包的时候, tomcat自动生成的类加载器, 也就是说 , 每一个项目打成一个war包, tomcat都会自动生成一个类加载器, 专门用来加载这个war包。而这个类加载器打破了双亲委派机制。我们可以想象一下, 加入这个webapp类加载器没有打破双亲委派机制会怎么样?

如果没有打破, 它就会委托父类加载器去加载, 一旦加载到了,  子类加载器就没有机会在加载了. 那么, spring4和spring5的项目想共存, 那是不可能的了。

所以, 这一部分它打破了双亲委派机制。

这样一来, webapp类加载器不需要再让上级去加载, 它自己就可以加载对应war里的class文件。当然了, 其他的项目文件, 还是要委托上级加载的。

3、自定义tomcat的war包类加载器

现在我有两个war包, 分处于不同的文件夹, tomcat如何使用各自的类加载器加载自己包下的class类呢?

我们来举个例子, 比如: 在我的home目录下有两个文件夹, tomcat-test和tomcat-test1. 用这两个文件夹来模拟两个项目。

在它们的下面都有一个com/lxl/jvm/User1.class,虽然类名和类路径都是一样的,但是他们的内容是不同的。这个时候,如果tomcat要同时加载这两个目录下的User1.class文件, 我们如何操作呢?

其实非常简单, 按照上面的思路, tomcat只需要为每一个文件夹生成一个新的类加载器就可以了。

public static void main(String[] args) throws Exception {     
    // 第一个类加载器
    DefinedClassLoaderTest classLoader = new DefinedClassLoaderTest("/Users/luoxiaoli/tomcat-test");
    Class<?> clazz = classLoader.loadClass("com.lxl.jvm.User1");
    Object obj = clazz.newInstance();
    Method sout = clazz.getDeclaredMethod("sout", null);
    sout.invoke(obj, null);
    System.out.println(clazz.getClassLoader().getClass().getName());


  // 第二个类加载器
    DefinedClassLoaderTest classLoader1 = new DefinedClassLoaderTest("/Users/luoxiaoli/tomcat-test1");
    Class<?> clazz1 = classLoader1.loadClass("com.lxl.jvm.User1");
    Object obj1 = clazz1.newInstance();
    Method sout1 = clazz1.getDeclaredMethod("sout", null);
    sout1.invoke(obj1, null);
    System.out.println(clazz1.getClassLoader().getClass().getName());
}

我们看到上面tomcat自定义的类加载器中, 还有一个jsp类加载器. jsp是可以实现热部署的, 那么他是如何实现的呢?

我们都知道jsp其实是一个servlet容器, 有tomcat加载. tomcat会为每一个jsp生成一个类加载器。这样每个类加载器都加载自己的jsp, 不会加载别人的。当jsp文件内容修改时, tomcat会有一个监听程序来监听jsp的改动。比如文件夹的修改时间, 一旦时间变了, 就重新加载文件夹中的内容。

具体tomcat是怎么实现的呢? tomcat自定义了一个thread, 用来监听不同文件夹中文件的内容是否修改, 如何监听呢? 就看文件夹的update time有没有变化, 如果有变化了, 那么就会重新加载。

JVM中表示两个class对象是否为同一个类

在jvm中表示两个class对象是否为同一个类存在的两个必要条件:

  • 类的完整类名必须一致,包括包名;
  • 即使类的完整类名一致,同时要求加载这个类的ClassLoader(指ClassLoader实例对象)必须相同;

换句话说,在jvm中,即使这两个类对象(class对象)来源同一个Class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的。

对类加载器的引用,JVM必须知道一个类型是由启动类加载器加载的还是由用户类加载器加载的。如果一个类型由用户类加载器加载的,那么jvm会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用的时候,JVM需要保证两个类型的加载器是相同的。

类的主动使用和被动使用

java程序对类的使用方式分为:主动使用和被动使用,即是否调用了clinit()方法。

主动使用在类加载系统中的第三阶段initialization即初始化阶段调用了clinit()方法,而被动使用不会去调用。

主动使用,分为七种情况:

  • 创建类的实例
  • 访问某各类或接口的静态变量,或者对静态变量赋值
  • 调用类的静态方法
  • 反射 比如Class.forName()
  • 初始化一个类的子类
  • java虚拟机启动时被标明为启动类的类
  • JDK 7 开始提供的动态语言支持:java.lang.invoke.MethodHandle实例的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic句柄对应的类没有初始化,则初始化。

除了以上七种情况,其他使用java类的方式都被看作是对类的被动使用,都不会导致类的初始化

 

posted @ 2021-11-24 22:16  残城碎梦  阅读(77)  评论(0编辑  收藏  举报