Java 类加载器 —— 从底层源码分析它帮我们做了什么?

目录

背景知识补充

加载概述    

一、类加载器基本概念

1.1 类加载器加载 Class 大致要经过如下8个步骤

1.2 JVM的类加载机制主要有如下3种

1.3 这里说明一下双亲委派机制

二、启动类加载器

三、扩展类加载器

四、双亲委派 - 源码分析1

五、双亲委派 - 源码分析2

扫描二维码关注公众号,回复: 11756619 查看本文章

六、线程上下文类加载器

七、自定义类加载器


背景知识补充

从 Java 类的生命周期而言,一个类包括如下几个阶段:

类加载阶段分类:加载链接(验证/准备/解析)、初始化(<cinit>()V方法 / 发生的时机),如下图所示:

参考链接: Java 类加载 —— 底层是如何实现的?

加载概述    

加载指的是将类的 class 文件读入到内存,并为之创建一个 java.lang.Class 对象,也就是说,当程序中使用任何类时,系统都会为之建立一个 java.lang.Class 对象。

类的加载由类加载器完成,类加载器通常由JVM提供,这些类加载器也是前面所有程序运行的基础,JVM 提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承 ClassLoader 基类来创建自己的自定义类加载器。


一、类加载器基本概念

类加载器是 Java 语言的一个创新,也是 Java 语言流行的重要原因之一。它使得 Java 类可以被动态加载到 Java 虚拟机中并执行。

顾名思义,类加载器(class loader)用来加载 Java 类到 Java 虚拟机中。一般来说,Java 虚拟机使用 Java 类的方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成 java.lang.Class 类的一个实例。每个这样的实例用来表示一个 Java 类。通过此实例的 newInstance() 方法就可以创建出该类的一个对象。实际的情况可能更加复杂,比如 Java 字节代码可能是通过工具动态生成的,也可能是通过网络下载的。

JDK 8 为例:各个类加载器,各司其责,同时又存在层级关系(比如应用程序加载器加载类时,会问一下它的上级拓展类加载器有没有加载过,如果没有加载,则会委托拓展类加载器继续向上级启动类加载器询问是否加载过类),如果上级都没有加载,则会加载了。

JDK 8 类加载器
英文名称 中文名称 加载哪些类 说明
Bootstrap ClassLoader 启动类加载器 JAVA_HOME/jre/lib 无法直接访问,C++编写,不能直接JAVA
Extension ClassLoader 扩展类加载器 JAVA_HOME/jre/lib/ext 上级为Bootstrap,显示为null
Application ClassLoader 应用程序类加载器 classpath 上级为Extension
自定义类加载 自定义类加载 自定义 上级为Application

1.1 类加载器加载 Class 大致要经过如下8个步骤

  1. 检测此 Class 是否载入过,即在缓冲区中是否有此 Class ,如果有直接进入第8步,否则进入第【2】步。
  2. 如果没有父类加载器,则要么 Parent 是启动类加载器,要么本身就是启动类加载器,则跳到第【4】步,如果父类加载器存在,则进入第【3】步。
  3. 请求使用父类加载器去载入目标类,如果载入成功则跳至第【8】步,否则接着执行第【5】步。
  4. 请求使用启动类加载器去载入目标类,如果载入成功则跳至第【8】步,否则跳至第【7】步。
  5. 当前类加载器尝试寻找 Class 文件,如果找到则执行第【6】步,如果找不到则执行第【7】步。
  6. 从文件中载入 Class ,成功后跳至第【8】步。
  7. 抛出 ClassNotFountException 异常。
  8. 返回对应的 java.lang.Class 对象。

1.2 JVM的类加载机制主要有如下3种

  • 缓存机制:
    • 缓存机制将会保证所有加载过的 Class 都会被缓存,当程序中需要使用某个 Class 时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该 Class 对象时,系统才会读取该类对应的二进制数据,并将其转换成 Class 对象,存入缓冲区中。这就是为什么修改了 Class 后,必须重新启动 JVM ,程序所做的修改才会生效的原因。
  • 双亲委派:
    • 所谓的双亲委派,则是先让父类加载器试图加载该 Class ,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。
  • 全盘负责:
    • 所谓全盘负责,就是当一个类加载器负责加载某个 Class 时,该 Class 所依赖和引用其他 Class 也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。

1.3 这里说明一下双亲委派机制

双亲委派机制,其工作原理:

如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己才想办法去完成。如下图:

双亲委派原理图

双亲委派机制的优势:避免重复加载 + 避免核心类篡改。

采用双亲委派模式的是好处是 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子 ClassLoader 再加载一次。其次是考虑到安全因素,java 核心 api 中定义类型不会被随意替换,假设通过网络传递一个名为 java.lang.Integer 的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心 Java API 发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的 Integer.class ,这样便可以防止核心API库被随意篡改。


二、启动类加载器

用 Bootstrap 类加载器加载类:代码示例

class F {
    static {
        System.out.println("bootstrap F init");
    }
}

调用 F类 执行 

// 用 Bootstrap 类加载器加载类
public class T01_ClassLoader_Bootstrap {
    public static void main(String[] args) throws ClassNotFoundException {
        // forName 可以完成类的加载,也可以类的链接、初始化操作
        Class<?> aClass = Class.forName("com.jvm.t10_class_loader.F");
        System.out.println(aClass.getClassLoader());
    }
}

 输出结果:null ,说明 F类 是由启动类加载器加载的

/Users/Li/IdeaProjects/JvmLearn> java -Xbootclasspath/a:.com.jvm.t10_class_loader.T01_ClassLoader_Bootstrap
bootstrap F init
null
  • -Xbootclasspath 表示设置 bootclasspath
  • 其中 /a:. 表示将当前目录追加至 boot classpath 之后
  • 可以用这个办法替换核心类(jvm开发经常做,普通开发不需要)
    • java -Xbootclasspath:<new bootclasspath>
    • java -Xbootclasspath/a:<追加路径>
    • java -Xbootclasspath/p:<追加路径>

三、扩展类加载器

测试一:当我们自己编写一个普通类时,由应用程序加载类加载

测试代码示例如下:

public class T02_G {
    static {
        // 1、打包:jar -cvf my.jar com\jvm\t10_class_loader\T02_G.class
        // 2、放入:jdk\jre\lib\ext\  下
        //System.out.println("ext G init");
        System.out.println("classpath G init");
    }
}
/**
 * 演示 扩展类加载器
 * 在 D:\Program Files\Java\jdk1.8.0_91 下有一个 my.jar
 * 里面也有一个 G 的类,观察到底是哪个类被加载了
 */
// 用扩展类加载器 Extension ClassLoader加载类
public class T02_ClassLoader_Extension {
    public static void main(String[] args) throws ClassNotFoundException {
        // forName 可以完成类的加载,也可以类的链接、初始化操作
        Class<?> aClass = Class.forName("com.jvm.t10_class_loader.T02_G");
        System.out.println(aClass.getClassLoader());
    }
}

运行结果:自己编写一个普通类时,由应用程序加载类加载


测试二:如果同名类也在拓展类加载器路径下,则会由扩展类加载器进行加载

于是,我们将 T02_G 代修改如下:

public class T02_G {
    static {
        // 1、打包:jar -cvf my.jar com\jvm\t10_class_loader\T02_G.class
        // 2、放入:jdk\jre\lib\ext\  下
        System.out.println("ext G init");
        //System.out.println("classpath G init");
    }
}

T02_G 编译打包: jar -cvf my.jar com\jvm\t10_class_loader\T02_G.class,放入jdk\jre\lib\ext\  下;再次运行 T02_ClassLoader_Extension 

测试结果:如果同名类也在拓展类加载器路径下,则会由扩展类加载器进行加载,即 T02_G 类是由扩展类加载器加载

结果说明:

双亲委派模式,当我们应用程序类加载器去加载类时得去问问它的上级类加载器是否已经加载,上级就找到了拓展类加载器,结果拓展类加载器就找到了同名的 G类文件,加载以后结果就是应用类加载器就没有机会加载了。优先级最高的是启动类加载器、其次是扩展类加载器,第三才是应用程序类加载器。


四、双亲委派 - 源码分析1

所谓的双亲委派:就是指调用类加载器的 loadClass 方法时,查找类的规则。

注意:这里的双亲,翻译为上级 似乎更为合适,因为它们并没有继承关系。业界一般叫双亲委派模式

类加载 ClassLoader 在 package java.lang 包下的 ClassLoader.java 文件中。核心代码在 395 行。具体如下:

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            // 1、检查类是否已经加载
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        // 2、有上级的话,委派上级 loadClass
                        c = parent.loadClass(name, false);
                    } else {
                        // 如果没有上级了(ExtClassLoader), 则委派 BootstrapClassLoader
                        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();
                    // 4、每一层找不到,调用 findClass 方法(每个类加载器自己扩展)来加载
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    // 5、记录耗时
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

五、双亲委派 - 源码分析2

双亲委派模式,避免类的重复加载(JVM  区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类)。

public class T03_H {
    static {
        System.out.println("classpath T03_H init");
    }
}
// 类加载器 —— 单步调试查看过程
public class T03_ClassLoader_StepDebug {
    public static void main(String[] args) throws ClassNotFoundException {
        // forName 可以完成类的加载,也可以类的链接、初始化操作
        Class<?> aClass = Class.forName("com.jvm.t10_class_loader.T03_H");
        System.out.println(aClass.getClassLoader());
    }
}

 单步调试如下:

六、线程上下文类加载器

线程上下文类加载器是比较特殊的加载器。我们在使用 JDBC 时,都需要加载 Driver 驱动,不知道你注意到没有,不写

Class.forName("com.mysql.jdbc.Driver")

也是可以让 com.mysql.jdbc.Driver 正确加载的,你知道是怎么做的吗?让我们追踪一下源码:

class DriverManager {
    // 注册驱动的集合
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers 
            = new CopyOnWriteArrayList<>();
    
    // 初始化驱动
    static {
        loadInitialDrivers();
        System.out.println("JDBC DriverManager initialized");
    }
}

先不看别的,看看 DriverManager 的类加载器:

System.out.println(DriverManager.class.getClassLoader());

打印 null,表示它的类加载器是 Bootstrap ClassLoader,会到 JAVA_HOME/jre/lib 下搜索类,但 JAVA_HOME/jre/lib/ 下显然没有 mysql-connect-java-5.1.47.jar 包,这样问题来了,在 DriverManager 的静态代码块中,怎么能正确加载 com.mysql.jdbc.Driver 呢?

继续看 loadInitialDrivers()  方法:JDK 在某些情况下需要打破双亲委派模式

    private static void loadInitialDriver() {
        String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                @Override
                public String run() {
                    return System.getProperty("jdbc.drivers");
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }

        // 1) 使用ServiceLoader 机制加载驱动,即 SPI
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            @Override
            public Void run() {
                ServiceLoader<Driver> loaderDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loaderDrivers.iterator();
                try {
                    while (driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch (Throwable t) {
                    // Do nothing
                }
                return null;
            }
        });

        System.out.println("DriverManager.initialize: jdbc.drivers = " + drivers);

        // 2) 使用 jdbc.drivers 定义的驱动名加载驱动
        if (drivers == null || drivers.equals("")) {
            return;
        }

        String[] driversList = drivers.split(":");
        System.out.println("number of Drivers:" + driversList.length);
        for (String aDriver : driversList) {
            try {
                System.out.println("DriverManager.Initialize: loading " + aDriver);
                // 这里的 ClassLoader.getSystemLoader() 就是应用程序类加载器
                // getSystemClassLoader 其实就是应用程序类加载器,打破了双亲委派模式。在某些情况下需要打破双亲委派模式
                Class.forName(aDriver, true, ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                System.out.println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }

再看如下,它就是大名鼎鼎 Service Provider Interface (SPI),主要是为了解耦

约定如下,在 jar 包的 META-INF/services 包下,以接口全限定名名为文件,文件内容是实现类名称。即根据接口找到文件,文件内容是要加载类的类名

这样就可以使用

ServiceLoader<接口类型> allImpls = ServiceLoader.load(接口类型.class);
Iterator<接口类型> iter = allImpls.iterator();
while (iter.hasNext()) {
    iter.next();
}

来得到实现类,体现的是【面向接口编程 + 解耦】的思想,在下面一些框架中都运用了此思想:

  • JDBC
  • Servlet 初始化器
  • Spring 容器
  • Dubbo (对 SPI 进行了扩展)

接着看 ServiceLoader.load 方法:

public static <S> ServiceLoader<S> load(Class<S> service) {
    // 获取线程上下文类加载器
    ClassLoader c1 = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, c1);
}

线程上下文类加载器是当前线程使用的类加载器,默认就是应用类加载器,它内部又是由 Class.forName 调用了线程上下文类加载器完成类加载,具体代码在 java.util.ServiceLoader 的内部类 LazyIterator 中:

        private S nextService() {
            if (!hasNextService())
                throw new NoSuchElementException();
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
                fail(service,
                     "Provider " + cn + " not found");
            }
            if (!service.isAssignableFrom(c)) {
                fail(service,
                     "Provider " + cn  + " not a subtype");
            }
            try {
                S p = service.cast(c.newInstance());
                providers.put(cn, p);
                return p;
            } catch (Throwable x) {
                fail(service,
                     "Provider " + cn + " could not be instantiated",
                     x);
            }
            throw new Error();          // This cannot happen
        }

七、自定义类加载器

问问自己,什么时候需要自定义类加载器

  1. 想加载非 classpath 随意路径的类文件
  2. 都是通过接口使用实现,希望解耦时,常用在框架设计
  3. 这些希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器

步骤:

  1. 继承 ClassLoader 父类
  2. 要遵从双亲委派机制,重写 findClass() 方法
    1. 注意:不是重写 loadClass() 方法,否则不会走双亲委派机制
  3. 读取类文件的字节码
  4. 调用父类的 defineClass() 方法来加载类
  5. 使用者调用该类加载器的 loadClass() 方法

示例:

准备两个类文件放入 D:\myclasspath,它实现了 java.util.Map 接口,可以先反编译看一下:

自定义类加载器实现代码:需要继承 ClassLoader 类

// 类加载器 —— 自定义类加载器
public class T05_ClassLoader_MyClassLoader extends ClassLoader {

    public static void main(String[] args) throws Exception {
        T05_ClassLoader_MyClassLoader classLoader = new T05_ClassLoader_MyClassLoader();
        Class<?> c1 = classLoader.loadClass("T05_ClassLoader_DefineV1");
        Class<?> c2 = classLoader.loadClass("T05_ClassLoader_DefineV1");
        System.out.println(c1 == c2); // true, 类文件只会加载一次,第一次加载放在自定义类加载器缓存中,第二次加载时发现缓存中已经有了

        // 如果使用不同的类加载器;确认唯一类,要包名、类名相同,同时类加载器也得相同,它会加载两次
        T05_ClassLoader_MyClassLoader classLoader2 = new T05_ClassLoader_MyClassLoader();
        Class<?> c3 = classLoader2.loadClass("T05_ClassLoader_DefineV1");
        System.out.println(c1 == c3); // false

        c1.newInstance();
    }


    @Override // name 就是类名称
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String path = "/Users/li/class_dir/" + name + ".class";

        try {
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            Files.copy(Paths.get(path), os);

            // 得到字节数组
            byte[] bytes = os.toByteArray();

            // byte[]  ->  *.class
            return defineClass(name, bytes, 0, bytes.length);
        } catch (IOException e) {
            e.printStackTrace();
            throw new ClassNotFoundException("类文件未找到", e);
        }
    }

}

文章最后,给大家推荐一些受欢迎的技术博客链接

  1. Hadoop相关技术博客链接
  2. Spark 核心技术链接
  3. JAVA相关的深度技术博客链接
  4. 超全干货--Flink思维导图,花了3周左右编写、校对
  5. 深入JAVA 的JVM核心原理解决线上各种故障【附案例】
  6. 请谈谈你对volatile的理解?--最近小李子与面试官的一场“硬核较量”
  7. 聊聊RPC通信,经常被问到的一道面试题。源码+笔记,包懂

欢迎扫描下方的二维码或 搜索 公众号“10点进修”,我们会有更多、且及时的资料推送给您,欢迎多多交流!

                                           

       

猜你喜欢

转载自blog.csdn.net/weixin_32265569/article/details/108092624
今日推荐