类加载机制

168 阅读6分钟

类加载的流程

  • 加载:将.class文件读取到jvm内存中;
  • 验证:确保加载进来的字节流符合JVM规范;
  • 准备:为静态变量在方法区分配内存,并设置默认初始值。就是还没有给值,全部赋初始值null或者0,准确的是申请了清空的内存空间;
  • 解析:虚拟机将常量池内的符号引用替换为直接引用的过程,依据"="后面计算进行复制;
  • 初始化:根据程序中的赋值语句主动为类变量赋值。

分类

Java中的类加载器可以分为两种:

  • 系统类加载器
  • 自定义类加载器

系统类加载器

Bootstrap ClassLoader(启动类加载器)

主要负责/jre/lib目录下的jar -Xbootclasspath:可以改变其默认加载的目录

C++实现的,所以Java代码中是无法引用的

Extensions ClassLoader (扩展类加载器)

主要负责/jre/lib/ext目录下的jar

-Djava.ext.dir:可以改变其加载的目录

App ClassLoader(系统类加载器)

主要负责Classpath目录下的的所有jar和Class文件,程序中的默认类加载器。

-Djava.class.path:可以修改其加载目录

自定义类加载器

有时候我们需要加载一个类,但是这个类是目前的类加载体系无法访问到的,这时候就要使用自定义的类加载器来加载这个类。

为了可以从指定的目录下加载jar包或者class文件,我们可以用继承java.lang.ClassLoader类的方式来实现一个自己的类加载器。

在开发自己的类加载器时,最好 覆写findClass()方法, 不要覆写 loadClass() 方法。(findClass()会在loadClass()中被调用,loadClass()其实就是双亲委托机制的具体实现)

先要了解下ClassLoader:

image.png

  • ClassLoader是一个抽象类,定义了ClassLoader的主要功能。
  • SecureClassLoader:拓展了ClassLoader类加入了权限方面的功能,加强了ClassLoader的安全性。
  • URLClassLoader:提供用来通过URl路径从jar文件和文件夹中加载类和资源功能。
  • ExtClassLoader和AppClassLoader都继承自URLClassLoader,它们都是Launcher.class的内部类。
    • Launcher 是Java虚拟机的入口应用,ExtClassLoader和AppClassLoader都是在Launcher中进行初始化的。 Bootstrap ClassLoader不在这继承体系中,因为他不是java的因此,他也没法在java代码中引用。
class NetworkClassLoader extends ClassLoader { public Class findClass(String name) { //复写 } }

双亲委托机制

当前类加载器总是将加载交给父级去完成。

记得有2步骤: 向上检查,从下加载

当需要加载某个类的时候,先从当前类加载器先判断该加载器是否加载了该class。如果没有找到就去parent判断是否已经加载。

如果到头了都没有加载过,那么就从头开始让类加载器去他们负责的区域找到该类。

16f8413a95a8812f.png

为什么要双亲委派

  • 防止重复加载类
    • 如果已经加载过一次Class,就不需要再次加载,而是先从缓存中直接读取。(委托的流程)
  • 为了安全(避免核心类篡改)
    • 比如我复写了一个String类,我想直接替代jdk里面的(不是简单的说我写个包,包里面有个String类那种替代)。因为类加载机制,我们要加载util.String类的时候永远都会交给父级,所以我们写的那个类永远都不会加载到。
  • 再比如能不能自己写个类叫java.lang.System?一般情况下当然不能!除非使用自定义类加载器进行加载。

jvm如何认定两个对象同属于一个类型

类的全限定类名+类加载器来唯一标识一个类。 也就是说一个类加载器不能加载同一个类。但是不同的类加载器可以加载相同的类。

破坏双亲委派模型

因为双亲委派模型的缺陷:越基础的类由越上层的加载器进行加载(所谓基础类就是它们总是作为被用户代码调用的API)。

所以有个问题: 如果基础类又要调用回用户的代码,该怎么处理?

以JDBC举例:

DriverManager由BootStrap类进行加载的(处于jdk中,SPI提供的接口)。

但是该Driver具体的实现类是数据库服务商提供的,会由App加载器来加载。这时候我们加载这些驱动的时候,会委托到Bootstrap来加载驱动,但是BootStrap是无法找到这些驱动对应的实现类的。这时候就要破坏这个委托模型,需要启动类加载器来委托子类来加载Driver实现。

怎么解决?

为了解决这个问题,Java设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,他将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。

Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI、JDBC、JCE、JAXB和JBI等。

什么是SPI

SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。

说白了就是jdk提供的一套接口规范,然后服务方会有一套自己的实现机制。比如jdbc接口,各个数据库厂商针对接口开发各自的驱动来实现扩展。(类似于IOC)

使用场景

  • 数据库驱动加载接口实现类的加载,JDBC加载不同类型数据库的驱动
  • 日志门面接口实现类加载,SLF4J加载不同提供商的日志实现类
  • Spring中大量使用了SPI,比如:对servlet3.0规范对ServletContainerInitializer的实现、自动类型转换Type Conversion SPI(Converter SPI、Formatter SPI)等
  • Dubbo中也大量使用SPI的方式实现框架的扩展, 不过它对Java提供的原生SPI做了封装,允许用户扩展实现Filter接口

面试题(加深对类加载流程理解)

class Singleton{
    // 2段代码的区别就在于 这三行代码的顺序。构造函数是在前面调用的还是在后面调用的,当然value1和value2都是静态的
    private static Singleton singleton = new Singleton();
    public static int value1;
    public static int value2 = 0;
    //在类准备阶段value1,value2都初始化好了默认值为0。然后初始化阶段就是为了将=号的赋值来初始化之前的默认值。这时候是程序执行的
    //1.先执行构造:value1=1 value2=1
    //2.继续value2的赋值操作:value2=0
    //结果就是1,0
    private Singleton(){
        value1++;
        value2++;
    }
    public static Singleton getInstance(){
        return singleton;
    }
}
class Singleton2{
    public static int value1;
    public static int value2 = 0;
    private static Singleton2 singleton2 = new Singleton2();
    //先初始值都是0
    //执行赋值value2=0,还是0
    //执行构造value2=1.value1=1
    //所以最后结果是1,1
    private Singleton2(){
        value1++;
        value2++;
    }

    public static Singleton2 getInstance2(){
        return singleton2;
    }

}

public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();
        System.out.println("Singleton1 value1:" + singleton.value1); //1
        System.out.println("Singleton1 value2:" + singleton.value2); //0

        Singleton2 singleton2 = Singleton2.getInstance2();
        System.out.println("Singleton2 value1:" + singleton2.value1); //1
        System.out.println("Singleton2 value2:" + singleton2.value2); //1
}