JVM学习——类加载器

1.什么是类加载器

一个类如果要运行,需要被加载到虚拟机内存中才能够运行。那么由谁来将其加载到虚拟机内存(也就是我们虚拟机的方法区中)中呢?就是我们所说的类加载器

2.类的加载过程

编译

该阶段通过java编译器将我们编写的.java文件编译成计算机可以识别的二进制形式的.class文件(机器码字节文件)

加载

通过全限定类名,将字节码文件读取到内存中,存放到方法区中

连接

这一步我们还不算完成完整的类加载,连接是极其重要的一步
连接分为三个部分

1)验证

确保文件字节流中包含的信息符合虚拟机要求

2)准备

为类的静态变量分配内存,并初始化默认值

3)解析

将类中的符号引用转化为直接引用

初始化

若该类具有父类,将先加载父类字节码,并进行初始化。也就是进行静态变量赋值并执行静态代码块


3.类加载器

类加载器分为两种,一种是虚拟机自带的类加载器,另一种是自定义的类加载器。我们通常不会自定义类加载器,而是使用java自带的类加载器,但是在框架、服务器中,由于一些特殊的应用方式,会在java自定义类加载器的基础上再增加一些自定义类加载器

我们主要介绍虚拟机自带的类加载器

3.1 启动类加载器

负责加载由系统属性sun.boot.class.path指定目录下的核心类库,也就是我们%JAVA_HOME%\jre\lib目录。它只加载java、javax、sun开头的类

该加载器由c++编写

我们都知道Obejct就是java包下的一个类,所以我们可以尝试看看Obejct的类加载器是什么

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

//Output:
//null

执行上面的语句会发现返回结果为null,为什么是null,其实是因为我们的根类加载器的编写语言是C++,VM不能够也不允许程序员获取该类,所以返回值用null替代

3.2 拓展类加载器(ExtClassLoader)

负责加载%JAVA_HOME%\jre\lib\ext目录下的类库,或者java.ext.dirs指定的目录

该类由java编写

该类的父加载器是启动类加载器,这里的父子关系并非java中的继承关系

3.3 系统类加载器(应用类加载器)

负责从classpath下或java.class.path指定目录下加载类,是自定义类的默认加载器

该类的父加载器是应用类加载器

public class test {
    public static void main(String[] args) {
        System.out.println(test.class.getClassLoader());
    }
}

//Output:
//sun.misc.Launcher$AppClassLoader@dad5dc

上面例子中的test就是我们的自定义类,并放在我们的classpath路径下,我们可以看到这个类的类加载器返回的是AppClassLoader

3.4 自定义类加载器

为何要花时间实现自己的ClassLoader

  • 我们需要的类不一定存放在已经设置好的classPath下(有系统类加载器AppClassLoader加载的路径),对于自定义路径中的class类文件的加载,我们需要自己的ClassLoader
  • 有时我们不一定是从类文件中读取类,可能是从网络的输入流中读取类,这就需要做一些加密和解密操作,这就需要自己实现加载类的逻辑,当然其他的特殊处理也同样适用。
  • 可以实现模块隔离,每个模块使用一个类加载器进行加载,每个模块独立作为一个Jar包对外提供功能。

所有的自定义类最终都是继承自java.lang.ClassLoader

类加载器方法说明

我们前面说到类的加载过程,因此在类加载器方法中自然有对应的方法

loadClass()
  1. 该方法获取一个全限定类名,根据全限定类名查看该类是否以及被加载过
  2. 如果没有加载过,则调用父类加载器加载
  3. 如果父类加载器加载不到,则调用自己的findClass进行查找读取
  4. 如果还是没找到则抛出异常ClassNotFoundException

这里贴上该接口的源码

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 {
                    //2.判断是否有父加载器,有的话委托父加载器加载该类,没有的话交给根类加载器
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        //Ext的parent为null,因为Bootstrap是无法被程序被访问的,默认parent为null时其父加载器就是Bootstrap
                        // 此时直接用native方法调用启动类加载加载,若找不到则抛异常
                        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.
                    // 如果父加载器无法加载那么就在本类加载器的范围内进行查找
                    // findClass找到class文件后将调用defineClass方法把字节码导入方法区,同时缓存结果
                    long t1 = System.nanoTime();
                    c = findClass(name);

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

该方法是空实现,需要子类进行实现。

也就是通过某种方式读取到.class文件的内容,然后将类名,字节数组、偏移量、长度传给defineClass进行调用

defineClass()

将字节码流解析成JVM能够识别的Class对象,此处不推荐重写

resolveClass()

此处对应我们的连接部分

自定义类加载器实例

下面我们来自定义一个类加载器

import java.io.*;

public class MyClassLoader extends ClassLoader {

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte [] bytes = null;
        try {
            File file = new File("d://"+name+".class");
            InputStream in = new FileInputStream(file);
            bytes = new byte[(int) file.length()];
            in.read(bytes);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return defineClass(name,bytes,0,bytes.length);
    }
}

public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        System.out.println(new MyClassLoader().findClass("Person").newInstance());
    }
//Output:
//Person has been loaded
//Person has been created
//Person@84aee7

Person.java

public class Person {
    private String name;

    static {
        System.out.println("Person has been loaded");
    }

    public Person(){
        System.out.println("Person has been created");
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        //Class clazz = new MyClassLoader().findClass("Person");
        System.out.println(new MyClassLoader().findClass("Person").newInstance());
    }
}

上面这个例子中我们继承了ClassLoader并重写了findClass方法,在类加载过程中,加载Class文件的过程就对应我们类加载器中的findClass方法,所以我们如果希望从一个自定义目录下读取类文件,只需要重写我们的findClass并将读取到的Class文件流传给defineClass即可。
例子中我们传入了Person参数给我们的额自定义类加载器,我们的类加载器会去D盘根目录下查找对应类名的.class文件,并读取文件流给defineClass完成类加载,并在类加载完成后,通过加载出来的类对象进行反射创建实例。
可以看到,我们的Person中定义了一块静态代码区以及自定义的构造函数,会在类被加载以及创建时分别被调用,结果刚好对应我们的输出。


4.双亲委派机制

加载某个类时,JVM采用双亲委派模式,把加载类的请求先交给父加载器加载。注意,双亲委派模式中的父子关系并非面向对象中的继承关系,而是通过组合模式来复用类加载器代码。
在这里插入图片描述

优势:

避免了类的重复加载,当父加载器已经加载过,子加载器就不需要再次加载
安全,避免了被外来重复类覆盖篡改。


5.ContextClassLoader 线程上下文类加载器

ContextClassLoadery也是一个类加载器,但是很明显,我们在双亲委派机制中并没有提到这个类加载器。

5.1 ContextClassLoader的作用

Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。

这些 SPI 的接口由 Java 核心库来提供,而这些 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径(CLASSPATH)里。SPI接口中的代码经常需要加载具体的实现类。那么问题来了,SPI的接口是Java核心库的一部分,是由**启动类加载器(Bootstrap Classloader)来加载的;SPI的实现类是由系统类加载器(System ClassLoader)**来加载的。引导类加载器是无法找到 SPI 的实现类的,因为依照双亲委派模型,BootstrapClassloader无法委派AppClassLoader来加载类。(如果一个类中使用到了另一个未被加载的类,那么默认是使用当前类的类加载器去加载依赖类)

解释一下:java中存在一些服务接口,但是一般由第三方提供实现,而这些接口负责调用。那么问题来了,SPI接口是由根类加载器进行加载,这时候在SPI接口中加载这些类默认是通过SPI的类加载器也就是根类加载器进行类加载,但是第三方的类都存放在classpath下,通常由应用类加载器加载,根类加载器无法加载这些类。因为双亲委派机制无法反向委派,所以也就使得SPI接口加载的类无法由应用类加载器加载。这时我们可以使用线程上下文类加载器实现反向委托(该类加载器破坏了双亲委派机制。实现了类加载器的逆向委托)。

直白一点说就是,我(JDK)提供了一种帮你(第三方实现者)加载服务(如数据库驱动、日志库)的便捷方式,只要你遵循约定(把类名写在/META-INF里),那当我启动时我会去扫描所有jar包里符合约定的类名,再调用forName加载,但我的ClassLoader是没法加载的,就委托当前执行线程的ThreadContextClassLoader进行加载,后续你想怎么操作(驱动实现类的static代码块)就是你的事了。

事实上,虽然看似是委托线程上下文类加载器去加载目标类,但实际上,线程上下文类加载器中保存了应用类加载器的引用,所以最后它会把加载类加载器的任务交给应用类加载器

6.补充

  • 所有类在没有显式使用某个类加载器加载的情况下,默认由应用类加载器进行加载
  • 上层类加载器的类无法访问下层类加载器加载的类,而下层则可以访问上层加载的类
    原理:由于双亲委派机制,一个类会被读个类加载器进行loadClass,但最终会由一个defineClass来加载,JVM为每个类加载器维护了一张表,该表中记录了所有该类加载loadClass过的类,该表中的所有类都可以互相访问
  • 同一个类被不同的类加载器加载后会出现不同的Class对象(这种情况的出现一般是我们重写了loadClass跳过了双亲委派,然后直接findClass引起的)
  • 当类A引用了类B,则由类的类加载器启动类B进行加载
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

原来是肖某人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值