类加载器-Java

本文详细阐述了Java类加载的过程,包括加载、连接(验证、准备、解析)、初始化,以及各类加载器(根、扩展、系统和自定义)的作用。重点讲解了双亲委派原理、URLClassLoader的应用,以及如何自定义文件和网络类加载器。此外,还讨论了显式与隐式加载的区别和线程上下文类加载器在SPI中的角色。
摘要由CSDN通过智能技术生成

视频地址: https://www.bilibili.com/video/BV1vJ41177cw
代码地址: https://gitee.com/dingwen-gitee/java8-study.git

类的加载就是把class文件加载到内存中。

1.加载过程

在这里插入图片描述

  • Java文件
  • Java c 编译生成class文件(字节码文件)
  • 加载
  • 连接
    • 验证
    • 准备
    • 解析
  • 初始化

在Java中当程序需要某个类时,虚拟机会保证这个类已经被加载到内存中。已经完成加载、连接(验证、准备、解析)、初始化。整个过程必须按照顺序严格执行。

2.类的加载

类加载器通过类的全限定类名(包名+类名)查找字节码文件,将字节码文件中的二进制数据读到内存中,存放在运行时数据区的方法区内。然后利用字节码文件创建Class对象,用来封装类在方法区内的数据结构并存放在堆内存中。

3.连接

3.1 验证

确保加载类的正确性。class文件的字节流包含的信息符合当前虚拟机的要求,不会危害虚拟机自身的安全。

3.2 准备

  • 为类的静态变量分配内存并设置默认值
  • 注意:对于static final修饰的变量在编译时就分配好内存了

3.3解析

把类中的符号引用转为直接引用。

4.初始化

类加载的最后阶段,如有父类则先对父类进行初始化,在执行静态变量赋值,静态代码块执行,成员变量初始化。

5.类加载器

类的加载是由类加载器完成的。

5.1 分类

  • Java虚拟机类加载器

    • 根类加载器
    • 扩展类加载器
    • 系统类加载器
  • 用户自定义加载器

    java.lang.ClassLoader的子类实例。

5.2 根类加载器(Bootrap)

根类加载器是对底层的类加载器,是虚拟机的一部分,由c++实现,没有父加载器。主要负责加载系统属性sun.boot.class.path指定路径下的核心类库(即:<JAVA_HOME>\jre\lib)。处于安全考虑,根类加载器只加载java、javax、sun开头的类。

package classloader;

/**
 * 类加载器 demo1
 *
 * @author dingwen
 * @date 2021/08/28
 */
public class ClassLoaderDemo1 {
    public static void main(String[] args) {
        ClassLoader classLoader = Object.class.getClassLoader();
        // null
        System.out.println("classLoader = " + classLoader);
    }
}

5.2 扩展类加载器(Extension)

扩展类加载器是原sun公司实现的sun.misc.Launcher$ExtClassLoader类(JDK9jdk.internal.loader.ClassLoaders$PlatformClassLoader类)。由Java编写,父加载器是根类加载器。负责加载<JAVA_HOME>\jre\lib\ext下的类库或者系统变量java.ext.dirs指定目录下的类库。

在这里插入图片描述

在这里插入图片描述

package classloader;

import sun.net.spi.nameservice.dns.DNSNameService;

/**
 * 扩展类加载器示例
 *
 * @author dingwen
 * @date 2021/08/28
 */
public class ClassLoaderDemo2 {
    public static void main(String[] args) {
        // dnsns.jar 位于jre/lib/ext目录下
        ClassLoader classLoader = DNSNameService.class.getClassLoader();
        //classLoader = sun.misc.Launcher$ExtClassLoader@14ae5a5
        System.out.println("classLoader = " + classLoader);
    }
}

5.3 系统类加载器(System)

系统类加载器也称为应用加载器。也是纯java类,是原SUN公司实现的 sun.misc.Launcher A p p C l a s s L o a d e r 类 ( J D K 9 是 j d k . i n t e r n a l . l o a d e r . C l a s s L o a d e r s AppClassLoader 类(JDK9是 jdk.internal.loader.ClassLoaders AppClassLoaderJDK9jdk.internal.loader.ClassLoadersAppClassLoader)。它的父加载器是扩展类加载器。它负责从classpath环境变量或者系统属性java.class.path所指定的目录中加载类,它是用户自定义的类加载器的默认父加载器。

package classloader;

/**
 * 类加载器 demo3
 *
 * @author dingwen
 * @date 2021/08/29
 */
public class ClassLoaderDemo3 {
    public static void main(String[] args) {
        ClassLoader classLoader = ClassLoaderDemo3.class.getClassLoader();

        // classLoader = sun.misc.Launcher$AppClassLoader@18b4aac2
        System.out.println("classLoader = " + classLoader);

    }
}

5.4 总结

在程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,同时我们还可以自定义类加载器,需要朱注意的是,java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存中生成class对象,而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把加载类的请求交由父加载器处理,它是一种任务委派模式。

6.双亲委派

在这里插入图片描述

需要calssLoader加载一个类时,该classLoader先委托自己的父加载器先去加载这个类,若父加载器能够加载,则由父加载器加载,否则才用classLoader自己加载的这个类。即每个类加载器都很懒,加载类时都先让父加载器去尝试加载,一直到根类加载器,加载不到时自己才去加载。真正加载类的加载器我们叫做启动类加载器。注意,双亲委派机制的父子关系并非面向对象程序设计中的继承关系,而是通过使用组合模式来复父加载器代码,实际代码是通过一个parent属性指定的。

package classloader;

/**
 * 类加载器 demo3
 *
 * @author dingwen
 * @date 2021/08/29
 */
public class ClassLoaderDemo3 {
    public static void main(String[] args) {
        ClassLoader classLoader = ClassLoaderDemo3.class.getClassLoader();

        // classLoader = sun.misc.Launcher$AppClassLoader@18b4aac2
        System.out.println("classLoader = " + classLoader);

        
        //classLoader = sun.misc.Launcher$AppClassLoader@18b4aac2
        //classLoader = sun.misc.Launcher$ExtClassLoader@4554617c
        // 
        while (classLoader != null){
            System.out.println("classLoader = " + classLoader);
            classLoader = classLoader.getParent();
        }

    }
}

6.1 实用双亲委派的好处

  • 避免类的重复加载
  • 安全,防止核心API被篡改

7.ClassLoader

除了根类加载器,所有的类加载器都必须继承java.lang.ClassLoader,是一个抽象类。

7.1 loadClass(String name, boolean resolve)

双亲委派实现,当父类加载器找不到类时,会调用findClass()方法进行查找。

// 双亲委派源码实现

 protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        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();
                    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;
        }
    }

7.2 findClass(String name)

当父类加载器找到不类时调用该方法。在自定义类加载器时,通常会覆盖这个方法,ClassLoader中给了一个默认错误的实现。

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

7.3 defineClass(String name, byte[] b, int off, int len,ProtectionDomain protectionDomain)

将 byte 字节解析成虚拟机能够识别的Class对象。defineClass() 方法通常与findClass()方法一起使用。在自定义类加载器时,会直接覆盖ClassLoader的findClass() 方法获取要加载类的字节码,然后调用defineClass() 方法生成Class对象。

7.4 resolveClass(Class<?> c)

指定连接的类。

8. URLClassLoader

在 java.net 包中,JDK提供了一个更加易用的类加载器URLClassLoader,它扩展了 ClassLoader,能够从本地或者网络上指定的位置加载类,我们可以使用该类作为自定义的类加载器使用。

8.1 构造方法

  • public URLClassLoader(URL[] urls):指定要加载的类所在的URL地址,父类加载器默认为系统类加载器
  • public URLClassLoader(URL[] urls, ClassLoader parent):指定要加载的类所在的URL地址,并指定父类加载器

8.2 案例

8.2.1 加载磁盘上的类

类准备

package classloader;

/**
 * URLClassLoader 程序演示
 * 加载本地磁盘的Java类
 * @author dingwen
 * @date 2021/08/29
 */
public class URLClassLoaderDemo {
    public URLClassLoaderDemo() {
        System.out.println("URLClassLoaderDemo new instance");
    }
}

javac URLClassLoaderDemo.java

// 编译

package classloader;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;

/**
 * 类加载器 demo4
 * 加载本地磁盘class
 * @author dingwen
 * @date 2021/08/29
 */
public class ClassLoaderDemo4 {
    public static void main(String[] args) throws Exception {
        File file = new File("/Users/dingwen/Documents/study/java8-study/src/classloader/");
        URI uri = file.toURI();
        URL url = uri.toURL();
        URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url});
        System.out.println("classLoader.getParent() = " + urlClassLoader.getParent());
        Class clazz = urlClassLoader.loadClass("classloader.URLClassLoaderDemo");
        clazz.newInstance();

        //classLoader.getParent() = sun.misc.Launcher$AppClassLoader@18b4aac2
        //URLClassLoaderDemo new instance


    }
}
8.2.3 加载网络的类
package classloader;

import java.net.URL;
import java.net.URLClassLoader;

/**
 * 类加载器 demo5
 * 加载网络的类
 *
 * @author dingwen
 * @date 2021/08/29
 */
public class ClassLoaderDemo5 {
    public static void main(String[] args) throws Exception{
        URL url = new URL("http://localhost:8080/examples/");
        URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url});
        System.out.println("urlClassLoader.getParent() = " + urlClassLoader.getParent());
        Class clazz = urlClassLoader.loadClass("classloader.URLClassLoaderDemo");
        clazz.newInstance();
        
        // urlClassLoader.getParent() = sun.misc.Launcher$AppClassLoader@18b4aac2
        //URLClassLoaderDemo new instance
    }
}

9.自定义类加载器

  • 继承ClassLoader
  • 重写findClass

9.1 自定义文件类加载器

package classloader;

import java.io.*;

/**
 * 我的自定义文件类加载器
 *
 * @author dingwen
 * @date 2021/08/29
 */
public class MyFileClassLoader extends ClassLoader {
    /**
     * 被加载类所在目录
     */
    private String directory;

    public MyFileClassLoader(ClassLoader parent, String directory) {
        super(parent);
        this.directory = directory;
    }

    public MyFileClassLoader(String directory) {
        this.directory = directory;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            // 把类名转为目录
            String file = directory + File.separator + name.replace(".", File.separator) + ".class";
            // 构建输入流
            FileInputStream fileInputStream = new FileInputStream(file);
            // 构建字节输出流
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            byte[] buf = new byte[1024];
            int len = -1;
            while ((len = fileInputStream.read(buf)) != -1) {
                byteArrayOutputStream.write(buf, 0, len);
            }
            //读取到到二进制数据
            byte[] data = byteArrayOutputStream.toByteArray();
            fileInputStream.close();
            byteArrayOutputStream.close();
            return defineClass(name, data, 0, data.length);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    public static void main(String[] args) throws Exception {
        MyFileClassLoader myFileClassLoader = new MyFileClassLoader("/Users/dingwen/Documents/study/java8-study/src/");
        Class<?> clazz = myFileClassLoader.findClass("classloader.URLClassLoaderDemo");
        clazz.newInstance();
      
      //URLClassLoaderDemo new instance
    }
}

9.2 自定义网络类加载器

package classloader;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;

/**
 * 自定义 网络类加载器
 *
 * @author dingwen
 * @date 2021/08/29
 */
public class MyURLClassloader extends ClassLoader {
    private String url;

    public MyURLClassloader(ClassLoader parent, String url) {
        super(parent);
        this.url = url;
    }

    public MyURLClassloader(String url) {
        this.url = url;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            // 把类名称转为目录
            String path = url + "/" + name.replace(".", "/") + ".class";
            URL url = new URL(path);
            // 构建输入流
            InputStream inputStream = url.openStream();
            // 构建输出流
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            byte[] buf = new byte[1024];
            int len = -1;
            while ((len = inputStream.read(buf)) != -1) {
                byteArrayOutputStream.write(buf, 0, len);
            }
            byte[] data = byteArrayOutputStream.toByteArray();

            inputStream.close();
            byteArrayOutputStream.close();
            return defineClass(name, data, 0, data.length);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    public static void main(String[] args) throws Exception {
        MyURLClassloader myURLClassloader = new MyURLClassloader("http://localhost:8080/examples");
        Class<?> clazz = myURLClassloader.findClass("classloader.URLClassLoaderDemo");
        clazz.newInstance();
        //URLClassLoaderDemo new instance
    }
}

9.3 热部署类加载器

当我们调用 loadClass 方法加载类时,会采用双亲委派模式,即如果类已经被加载,就从缓存中获取,不会重新加载。如果同一个 class 被同一个类加载器多次加载,则会报错。因此,我们要实现热部署让同一个class文件被不同的类加载器重复加载即可。但是不能调用 loadClass 方法,而应该调用 findClass 方法,避开双亲委托模式,从而实现同一个类被多次加载,实现热部署。

package classloader;

/**
 * 类加载器  热部署
 *
 * @author dingwen
 * @date 2021/08/29
 */
public class ClassLoaderDemo6 {
    public static void main(String[] args) throws Exception {
//        MyFileClassLoader myFileClassLoader1 = new MyFileClassLoader("/Users/dingwen/Documents/study/java8-study/src/");
//        MyFileClassLoader myFileClassLoader2 = new MyFileClassLoader(myFileClassLoader1,"/Users/dingwen/Documents/study/java8-study/src/");
//        Class<?> clazz1 = myFileClassLoader1.findClass("classloader.URLClassLoaderDemo");
//        Class<?> clazz2 = myFileClassLoader2.findClass("classloader.URLClassLoaderDemo");

        // hash code 不相等
//        System.out.println("clazz1.hashCode() = " + clazz1.hashCode());
//        System.out.println("clazz2.hashCode() = " + clazz2.hashCode());



        // 执行类双亲委派模式
        MyFileClassLoader myFileClassLoader1 = new MyFileClassLoader("/Users/dingwen/Documents/study/java8-study/src/");
        MyFileClassLoader myFileClassLoader2 = new MyFileClassLoader(myFileClassLoader1,"/Users/dingwen/Documents/study/java8-study/src/");
        Class<?> clazz1 = myFileClassLoader1.loadClass("classloader.URLClassLoaderDemo");
        Class<?> clazz2 = myFileClassLoader2.loadClass("classloader.URLClassLoaderDemo");

        // hash code 相等
        System.out.println("clazz1.hashCode() = " + clazz1.hashCode());
        System.out.println("clazz2.hashCode() = " + clazz2.hashCode());
    }
}

10.类的显式与隐式加载

类的加载方式是指虚拟机将class文件加载到内存的方式。显示加载是指在 java 代码通过调用 ClassLoader 加载 class 对象,比如 Class.forName(String name); this.getClass().getClassLoader().loadClass()加载类。隐式加载指不需要在 java 代码中明确调用加载的代码,而是通过虚拟机自动加载到内存中。比如在加载某个class 时,该class引用了另外一个类的对象,那么这个对象的字节码文件就会被虚拟机自动加载到内存中。

11. 线程上下文类加载器

在这里插入图片描述

在 java 中存在着很多的服务提供者接口 SPI,全称 Service Provider Interface,是Java 提供的一套用来被第三方实现或者扩展的API,这些接口一般由第三方提供实现,常用 SPI 有 JDBC、JNDI等。这些 SPI 的接口(比如JDBC中的java.sql.Driver)属于核心类库,一般存在rt.jat包中,由根类加载器加载。而第三方实现的代码一般作为依赖jar包存放在classpath路径下,由于SPI接口中的代码需要加载具体的第三方实现类并调用其相关方法,SPI的接口类是由根类加载器加载的,Bootstrap 类加载器无法直接加载位于classpath下的具体实现类。由于双亲委派模式的存在,Bootstrap类加载器也无法反向委托AppClassLoaser加载SPI的具体实现类。在这种情况下,java提供了上下文类加载器用于解决以上问题。

线程上下文类加载器可以通过 java.lang.Thread 的 getContextClassLoader() 来获取,或者通过 setContextClassLoader(ClassLoader cl)来设置线程的上下文类加载器。如果没有手动设置上下文类加载器,线程将继承其父线程的上下文类加载器,初始线程的上下文类加载器是系统类加载器(AppClassLoader),在线程运行的代码可以通过此类加载器来加载类或资源。

显现这种加载类的方式破坏了双亲委托模型,但它使得 java 类加载器变得更加灵活。

我们以 JDBC 的类为例做一下说明。在 JDBC中有一个类 java.sql.DRiverManager,它是 rt.jar中的类,用来注册实现了 java.sql.Driver 接口的驱动类,而java.sqlDriver的实现类一般都是位于数据库的驱动jar包中的。

java.sql.DriverManager的部分源码截图:

static {
	loadInitiaDrivers();
    println("JDBC DriverManager initialized");
}

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

dingwen_blog

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

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

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

打赏作者

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

抵扣说明:

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

余额充值