浅谈Java类加载:ClassLoader

本文围绕Java类加载器展开,介绍了JVM加载Java文件的步骤,阐述了类加载器的概念、结构,包括引导、扩展和系统类加载器。还讲解了双亲委托模型及自定义类加载器的实现方式,通过JDBC案例说明了上下文类加载器解决类逆向访问的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

关于java文件的编译,我们都知道这是通过虚拟机JVM将源代码装换为源指令,并且以.class为扩展名的类文件中,每个类文件都包含某个类或者接口的定义和代码实现。下面是JVM加载Test.class文件的步骤:(摘录自《java核心技术 卷二》)

1.虚拟机JVM有一个用于加载类文件的机制,例如,从磁盘上读取文件或者请求web上的文件;虚拟机使用该机制来加载Test类文件中的内容。

2.如果Test类拥有类型为另一个类的域,或者是拥有超类,那么这些类文件也会被加载。(加载某个类所依赖的所有类的过程称为类的解析。)

3.接着,虚拟机执行Test中的main方法(因为main方法是静态的,无需创建类的实例)。

4.如果main方法或者main调用的方法需要更多的类,那么接下来就会加载这些类。然而,类加载机制并非只使用单个的类加载器。每个Java程序至少拥有三个类加载器:引导类加载器扩展类加载器系统加载器(有时也称为应用类加载器),引导类加载器负责加载系统类(通常从JAR文件rt.jar中进行加载)。他是虚拟机不可分割的一部分,而且通常是使用C语言实现的。引导类加载器没有对应的ClassLoader对象,例如:String.class.getClassLoader()将返回null。

扩展类加载器用于从jre/lib/ext目录加载“标准的扩展”。可以将JAR文件放入该目录,这样即使没有任何类路径,扩展类加载器也可以找到其中的各个类。

系统类加载器用于加载应用类,他在CLASSPATH环境变量或者-classpath命令行选项设置的类路径中的目录或者是JAR/ZIP文件里查找这些类。

从上面的实现步骤中我们可以发现,类加载器是JVM不会可或缺的一部分,那么到底什么是类加载器呢?

Java类加载器(英语:Java Classloader)是Java运行时环境(Java Runtime Environment)的一部分,负责动态加载Java类Java虚拟机的内存空间中。类通常是按需加载,即第一次使用该类时才加载。由于有了类加载器,Java运行时系统不需要知道文件与文件系统。学习类加载器时,掌握Java的委派概念很重要。(来自维基百科的定义)

我们来看看类加载器的结构:

6416539-ae02e7468fc3f25f.png (471Ã448)

自定义的类加载器 通过继承ClassLoader实现,而扩展类加载器和系统类加载器都是通过java实现的,在Oracle的Java语言实现中,他们都是URLClassLoader的实例,并且他们都是通过sun.misc.Launcher进行初始化,而Launcher类则由根类加载器进行加载。

看下Launcher初始化代码:

public Launcher() {
        Launcher.ExtClassLoader var1;
        try {
            //初始化扩展类加载器,注意这里构造函数没有入参,即无法获取根类加载器
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }

        try {
            //初始化应用类加载器,注意这里的入参就是扩展类加载器
            this.loader = Launcher.CryptoClassLoader .getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }

        Thread.currentThread().setContextClassLoader(this.loader);

   
}

接下来我们再进一步了解一下双亲委托模型:

类加载器中存在一种父子关系,除了引导类加载器外,每个类都有一个父类加载器。根据规定,类加载器会为他的父类加载器一个机会,也就是,只有当父类加载器加载失败时,才会使用子类加载器进行加载。例如:当要求系统类加载器加载一个系统类(比如java.util.ArrayList)时,他会首先要求扩展类加载器进行加载,而扩展类加载器又会先要求引导类加载器进行加载;引导类加载器查找并加载rt.jar中的这个类,这样就无需其他类加载器做过多的搜索。那么问题来了,自定义类加载器的层级肯定是最低的,我们怎么去确保自定义的类加载器能够产生作用呢?

针对这种问题,我们使用两种方式进行解决:

  1. 想要实现自己的类加载器且不破坏双亲委派模型,只需要继承ClassLoader类并重写findClass方法。
  2. 想要实现自己的类加载器并且破坏双亲委派模型,则需要继承ClassLoader类并重写loadClass,findClass方法

接下来,我们引用来某篇文章中的一个问题及回答来了解一下什么是上下文类加载器:


鉴于双亲委派模型的设计,子类加载器都保留了父类加载器的引用,也就是说当由子类加载器加载的类需要访问由父类加载器加载的类时,毫无疑问是可以访问到的。但考虑一种场景,会不会有父类加载器加载的类需要访问子类加载器加载的类这种情况?如果有,怎么解决(父类加载器并没有子类加载器的引用)?
这就是我们要讨论的常常被人们忽略的上下文类加载器。
经典案例:
JDBC是Java制定的一套访问数据库的标准接口,它包含在Java基础类库中,也就是说它是由根类加载器加载的。与此同时,各个数据库厂商会各自实现这套接口来让Java工程师可以访问自己的数据库,而这部分实现类库是需要Java工程师在工程中作为一个第三方依赖引入使用的,也就是说这部分实现类库是由应用类加载器进行加载的。
先上一段Java获取Mysql连接的代码:

//加载驱动程序
Class.forName("com.mysql.jdbc.Driver");
//连接数据库
Connection conn = DriverManager.getConnection(url, user, password);

这里DriverManager类就属于Java基础类库,由根类加载器加载。我们可以通过它获取到数据库的连接,显然是它通过com.mysql.jdbc.Driver驱动成功连接到了数据库,上面也说了数据库驱动(作为第三方类库引入)是由应用类加载器(系统类加载器)加载的。这个场景就是典型的由父类加载器加载的类需要访问由子类加载器加载的类。
Java是怎么实现这种逆向访问的呢?直接看DriverManager类的源码:

//建立数据库连接各个不同参数的方法最终都会走到这里
private static Connection getConnection(
        String url, java.util.Properties info, Class<?> caller) throws SQLException {
        //获取调用者的类加载器
        ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
        synchronized(DriverManager.class) {
            //如果为null,则使用上下文类加载器
            //这里是重点,什么时候类加载器才会为null? 当然就是由根类加载器加载的类了
            if (callerCL == null) {
                callerCL = Thread.currentThread().getContextClassLoader();
            }
        }

        //...省略

        for(DriverInfo aDriver : registeredDrivers) {
            //使用上下文类加载器去加载驱动
            if(isDriverAllowed(aDriver.driver, callerCL)) {
                try {
                    //如果加载成功,则进行连接
                    Connection con = aDriver.driver.connect(url, info);
                    //...
                } catch (SQLException ex) {
                    if (reason == null) {
                        reason = ex;
                    }
                }
            } 
            //...
        }
    }

重点说明:
为什么上下文类加载器就可以加载到数据库驱动呢?回到上面一开始Launcher初始化类加载器的源码,我们发现原来所谓的上下文类加载器本质上就是应用类加载器,有没有豁然开朗的感觉?上下文类加载器只是为了解决类的逆向访问提出来的一个概念,并不是一个全新的类加载器,它本质上就是应用类加载器

 

作者:凌风郎少
链接:https://www.jianshu.com/p/a6ba4f152968
来源:简书


 

最后时本人自己实现的一个自定义类加载器CryptoClassLoader .java

package cn.jxufe.classloader;

import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

/**
 * 自定义的ClassLoader
 * @author 曾鹏
 *
 */
public class CryptoClassLoader extends ClassLoader{
	
	private int key;
	
	public CryptoClassLoader(int key) {
		this.key=key;
	}
	
	protected Class<?> findClass(String name) throws ClassNotFoundException{
		try {
			byte[] classBytes=null;
			classBytes=loadClassBytes(name);
			
			//将转换后的的caesat文件的字节数组放入文件 ${name}.class文件中
			try(FileOutputStream out=new FileOutputStream(name+".class")){
				out.write(classBytes);
			}
			
//			Class<?> cl=defineClass(name, classBytes,0,classBytes.length);
			
//			if(cl==null) throw new ClassNotFoundException(name);
			
			return null;
			
		} catch (Exception e) {
			throw new ClassNotFoundException(name);
		}
	}
	
	private byte[] loadClassBytes(String name) throws IOException {
		String cname=name.replace(".", "/")+".caesar";
		byte[] bytes = Files.readAllBytes(Paths.get(cname));
		for(int i=0;i<bytes.length;i++) {
			bytes[i]=(byte) (bytes[i]-key);  //将获得的caesar文件中每个字节减去一个key
		}
		
		return bytes;
	}

}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值