前言
一切Java类都必须经过JVM加载才能运行,加载类的类就是类加载器(ClassLoader),它负责将class字节码转换成内存的class类。JVM 运行并不是一次性加载全部类,是按需加载,当用到未知类时才进行加载。
Java自带的三个类加载器
Java自带三个类加载器:
- BootstrapClassLoader:引导类加载器(根加载器)
- ExtensionClassLoader:扩展类加载器
- AppClassLoader:系统类加载器(默认)
类加载器的定义声明在%JAVA_HOME%/lib/rt.jar/sun/Launch.class ,它是JVM的入口。
BootstrapClassLoader
BootstrapClassLoader 负责加载 JVM 运行时核心类,它负责索引环境变量%JAVA_HOME%/lib 下的所有核心类jar包和class文件,最常见的就是rt.jar 。Bootstrap ClassLoader是由C/C++编写的,是JVM的一部分,并不是一个Java类。也被称为根加载器。
由于是C/C++定义的因此没有其Java类,在Launch.class定义了根加载器的索引路径sun.boot.class.path 获取并输出:
System.out.println(System.getProperty("sun.boot.class.path"));
ExtensionClassLoader
ExtensionClassLoader负责加载扩展类,索引环境变量%JAVA_HOME%/lib/ext 下的所有扩展类jar包和class文件,这些扩展包通常以javax 开头。为了方便简写为ExtClassLoader。
Launch.class 下面定义了它 内部通过java.ext.dirs 定义了索引路径 输出:
System.out.println(System.getProperty("java.ext.dirs"));
AppClassLoader
AppClassLoader负责索引环境变量%CLASSPATH% 下的 jar 包和class文件。我们自己通常都是由它来加载,此外如果不声明类加载器,它是默认的类加载器。
Launch.class 同样也定义了索引路径:
可以看到本项目的out目录
System.out.println(System.getProperty("java.class.path"));
类加载器顺序
从运行开始,类加载器按照以下顺序加载类
- BootstrapClassLoader
- ExtClassLoader
- AppClassLoader
按需加载
程序在运行中由于是按需加载,加载到未知类时,由调用它的类的类加载器加载。也就是说:在我们编写的Java文件public class下的新类都是由AppClassLoader加载。
获取类加载器
每个对象里面都有一个classLoader属性记录了当前的类是由谁来加载的。使用静态方法Class.class.getClassLoader() 可以获取某类的类加载器。
ClassLoader cl = Main.class.getClassLoader();
可以看到我们编写的Java确实是由AppClassLoader加载的
BootstrapClassLoader为空
打印String类的类加载器: 抛出空指针异常,这是由于String等基础类是由根加载器加载的,如果加载器为空则表示由根加载器加载。
父加载器不是加载器的父类
加载器的父类
在Launch.class看到:AppClassLoader和ExtClassLoader都继承自URLClassLoader Java定义了URLClassLoader可以加载远程和本地的类,由于Ext和App只需要加载本地的,因此它们都是URLClassLoader的子类。这里是代码实现上的父子关系
同样URLClassLoader继承SecureClassLoader,SecureClassLoader最终继承自ClassLoader,以下是类加载器的继承关系:
另外:我们可以使用URLClassLoader加载远程的jar来实现远程的类方法调用以验证漏洞。
父加载器
每个加载器都有一个父加载器,使用方法ClassLoader.getParent() 获取父加载器 可以发现:AppClassLoader的父加载器是ExtClassLoader,然后父加载器是Null(根加载器),以下是类加载器的功能关系: 在Launch.class中定义了App的父加载器是Ext
BootstrapClassLoader为空的原因
Ext的父加载器并没有显式的定义,Ext的声明也是隐式的: 然后调用父类URLClassLoader的构造方法,注意第二个传参是Null URLClassLoader又将这个空的parent继续super向上传递 然后一直到ClassLoader的构造方法,调用this.parent=parent ,因此是Ext的父加载器是Null
类加载流程
ClassLoader.class 规定了类加载的流程,入口在public Class<?> loadClass(String name)
- 首先检测该类是否被加载,如果被加载直接返回该类
- 如果未被加载,检测是否声明了父加载器,如果声明类父加载器则使用父加载器加载它
- 如果未被加载,也未声明父加载器,再次尝试用根加载器加载它
- 如果根加载器也无法加载,再使用该加载器自身加载
双亲委派
根据以上步骤,首先委托会依照App-Ext-Boot向上传递,逐级检查是否被加载;如果未被加载再依照Boot-Ext-App向下查找,直至找到或抛出错误。
以上方法被称为双亲委派,三个 ClassLoader之间形成了级联的父子关系,每个 ClassLoader都很懒,尽量把工作交给父加载器做,父加载器干不了了自己才会干,最后会递归到根加载器。
优点
- 避免类的重复加载, 确保一个类的全局唯一性
- 保护程序安全, 防止核心 API 被随意篡改
缺点
自定义类加载器
因为自带的类加载器只能从指定的环境变量加载,而非环境变量路径的加载就需要自定义类加载器来实现。
自定义类加载器的条件
编写自定义类加载器需要继承ClassLoader类,需要涉及以下两个重要的方法:
findClass() ,自定义加载器自己的加载方法,如果父加载器都无法加载时调用,需要重写,该方法的目的是获取class字节码defineClass() ,在findClass() 内调用,用于组装class对象
另外,自定义类加载器不要轻易覆盖loadClass() 方法,否则可能会导致自定义加载器无法加载核心类库。在使用自定义加载器时,要明确好它的父加载器是谁,将父加载器通过子类的构造器传入,如果缺省,父加载器是App。
自定义类加载器示例
HelloWorld.class
编写一个HelloWorld.java,编译为class文件等待导入
public class HelloWorld {
public void hello(){
System.out.println("Hello World!");
}
}
javac HelloWorld.class
然后将它移动到其他目录下,作为一个临时lib,这里我选择的是./resource
CustomizedHelloWorldClassLoader.java
写一个最简单的自定义类加载器
package Test;
import java.io.File;
import java.io.FileInputStream;
import java.io.ByteArrayOutputStream;
public class CustomizedHelloWorldClassLoader extends ClassLoader{
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String fileName = "HelloWorld.class";
String customizedPath = "./resource";
File file = new File(customizedPath, fileName);
try {
FileInputStream fis = new FileInputStream(file);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
int length;
try {
while ((length = fis.read()) != -1){
bos.write(length);
}
}catch (Exception e){
e.printStackTrace();
}
byte[] data = bos.toByteArray();
fis.close();
bos.close();
return defineClass(name, data, 0 ,data.length);
} catch (Exception e){
e.printStackTrace();
}
return super.findClass(name);
}
}
CustomizedClassLoaderTest.java
写一个测试的main方法
package Test;
import java.lang.reflect.Method;
public class CustomizedClassLoaderTest {
public static void main(String[] args) {
CustomizedHelloWorldClassLoader helloWorldClassLoader = new CustomizedHelloWorldClassLoader();
try {
Class cls = helloWorldClassLoader.loadClass("Test.HelloWorld");
if (cls != null){
try {
Object obj = cls.newInstance();
Method method = cls.getMethod("HelloWorld");
method.invoke(obj);
}catch (Exception e){
e.printStackTrace();
}
}
}catch (Exception e){
e.printStackTrace();
}
}
}
测试
运行CustomizedClassLoaderTest.java,成功调用
自定义类加载器的用途
- 调用自己编译的类绕过检测
- (弱)加解密Java字节码
类加载隔离——解决钻石依赖问题
不同的类加载器可以加载相同的类(非继承关系),同级跨类加载器调用方法时必须使用反射。也就是说被不同类加载器加载的名称一样的类实际上是不同的类。,那么如果要加载不同版本的class就可以使用两个加载器了。
利用上面的自定义类加载器,复制一份重命名为CustomizedHelloWorldClassLoaderMimic.java ,然后运行代码
package Test;
public class ClassLoaderIsolation {
public static void main(String[] args) throws Exception{
CustomizedHelloWorldClassLoader helloWorldClassLoader = new CustomizedHelloWorldClassLoader();
CustomizedHelloWorldClassLoaderMimic helloWorldClassLoaderMimic = new CustomizedHelloWorldClassLoaderMimic();
Class cls1 = helloWorldClassLoader.loadClass("HelloWorld");
Class cls2 = helloWorldClassLoaderMimic.loadClass("HelloWorld");
System.out.println(helloWorldClassLoader);
System.out.println(helloWorldClassLoaderMimic);
System.out.println(cls1==cls2);
}
}
运行结果显示:加载器不相等,加载的类也不相等
引用
完
欢迎在评论区留言,欢迎关注我的CSDN @Ho1aAs
|