Java类加载器时Java虚拟机(JVM)的一部分,负责将类的字节码加载到内存中,并将其转换为可执行的Java对象。Java中每个类都是由特定的类加载器加载,并在运行时创建为一个Class对象。类加载器支持从文件系统、网络、内存等多个不同来源加载类的字节码,同时还负责分析类的依赖关系,加载所需的关联类。
Java虚拟机定义了三个主要类加载器:
备注:类具体加载范围说明不一定准确,在java启动时通过-cp,-mp等参数修改类加载器扫描的范围
类加载器工作主要分三个步骤:
flowchart LR
RF(读取字节码) --> DF(调用defineClass实现类定义/链接)
DF --> IT(进行类对象初始化)
类加载器使用双亲委派模型来加载类,但是在部分自定义类加载器中不一定遵循该模型,比如Tomcat实现的类加载器。
类加载器之间的父子关系不会以继承来实现,而是以组合(Composition)关系来复用父加载器的代码。加载时优先委派给父加载器进行加载,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去完成加载。JDK9之后在委派给父加载器前,先判断该类是否能够归属到某一个系统模块中,若可以找到归属关系,则优先委派给负责那个模块的类加载器完成加载。
比如自定义ClassLoader加载一个类的顺序从1逐层往上委派父加载器:
java类加载器机制现在随便问谁都能说出双亲委派机制,但是他真的是完全的双亲委派么?会不会在某些情况下不遵循双亲委派机制呢?还有在双亲委派机制中,由父加载器加载的class中如果依赖了只能由子类加载器加载的class会不会由问题呢?下面通过实例来记录踩坑记录。
/**
* TestClassLoader
*/
public class TestClassLoader {
public static void main(String[] args) {
// 获取应用程序类加载器
ClassLoader applicationLoader = ClassLoader.getSystemClassLoader();
System.out.println("应用程序类加载器:" + applicationLoader);
// 应用程序类加载器父加载器:Ext Class Loader
System.out.println("扩展类加载器:" + applicationLoader.getParent());
// 获取Bootstrap 类加载器,这里会返回 null
System.out.println("启动类加载器:" + applicationLoader.getParent().getParent());
// ClassLoader 是由BootstrapClassLoader加载,再次验证classLoader为空
System.out.println("启动类加载器:" + ClassLoader.class.getClassLoader());
}
}
// 输出结果
// 应用程序类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
// 扩展类加载器:sun.misc.Launcher$ExtClassLoader@3b22cdd0
// 启动类加载器:null
// 启动类加载器:null
java每个类加载器都有自己的空间,同时会包含父加载器的命名空间。父加载器加载的类对子加载器是可见的,但是子加载器加载的类对父加载器不可见。同时相互没有父子关系的类加载器之间加载的类也是不可见的。
import sun.misc.Unsafe;
import test.ClassJavaFileManager;
import test.MemoryClassLoader;
import test.StringJavaFileObject;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* TestClassLoader
*/
public class TestClassLoader {
private static final String animalCode = "public class Animal {\n" +
" public void sayHello() {\n" +
" System.out.println(\"animal sayHello: animal\");\n" +
" }\n" +
"}";
private static final String dogCode = "public class Dog {\n" +
" public void sayHello() throws Exception {\n" +
" System.out.println(\"dog sayHello: bingo dog\");\n" +
" // 这里使用Dog类加载器加载Animal,用于验证依赖加载\n" +
" Class<?> animalCls = Class.forName(\"Animal\");\n" +
" Object animalInstance = animalCls.newInstance();\n" +
" animalCls.getMethod(\"sayHello\").invoke(animalInstance);\n" +
" }\n" +
"}";
public static void main(String[] args) throws Exception {
// Dog使用自定义类加载器加载,父加载器为 "应用程序加载器"
//ClassLoader dogClassLoader = ClassLoader.getSystemClassLoader();
ClassLoader dogClassLoader = new MemoryClassLoader(Collections.EMPTY_MAP);
// Animal使用应用程序加载器加载
// ClassLoader animalClassLoader = new MemoryClassLoader(Collections.EMPTY_MAP);
ClassLoader animalClassLoader = ClassLoader.getSystemClassLoader();
// 将Animal加载到Application Class Loader中
loadClass("Animal", animalCode, animalClassLoader);
// 将Dog加载到Application Class Loader中
// loadClass("Dog", dogCode, animalClassLoader);
loadClass("Dog", dogCode, dogClassLoader);
// 通过自定义类加载器加载编译后的类文件
// Class<?> dogCls = Class.forName("Dog", true, animalClassLoader);
Class<?> dogCls = Class.forName("Dog", true, dogClassLoader);
// 通过反射创建类实例,需要无餐构造方法
Object instance = dogCls.newInstance();
// 通过反射回调hello方法
dogCls.getMethod("sayHello").invoke(instance);
}
public static void loadClass(String className, String code, ClassLoader classLoader) throws Exception {
// 获取当前平台编译器
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
// 获取java文件管理器,默认会将编译后的class保存到当前目录下
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
// 自定义class文件管理器,用于在内存中保存字节码信息
ClassJavaFileManager classJavaFileManager = new ClassJavaFileManager(fileManager);
// 创建需要编译的java文件对象
StringJavaFileObject strJavaFileObject = new StringJavaFileObject(className, code);
// 构建需要编译的java文件集合
Iterable<? extends JavaFileObject> fileObjects = Collections.singletonList(strJavaFileObject);
// 设置编译选项
List<String> options = Arrays.asList("-classpath", System.getProperty("java.class.path"));
// 创建编译任务
JavaCompiler.CompilationTask task = compiler.getTask(null, classJavaFileManager, null, options, null,
fileObjects);
// 执行编译任务
Boolean result = task.call();
// Check the result of compilation
if (result == null || !result) {
throw new Exception("Compilation failed");
}
// 通过反射获取Unsafe实例,Unsafe只能在通过bootstrapClassLoader加载的类中直接获取
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
// 获取编译后的字节码
byte[] classByte = classJavaFileManager.getClsJavaFileObject().getBytes();
// 将该类动态加入应用类加载器中
unsafe.defineClass(className, classByte, 0, classByte.length, classLoader, null);
}
}
// 输出:
// 1. Animal使用自定义的MemoryClassLoader加载,而Dog使用应用程序类加载器加载时运行会提示错误:
// dog sayHello: bingo dog
// ...
// Caused by: java.lang.ClassNotFoundException: Animal
// 2. Animal和Dog使用相同的MemoryClassLoader加载,运行时会正常输出:
// dog sayHello: bingo dog
// animal sayHello: animal Animal
// 3. Animal使用应用程序类加载器加载,Dog使用自定义的MemoryClassLoader加载,运行时会正常输出
// dog sayHello: bingo dog
// ...
// Caused by: java.lang.ClassNotFoundException: Animal
// 备注: Java的依赖关系通常只能由自己的类加载器或其父加载器来处理。
// 1. 借助javaAgent的Instrumentation
// 将jar包加入bootstrapClassLoader扫描范围,常用语加载外部jar
// void appendToBootstrapClassLoaderSearch(JarFile jarfile);
// 将jar包加入AppClassLoader扫描范围,常用于加载外部jar
// void appendToSystemClassLoaderSearch(JarFile jarfile);
// 2. 通过反射将jar加入到java.class.path下
import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
public class ClassUtil {
private static final Method addURL = initAddURLMethod();
private static final URLClassLoader system = (URLClassLoader) ClassLoader.getSystemClassLoader();
private static Method initAddURLMethod() {
try {
Method add = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
add.setAccessible(true);
return add;
} catch (Exception e) {
throw new RuntimeException("获取URLClassLoader.addURL方法失败");
}
}
public static void loadJarFile(File file) throws Exception {
addURL.invoke(system, file.toURI().toURL());
}
}
// 3. 在java启动时通过 -cp -mp参数指定java.class.path路径
// 3.1 指定目录
// java -cp /path/to/classes MyApplication
// 3.2 指定一个JAR文件作为类路径
// java -cp /path/to/myjar.jar MyApplication
// 3.3 指定多个目录和JAR文件作为类路径
// java -cp /path/to/myjar.jar:/path/to/ MyApplication
// 比如,使用ByteBuddy Adivce方式进行方法增强
import net.bytebuddy.asm.Advice;
import java.lang.reflect.Method;
import java.util.logging.Logger;
public class HutoolHttp5Advice {
// public static final Logger logger = Logger.getLogger("HutoolHttp5Advice");
private static final Logger logger = Logger.getLogger("HutoolHttp5Advice");
@Advice.OnMethodEnter
public static void enter(
@Advice.Origin Method method,
@Advice.This Object thiz,
@Advice.AllArguments Object[] args) throws Throwable {
// 这里在实际运行时会出现异常
logger.info("record log=====");
}
}
// 原因:
// 1. HutoolHttp5Advice这个类是由Application Class Loader加载并且时一个单独的Class对象。
// 2. 方法增强实际是通过修改原class字节码,把增强的代码和原方法合并后生成一个新方法并在class中进行覆写,最后对应的类加载器加载生成对应的Class对象。
// 3. 这时访问HutoolHttp5Advice.logger就类似跨包或类访问其他类的private成员变量,因此会出现异常。
// 修复方法:将logger 字段的访问修饰符调整为 public即可。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。