Java - 类加载器

本文详细介绍了Java类加载的过程,包括加载、验证、准备、解析和初始化。类加载器分为Bootstrap、ExtClassLoader和AppClassLoader,它们之间采用双亲委派机制。此外,还探讨了如何自定义类加载器,以及URLClassLoader的使用,展示了从本地和网络加载类的示例。
摘要由CSDN通过智能技术生成

1. 类加载的过程

在这里插入图片描述

加载:将字节码文件通过IO流读取到JVM的方法区,并同时在堆中生成Class对像。

验证:校验字节码文件的正确性。

准备:为类的静态变量分配内存,并初始化为默认值;对于final static修饰的变量,在编译时就已经分配好内存了。

解析:将类中的符号引用转换为直接引用。

初始化:对类的静态变量初始化为指定的值,执行静态代码。

2. 类加载器的分类

主要分为两类:

1. JVM内置的类加载器,Bootstrap加载器、ExtClassLoader加载器和AppClassLoader加载器 三种,分别负责加载不同目录下的.class文件

2. 用户自定义的类加载器,负责的加载目录自己决定。

2.1 引导类加载器 Bootstrap

引导类加载器属于JVM的一部分,由C++代码实现。

引导类加载器负责加载<JAVA_HOME\>\jre\lib路径下的核心类库,由于安全考虑只加载 包名 java、javax、sun开头的类。

package classloader.bootstrap;

import sun.misc.Launcher;

import java.net.URL;

public class Demo01 {
    public static void main(String[] args) {
        //Bootstrap 引导类加载器
        //打印为null,是因为Bootstrap是C++实现的。
        ClassLoader classLoader = Object.class.getClassLoader();
        System.out.println(classLoader);

        //查看引导类加载器会加载那些jar包
        URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
        for (URL urL : urLs) {
            System.out.println(urL);
        }
    }
}

2.2 扩展类加载器 ExtClassLoader

全类名:sum.misc.Launch$ExtClassLoader,Java语言实现。

扩展类加载器的父加载器是Bootstrap启动类加载器 (注:不是继承关系)

扩展类加载器负责加载<JAVA_HOME>\jre\lib\ext目录下的类库。

加载的jar包
在这里插入图片描述
获取扩展类加载器
在这里插入图片描述
注: JDK9是jdk.internal.loader.ClassLoaders$PlatformClassLoader

2.3 系统类加载器 AppClassLoader

全类名: sun.misc.Launcher$AppClassLoader

系统类加载器的父加载器是ExtClassLoader扩展类加载器(注: 不是继承关系)。

系统类加载器负责加载 classpath环境变量所指定的类库,是用户自定义类的默认类加载器。

获取系统类加载器
在这里插入图片描述
注: JDK9是jdk.internal.loader.ClassLoaders$AppClassLoader

2.4 三者之间的关系

AppClassLoader的父加载器是ExtClassLoader
ExtClassLoader的父加载器是Bootstrap
Bootstrap是根加载器

三者之间是没有继承关系的。
AppClassLoaderExtClassLoader都实现了抽象类ClassLoader。

抽象类ClassLoader有一个字段parent, AppClassLoaderExtClassLoader通过设置该字段引用,指定父加载器。(是组合关系)

AppClassLoader 的parent指向 ExtClassLoader
ExtClassLoader 的parent指向 null,(null的原因是因为BootstrapC++实现的,通过代码中逻辑判断来转向Bootstrap)

// The parent class loader for delegation
// Note: VM hardcoded the offset of this field, thus all new fields
// must be added *after* it.
private final ClassLoader parent;

2.5 自定义类加载器

自定义类加载器是为了加载在jvm三个加载器负责的目录范围之外的类

package com;

import java.io.*;

/**
 * @Date: 2022/5/2 10:09
 * @author: ZHX
 * @Description: 自定义类加载器
 */
public class MyClassLoader extends ClassLoader {

    private String classPath;

    public MyClassLoader(String classPath) {
        this.classPath = classPath;
    }

    //parent: 指定父加载器, AppClassLoader/ExtClassLoader/Bootstrap
    public MyClassLoader(ClassLoader parent, String classPath) {
        super(parent);
        this.classPath = classPath;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        //要求返回的是你要加载的字节码文件的Class对象.

        //这里都是我们说了算的。
        //步骤:
        //1. 从本地或网络某处读一个输入流到内存中 .
        //2. 将流内容字节数组 封装成Class对象 (直接调ClassLoader的defineClass方法,JVM会帮我们按照.class文件格式创建好的。)

        //1.
        //处理得到完整路径
        String path = this.classPath + name.replace(".", File.separator) + ".class";

        //2.读取到内存
        try (FileInputStream fis = new FileInputStream(path); ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            byte[] buffer = new byte[1024];
            int len = 0;
            while ((len = fis.read(buffer)) != -1) {
                //用ByteArrayOutputStream暂存一下。
                baos.write(buffer, 0, len);
            }
            byte[] allByte = baos.toByteArray();
            //将字节数组生成Class对象
            return super.defineClass(name, allByte, 0, allByte.length);
        } catch (IOException e) {
            throw new ClassNotFoundException(name + "加载失败");
        }
    }

	//测试下
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        //使用自己的类加载器,加载D:\\ + com.ali.Hello
        MyClassLoader myClassLoader = new MyClassLoader("d:\\");  //
        //加载 全限定名类
        Class<?> clazz = myClassLoader.loadClass("com.ali.Hello");

        clazz.newInstance();

        System.out.println(clazz.getClassLoader()); //out: 使用的类加载器 MyClassLoader@481248
    }
}

加载jar包的写法:

从jar包加载类:String path = "jar:file:\\" + classPath + "!/" + name.replace(".", File.separator) + ".class";

2.6 注:谁来准备类加载器呢?

AppClassLoader和ExtClassLoader是Launcher的静态内部类,在程序启动时JVM会创建Launcher对象,Launcher构造器会同时会创建扩展类加载器和应用类加载器。

Launcher类
在AAAA述

3. 双亲委派机制

双亲委派机制就是: 每个类加载器都很懒,加载类时都先让父加载器去尝试加载,父加载器加载不了时自己才去加载。

在这里插入图片描述
图片来源 : https://www.bilibili.com/video/BV16T4y1P79h?p=2

例如: 加载自定义类Demo.class的流程

  1. 首先使用AppClassLoader类加载器尝试加载,AppClassLoader加载器会先检查它的缓存,查看该类是否已经被加载,有则不加载,没有则向上交给ExtClassLoader加载器。
  2. ExtClassLoader加载器同样会先检查它的缓存,查看该类是否已经被加载,有则不加载,没有则向上交给Bootstrap加载器。
  3. Bootstrap加载器同样会先检查它的缓存,查看该类是否已经被加载。有则不加载,没有则尝试从它负责的目录中加载,
  4. Bootstrap加载器加载失败(不在它负责的目录范围)则向下交给ExtClassLoader加载器。
  5. ExtClassLoader加载器会从它负责的目录中尝试加载,加载失败则向下交给AppClassLoader加载器
  6. AppClassLoader加载器从它负责的classpath尝试加载,加载完成。

双亲委派机制的好处:

  1. 避免类的重复加载:当父加载器已经加载该类时,就没有必要子加载器再加载一遍,保证被加载类的唯一性。

  2. 同时Java有沙箱安全机制:自定义类的包名以 java.开头被禁止, 防止核心API被篡改,判断逻辑在defineClass方法中。在这里插入图片描述

打破双亲委派机制

双亲委派机制的实现其实就是在loadClass方法中实现的。
直接调用findClass方法就可以跳过双亲委派机制,这样就可以直接加载,而不用向上委托了。
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        //使用上面自定义的类加载器。
        MyClassLoader myClassLoader1 = new MyClassLoader("d:\\");

        //find方法调用,加载 全限定名类
        Class<?> clazz1 = myClassLoader1.findClass("com.ali.Hello");
        
		System.out.println(clazz1.hashCode()); //out: 26508395

        System.out.println(clazz1.getClassLoader()); //out: 使用的类加载器 MyClassLoader@481248
}
//如果要想一个类加载两次,就需要创建两个类加载器。(因为判断缓存中该字节码文件是否已经已经被加载是在defineClass方法中,而该方法为final我们没法改写.)
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
    //使用上面自定义的类加载器。
    MyClassLoader myClassLoader1 = new MyClassLoader("d:\\");
    MyClassLoader myClassLoader2 = new MyClassLoader("d:\\");

    //加载 全限定名类
    Class<?> clazz1 = myClassLoader1.findClass("com.ali.Hello");
    Class<?> clazz2 = myClassLoader2.findClass("com.ali.Hello");

    System.out.println(clazz1.hashCode());//out: 22913620
    System.out.println(clazz2.hashCode());//out: 29768086

    System.out.println(clazz1.getClassLoader()); //out: 使用的类加载器 MyClassLoader@481248
    System.out.println(clazz2.getClassLoader()); //out: 使用的类加载器 MyClassLoader@1947c6b
}

4. ClassLoader抽象类

所有的类加载器(除了Bootstrap)都要继承ClassLoader抽象类。

主要方法

方法名作用
public Class<?> loadClass(String name)双亲委派机制的实现
protected Class<?> findClass(String name)读取字节码文件到内存并调用defindClass方法生成Class对象
protected final Class<?> defineClass(String name, byte[] b, int off, int len)先判断是否加载过,然后将字节数组解析成Class对象
protected final void resolveClass(Class<?> c)连接指定的类

loadClass()方法源码

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;
	}
}

5. URLClassLoader类

java.net.URLClassLoader继承了ClassLoader类. 拓展了功能,能够从网络或本地加载类。默认的父加载器是AppClassLoader系统类加载器。
在这里插入图片描述
加载磁盘上的类

package classloader.urlclassloader;

/**
 * @Date: 2022/4/30 9:35
 * @author: ZHX
 * @Description:
 */
public class LoadLocal {

    public LoadLocal(){
        System.out.println("本地的字节码文件");
    }

}
package classloader.urlclassloader;

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

/**
 * @Date: 2022/4/30 0:51
 * @author: ZHX
 * @Description:
 */
public class Demo {

    public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, InstantiationException, IllegalAccessException {

        File file = new File("D:\\Project\\java-advence\\day16-classloader\\src\\main\\java\\");
        URL url = file.toURI().toURL();

        URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url});

        Class<?> clazz = urlClassLoader.loadClass("classloader.urlclassloader.LoadLocal");
        clazz.newInstance(); //out: 本地的字节码文件
    }
}

加载网络上的类

package classloader.urlclassloader;

/**
 * @Date: 2022/4/30 9:35
 * @author: ZHX
 * @Description:
 */
public class LoadURL {

    public LoadURL(){
        System.out.println("网络上的字节码文件");
    }
}
package classloader.urlclassloader;

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

/**
 * @Date: 2022/4/30 0:51
 * @author: ZHX
 * @Description:
 */
public class Demo {

    public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, InstantiationException, IllegalAccessException {
    	//放到了tomcat服务器上。
        URL url = new URL("http://localhost/");

        URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url});

        Class<?> clazz = urlClassLoader.loadClass("classloader.urlclassloader.LoadURL");

        clazz.newInstance(); //out: 网络上的字节码文件
    }
}

6. 参考资料

阿里P7面试题,Java程序员手写自定义类加载器,讲透Java类加载全过程

面试官:谈谈类加载器吧,你有没有看过类加载器的源码?(总结的十分详细)

系统学习让你轻松定义java类加载器

AppClassLoader

类的加载与类加载器

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值