还不懂JVM类加载?一篇文章让你10分钟搞定!

Posted 守夜人爱吃兔子

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了还不懂JVM类加载?一篇文章让你10分钟搞定!相关的知识,希望对你有一定的参考价值。

类加载的过程是较为复杂的,今天来梳理下

加载-双亲委派机制

什么是双亲委派机制?

如上图,如果一个类收到类加载请求,它并不会自己先去加载类,而是把这个请求委托给父类加载器执行,如果父类加载器还有父类加载器,则会进一步向上委托,依次递归,直到请求到达启动类加载器,如果父类加载器能够完成加载任务,则成功返回,如果父类加载器无法完成加载任务,子类加载器会自己尝试去加载,这就是双亲委派机制

public class ClassLoaderTest {
​
    public static void main(String[] args) {
        // 获取系统类加载器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println(systemClassLoader);
​
        // 获取其上层:扩展类加载器
        ClassLoader extClassLoader = systemClassLoader.getParent();
        System.out.println(extClassLoader);
​
        // 获取其上层:获取不到启动类加载器
        ClassLoader bootstrapClassLoader = extClassLoader.getParent();
        System.out.println(bootstrapClassLoader);
​
        // 对于用户自定义类来说:默认使用系统类加载器进行加载
        ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
        System.out.println(classLoader);
​
        // String类使用启动类加载器进行加载的
        ClassLoader classLoader1 = String.class.getClassLoader();
        System.out.println(classLoader1);
​
        // DNSNameService使用扩展类加载器加载
        ClassLoader classLoader2 = DNSNameService.class.getClassLoader();
        System.out.println(classLoader2);
    }
}

如下,jdk的内置类加载器会默认加载一些jar包

双亲委派机制的好处是什么?

  1. 确保Java核心类库中的类不会被随意替换
  2. 避免类的重复加载

验证双亲委派机制

  1. 先自定义一个类加载器,并将系统类加载器作为该加载器的父类,重写findClass方法,至于为什么要重写findClass方法,可以去看ClassLoader类中的loadClass方法,该方法会使用父类加载器一直去加载类,如果父类加载器没有完成加载任务,就会调用findClass方法,重写的findClass方法会去指定的目录下加载类
  2. 手动将target目录中要加载的测试类移出该目录,因为系统类加载器会加载classpath下的类,如果被系统类加载器加载,根据双亲委派机制,我们自定义的类加载器自然不会加载测试类
public class MyClassLoader extends ClassLoader {
​
    private String classLoaderName;
​
    private String path;
​
    public void setPath(String path) {
        this.path = path;
    }
​
    public MyClassLoader(String classLoaderName, String path) {
        // 将系统类加载器当作该类的父加载器
        super();
        this.classLoaderName = classLoaderName;
        this.path = path;
    }
​
    public byte[] loadClassData(String name) {
​
        System.out.println("abc");
​
        InputStream is = null;
        byte[] data = null;
        ByteArrayOutputStream byteArrayOutputStream = null;
​
        try {
            this.classLoaderName = name.replace(".", "/");
            String fileExtension = ".class";
            is = new FileInputStream(path.concat(classLoaderName).concat(fileExtension));
            byteArrayOutputStream = new ByteArrayOutputStream();
​
            int ch = 0;
            while (-1 != (ch = is.read())) {
                byteArrayOutputStream.write(ch);
            }
​
            data = byteArrayOutputStream.toByteArray();
​
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                assert is != null;
                is.close();
                assert byteArrayOutputStream != null;
                byteArrayOutputStream.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
​
        return data;
    }
​
    @Override
    protected Class<?> findClass(String name) {
        byte[] data = loadClassData(name);
        return this.defineClass(name, data, 0, data.length);
    }
}

测试一下,我删除了target目录下的MyTest.class,然后将MyTest.class移动到/Users/zhangxiaobin/Desktop这个目录下,findClass时系统类加载器没有加载到MyTest.class,自定义类加载器就能够加载到这个类了

public static void main(String[] args) throws Exception {
    // 定义第一个类加载器
    MyClassLoader myClassLoader = new MyClassLoader("myClassLoader", "/Users/zhangxiaobin/Desktop/");
    Class<?> clazz = myClassLoader.loadClass("com.example.jvm.MyTest");
    Object object = clazz.newInstance();
    System.out.println(object);
    System.out.println(clazz.getClassLoader());
    System.out.println(clazz.getClassLoader().getParent());
    System.out.println(clazz.hashCode());
    // 定义第二个类加载器
    MyClassLoader myClassLoader2 = new MyClassLoader("myClassLoader", "/Users/zhangxiaobin/Desktop/");
    Class<?> clazz2 = myClassLoader2.loadClass("com.example.jvm.MyTest");
    Object object2 = clazz2.newInstance();
    System.out.println(object2);
    System.out.println(clazz.getClassLoader());
    System.out.println(clazz.getClassLoader().getParent());
    System.out.println(clazz2.hashCode());
}

测试一下,发现自定义了两个类加载器,同一个类被加载了两次,这是因为类加载器有一个命名空间的问题,每个类加载器都有自己的命名空间,在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类,在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类

将target目录下的MyTest.class弄回来,会发现该类是使用了系统类加载器来加载的

类加载器命名空间的一些问题

  1. 同一个命名空间内的类是相互可见的
  2. 子加载器的命名空间包含所有父加载器的命名空间,因此由子加载器加载的类能看到父加载器加载的类,父加载器加载的类不能看到子加载器加载的类
  3. 如果两个加载器之间没有直接或间接的父子关系,那么他们各自加载的类相互不可见

SPI打破双亲委派机制

在类加载器命名空间的限制下,双亲委派机制在某些场景下无法满足我们的需求,比如SPI机制

Java核心类库定义了接口,并未给出实现,这些接口的实现来自不同的jar包(厂商),比如JDBC,Java核心类库定义了Connection等接口,不同的厂商有不同的实现,mysql、Oracle等等,这些实现是通过jar包的方式加载的,jar包位于ClassPath下。Java核心类库是由启动类加载器加载的,ClassPath下的jar包是由系统类加载器加载的,按照命名空间的规则,他们是不可见的

办法总比问题多,jdk在双亲委派机制的基础上,新增了线程上下文类加载器,通过给当前线程设置线程上下文类加载器的方式来实现对于接口实现类的加载

这个线程上下文类加载器一般是系统类加载器

// 具体设置的代码在Launcher这个类中
public Launcher() {
    // Create the extension class loader
    ClassLoader extcl;
    try {
      // 获取扩展类加载器
      extcl = ExtClassLoader.getExtClassLoader();
    } catch (IOException e) {
      throw new InternalError(
        "Could not create extension class loader", e);
    }
​
    // Now create the class loader to use to launch the application
    try {
      // 获取系统类加载器
      loader = AppClassLoader.getAppClassLoader(extcl);
    } catch (IOException e) {
      throw new InternalError(
        "Could not create application class loader", e);
    }
​
    // 将线程上下文类加载器设置为系统类加载器,此加载器是可以替换的
    Thread.currentThread().setContextClassLoader(loader);
​
    ....
}

SPI机制是通过ServiceLoader这个类进行实现类的加载

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

示意图如下

链接

链接分为三步,分别是验证、准备、解析

验证:确保Class文件中包含的信息符合Java虚拟机的规范

准备:设置类变量的默认初始值,注意:是类变量,并且被final修饰的变量会显式初始化

解析:将符号引用转化为直接引用

初始化

此阶段是执行类的初始化器,进行类变量的初始化

public class MyTest2 {
​
    public static int a = 10;
​
    public static void main(String[] args) {
        System.out.println(MyTest2.a);
    }
​
}

如下图,使用jclasslib Bytecode Viewer可以看到的信息,如果整个类没有类变量,是不会出现,如果该类有父类,会执行父类的

最后

 一直想整理出一份完美的面试宝典,但是时间上一直腾不开,这套一千多道面试题宝典,结合今年金三银四各种大厂面试题,以及 GitHub 上 star 数超 30K+ 的文档整理出来的,我上传以后,毫无意外的短短半个小时点赞量就达到了 13k,说实话还是有点不可思议的。

一千道互联网 Java 工程师面试题

内容涵盖:Java、MyBatis、ZooKeeper、Dubbo、Elasticsearch、Memcached、Redis、MySQL、Spring、SpringBoot、SpringCloud、RabbitMQ、Kafka、Linux等技术栈(485页)

《Java核心知识点合集(283页)》

内容涵盖:Java基础、JVM、高并发、多线程、分布式、设计模式、Spring全家桶、Java、MyBatis、ZooKeeper、Dubbo、Elasticsearch、Memcached、MongoDB、Redis、MySQL、RabbitMQ、Kafka、Linux、Netty、Tomcat、数据库、云计算等

《Java中高级核心知识点合集(524页)》

《Java高级架构知识点整理》

 

 由于篇幅限制,详解资料太全面,细节内容太多,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!

需要的小伙伴,可以一键三连,点击这里获取免费领取方式

以上是关于还不懂JVM类加载?一篇文章让你10分钟搞定!的主要内容,如果未能解决你的问题,请参考以下文章

搞定这些jvm面试题,让你offer拿到手软!

别翻了,这篇文章绝对让你深刻理解java类的加载以及ClassLoader源码分析JVM篇二

最通俗易懂的JAVA虚拟机类加载过程详解,看完还不懂你考虑转行吧!

Java架构师必须知道的JVM类加载机制,这都不懂趁早放弃吧!

安利10个让你爽到爆的IDEA必备插件,你不懂还不学?

10分钟搞定让你困惑的 Jenkins 环境变量