转载

Java类加载知识总结

Java类加载器的作用就是在运行时加载类。它通过加载class文件、网络上的字节流或其他来源构造Class对象,用于生成对象在程序中运行。

在平时的程序开发中,我们一般不需要操作类加载,因为Java本身的类加载器机制已经帮我们做了很多事情。但后面很多时候,比如说自己开发框架或者排查问题的时候,我们需要理解类加载的机制和如何按自己的需求去自定义类加载器。类加载器知识就像Java开发的一道门,门内外隔离了开发人员对类加载的使用,而了解类加载器就掌握了这道门的钥匙。

什么是类加载器

类加载器是一个用来加载类文件的类。Java源代码通过编译器编译后,类加载器加载文件中的字节码来执行程序,字节码的来源也可以来自于网络。Java有3中默认的类加载器:Bootstrap类加载器、Extension类加载器和System类加载器(或者叫做Application类加载器)。每个类加载器都设定好从哪里加载类。

Bootstrap类加载器:JRE/lib/rt.jar中的JDK类文件

Bootstrap类加载器是所有类加载器的父类(非类继承关系)。它的大部分由C来写的,通过代码获取不到,比如调用String.class.getClassLoader(),会返回null。

Extension类加载器:JRE/lib/ext或者java.ext.dirs指向的目录

Extension加载器由 sun.misc.Launcher$ExtClassLoader 实现 System类加载器:CLASSPATH环境变量,由 -classpath-cp 选项定义,或者是JAR中的Manifest的classpath属性定义。 System类加载器由 sun.misc.Launcher$AppClassLoader 实现。

类加载器的工作原理

Java的类加载性保证了很好的稳定性和拓展性。类加载器的工作原理基于三个机制

委托机制

某加载器在尝试加载类的时候,都会委托其父类加载器尝试加载类。比如一个应用要加载CLASSPAH下A.class。加载这个类的请求由System类加载器委托父加载器Extension类加载器,Extension类加载器会委托父加载器Bootstrap类加载器。Bootstrap类加载器先从rt.jar中尝试加载这个类。这个类在rt.jar中不存在,所以加载工作回到Extension类加载器。Extension类加载器会查看jre/lib/ext目录下有没有这个类,如果存在那么这个类将被加载,且只加载一次。如果没找到,加载请求有回到了System类加载器。System类加载器从CLASPATH中查看该类,如果存在则加载这个类,如果没找到,则报java.lang.ClassNotFoundException。

可见性机制

子类加载器可以看到父类加载器加载的类,而反之则不行。

单一性机制

父加载器加载过的类不能被子加载器加载第二次,同一个类加载器实例也只能加载一个类一次。

如何加载类

我们可以使用显示调用的方式或者交由JVM隐式加载一个类。在类加载的过程中,一般有三个概念上的类加载器提供使用。

CurrentClassLoader,称之为当前类加载器

SpecificClassLoader, 称之为指定的类加载器。值得是一个特定的ClassLoader示例

ThreadContextClassLoader,称之为线程上下文类加载器。每个线程都会拥有一个ClassLoader引用,而且可以通过Thread.currentThread().setContextClassLoader(ClassLoader classLoader)进行切换

其中CurrentClassLoader的加载过程是JVM运行时候控制的,非显示调用。

显示加载

Class.forName和ClassLoader.loadClass都可以用来进行类加载。比如

Class.forName("com.xiaoyi.pandora.vo.A");
Class.forName("com.xiaoyi.pandora.vo.A", true, customClassLoader1);
customClassLoader1.loadClass("com.xiaoyi.pandora.vo.A");
复制代码

Class.forName的方式其实是使用了CurrentClassLoader这种方式,本质上还是找到一个类加载器去执行ClassLoader.loadClass动作。

@CallerSensitive
public static Class<?> forName(String className)
            throws ClassNotFoundException {
    Class<?> caller = Reflection.getCallerClass();
    return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
复制代码

从代码中可以看出,利用反射的机制获取执行方法的类示例caller,从而找到加载caller对应类的类加载器。从而将加载的动作交由这个类加载去执行。

类名,类加载器,类示例这三者的关系是紧密相连的。这里要提到一个数据结构来保存Java加载类过程中这三者的关系。SystemDictionary(系统字典)。

Java类加载知识总结

SystemDictionary以类名和类加载器实例作为一个key,Class对象引用为value。Class对象能过找到它的类加载器,类名和类加器实例对应一个唯一的Class对象。

所以,Class.forName的调用方式是先从SystemDictionary获取当前类加载器,然后以ClassLoader.loadClass的方式去加载一个类。

隐式加载

大多数情况下,程序中的类加载都是通过隐式加载的形式。不需要显示调用ClassLoader对象去加载类。 我们看下面一个很普通的场景。为了显示实际效果,这里自定义了一个类加载器去显示类的加载动作。

public class A {
    B b;
    public A() {
        this.b = new B();
    }
    public void show() {
        System.out.println("b's classLoader is " + b.getClass().getClassLoader());
    }
}
复制代码

在加载A.class后,如果实例化类A对象,JVM会自动帮我们利用类加载器帮我们加载B类。

String classPath = "/Users/xiaoyi/work";
        CustomClassLoader customClassLoader1 = new CustomClassLoader(classPath, "xiao");
Class aClazz = Class.forName("com.xiaoyi.pandora.vo.A", true, customClassLoader1);
Object a = aClazz.newInstance();
复制代码

控制台的输出如下

xiao classLoader start load class :com.xiaoyi.pandora.vo.A
xiao classLoader start load class :com.xiaoyi.pandora.vo.B
复制代码

在不执行Object a = aClazz.newInstance();这条语句的时候,控制台不会输出xiao classLoader start load class :com.xiaoyi.pandora.vo.B。

总结:在加载B类时,JVM会隐式获取加载A类的类加器去执行加载B类的工作。

类加载过程

java.lang.ClassLoader中Class<?> loadClass(String name, boolean resolve)方法介绍了详细的类加载过程。类加载中重要的四个方法,loadClass是下面1,2,3方法的入口

1、Class<?> findLoadedClass(String name)

判断类是否已经被加载过。即从SystemDictionary中根据类型和当前类加载器作为key,查看是否能找到对应的Class

2、Class<?> findClass(String name)

交给子类加载器的拓展

3、void resolveClass(Class<?> c)

将类名,当前类加载器,类示例对象关联起来,存储在SystemDictionary中。Java中规范在一个类被使用之前,必须做关联(Before the class can be used it must be resolved)

4、Class<?> defineClass(String name, byte[] b, int off, int len)

根据类文件或者网络上的字节流,转化为一个Class的实例。常用于自定义类加载器。

java.lang.ClassLoader中Class<?> loadClass(String name, boolean resolve)的代码如下(省略了非核心流程的代码)

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
       // 获取锁,类的加载过程是线程安全的
        synchronized (getClassLoadingLock(name)) {
           // 查看是否已经被加载过
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                       // 如果存在父加载器,委托父加载器去尝试加载
                        c = parent.loadClass(name, false);
                    } else {
                       // 委托Bootstrap类加载器去尝试加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                }

                if (c == null) {
                    // 类加载器本身去加载类
                    c = findClass(name);  
                }
            }
            if (resolve) {
                // 关联类和类加载器的关系
                resolveClass(c);
            }
            return c;
        }
    }
复制代码

一些类加载器的使用场景

自定义类加载器

自定义类加载器是使用类加载器一个基本场景。关键的方法在前面“类加载过程”已经给出。主要有2点

1、继承ClassLoader

2、重写findClass方法,实现获取类的字节流转化为类实例返回

一个简单的自定义类加载器如下

public class CustomClassLoader extends ClassLoader {
    private String name;
    private String classPath;

    public CustomClassLoader(String classPath, String name) {
        this.name = name;
        this.classPath = classPath;
    }

    @Override
    public Class<?> findClass(String name) throws ClassNotFoundException {
        System.out.println(this.name + " classLoader start load class :"  + name);
        try {
            byte[] classData = getData(name);
            if(classData != null) {
                return defineClass(name, classData, 0, classData.length);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return super.findClass(name);
    }

    private byte[] getData(String className) throws IOException {
        InputStream in = null;
        ByteArrayOutputStream out = null;
        String path = classPath + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
        try {
            in = new FileInputStream(path);
            out = new ByteArrayOutputStream();
            byte[] buffer = new byte[2048];
            int length = 0;

            while ((length = in.read(buffer)) != -1) {
                out.write(buffer, 0, length);
            }
            return out.toByteArray();
        } catch (Exception e) {

        } finally {
            if(in != null) {
                in.close();
            }
            if(out != null) {
                out.close();
            }

        }
        return null;
    }
}
复制代码

应用隔离

从SystemDictionary数据结构中,一个类名和类加载器实例,对应一个Class实例。也就是说一个类文件,可以交由不同的类加载器去生成不同的类实例,这些类实例是可以在JVM中并存的。但是如果在对类加载器的访问上做好隔离,这些类实例在JVM中是可以实现隔离的。具体可以参考Tomcat容器如何利用WebappClassLoader实现应用上的隔离的技术相关文档或者Pandora的隔离实现原理。

SPI

SPI全称是Service Provider Interface, 是JDK内置的一种服务提供发现机制。是一种动态替换发现机制。举个例子:有个接口想在运行时才发现具体的实现类,那么你只需在程序运行前添加一个实现即可。常见的SPI有JDBC、JCE、JNDI、JAXP等。

这些SPI的接口是由Java核心库来提供,而SPI的实现则是作为第二方jar包被包含进类路径(classpath)中。以JDBC的mysql为例子,调用代码如下所示

Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/mydb", "root", "root");
复制代码

com.mysql.jdbc.Driver中的代码如下,在类被加载后,向DriverManager中注册mysql的Driver。

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }

    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can/'t register driver!");
        }
    }
}
复制代码

下面来分析一下其中的类加载过程

1、隐式方式加载DriverManager类,因为DriverManager在JDK的rt.jar中,这是对应的类加载器是Bootstrap类加载器

2、DriverManager类加载后,执行静态代码块,初始化java.sql.Driver的实现类。按照SPI的规范,ServiceLoader会去/META-INF/services下面查找java.sql.Driver资源

Java类加载知识总结

3、ServiceLoader获取ThreadContextClassLoader,构造ServiceLoader实例,并将ThreadContextClassLoader赋给ServiceLoader实例。这时候的类加载器是System类加载器。

public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> load(Class<S> service,
                                            ClassLoader loader)
    {
        return new ServiceLoader<>(service, loader);
    }
复制代码

4、DriverManager利用ServiceLoader去获取/META-INF/services下的java.sql.Driver文件,加载对应的实现类,并初始化具体的Driver类。这时候的类加载器是第3步的System类加载器,它能够加载classpath下面的类。

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
    while(driversIterator.hasNext()) {
         driversIterator.next();
     }
} catch(Throwable t) {
     // Do nothing
}
复制代码
//driversIterator在调用next方法时,加载驱动类
Class<?> c = null;
try {
    c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
      fail(service, "Provider " + cn + " not found");
}
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
复制代码

5、Driver类加载实例化后,会执行静态代码将自己注册到DriverManager中

SPI的实现方式其实是破坏了类加载器的委托机制,在类加载的过程中,Bootstrap类加载在获取不到具体的SPI Provider实现类的情况下,委托ThreadContextLoader去加载classpath下的实现类。

Java类加载知识总结

原文  https://juejin.im/post/5c734547f265da2dae512872
正文到此结束
Loading...