【JVM】02. 类加载(二):类加载器
在前两篇博文中,我们对JVM的整体架构进行了概览,并深入探讨了类加载器子系统中的类加载过程。本文将聚焦于类加载器本身,为后续介绍双亲委派机制做铺垫。
1 类加载器类型
类加载过程主要是由类加载器实现,在JVM中类加载器根据类的加载路径实现了以下4种类加载器:
- BootstrapClassLoader(启动类加载器): 最顶层的加载类,由 C++实现,主要用来加载 JDK 内部的核心类库( jre/lib/rt.jar)以及被 -Xbootclasspath参数指定的路径下的所有类。
- ExtensionClassLoader(扩展类加载器): 平台类加载器,负责加载扩展jar包,主要位于jre/lib/ext/.jar。
- AppClassLoader(应用程序类加载器):面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。如果应用中没有自定义的类加载器,一般情况下默认使用这个类加载器。
- 自定义加载器: 负责加载用户自定义路径下的类包。
不同类加载器之间通过parent属性形成父子关系,这种关系对于双亲委派机制至关重要。整体关系如下:
2 类加载器的初始化过程
类加载器的初始化过程是JVM启动和类加载过程的基础。
BootstrapClassLoader是在启动Java虚拟机时由底层C++代码创建。ExtClassLoader和AppClassLoader则是由JVM启动器实例sun.misc.Launcher类在构造时进行创建,Launcher类是由BootstrapClassLoader负责加载,以下是相关核心代码:
public class Launcher {
private static Launcher launcher = new Launcher();
private static String bootClassPath = System.getProperty("sun.boot.class.path");
private ClassLoader loader;
//Launcher的构造方法
public Launcher() {
Launcher.ExtClassLoader var1;
try {
//构造ExtClassLoader,在构造的过程中将其父加载器设置为null
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
//构造AppClassLoader,在构造的过程中将其父加载器设置为ExtClassLoader
//Launcher的loader属性值是AppClassLoader,我们一般就是用这个类加载器来加载我们自己写的应用程序
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
Thread.currentThread().setContextClassLoader(this.loader);
}
}
- sun.misc.Launcher初始化使用了饿汉的单例模式,保证一个JVM虚拟机内只有一个sun.misc.Launcher实例。
- 在Launcher构造方法内部,其创建了两个类加载器,分别是sun.misc.Launcher.ExtClassLoader(扩展类加载器)和sun.misc.Launcher.AppClassLoader(应用类加载器)。
- JVM默认使用Launcher的getClassLoader()方法返回的类加载器AppClassLoader的实例加载我们的应用程序。
3 自定义类加载器
自定义类加载器可以用于实现特定的类加载逻辑,如热部署、网络加载等。自定义类加载器的基本步骤如下:
- 继承
ClassLoader
类:创建一个新的类继承自java.lang.ClassLoader
。该类有两个核心方法,一个是loadClass方法,其中实现了双亲委派机制,另一个方法是findClass,默认是空方法。 - 重写
findClass
方法:这是自定义类加载器的核心,可用于实现类文件的自定义加载。 - 调用
defineClass
方法:调用父类的defineClass
方法,使用字节数据和类名创建Class
对象。
以下是一个简单的自定义类加载器示例:
public class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String fullClassName) throws ClassNotFoundException {
// 自定义类文件加载路径
String filePath = this.classPath + fullClassName.replace(".", "/").concat(".myclass");
int code;
try {
FileInputStream fis = new FileInputStream(filePath);
fis.read();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
while ((code = fis.read()) != -1) {
bos.write(code);
}
} catch (IOException e) {
e.printStackTrace();
}
byte[] data = bos.toByteArray();
bos.close();
// 将字节数组转为Class对象
return defineClass(fullClassName, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static void main(String[] args) throws Exception{
MyClassLoader myClassLoader = new MyClassLoader("/Users/JVMDemo/target/classes");
Class<?> clazz = myClassLoader.loadClass("com.classLoader.SecretUtil");
Object obj = clazz.newInstance();
Object result = clazz.getMethod("hello", null).invoke(obj);
System.out.println(result);
}
public class SecretUtil {
public String hello(){
return "Hello";
}
}
}
运行结果:
Hello
3.1 设置父ClassLoader
在实例化自定义类加载器前会先初始化继承的ClassLoader,即最终会先调用ClassLoader类的构造方法,将AppClassLoader设置为自定义类加载器的父加载器,意味着自定义ClassLoader调用的还是AppClassLoader的loadClass方法,遵循双亲委派的加载机制。
public abstract class ClassLoader {
// 持有父加载器的引用
private final ClassLoader parent;
protected ClassLoader() {
// 将默认的系统类加载器,作为父加载器传入,赋值parent
this(checkCreateClassLoader(), getSystemClassLoader());
}
private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
if (ParallelLoaders.isRegistered(this.getClass())) {
parallelLockMap = new ConcurrentHashMap<>();
package2certs = new ConcurrentHashMap<>();
assertionLock = new Object();
} else {
parallelLockMap = null;
package2certs = new Hashtable<>();
assertionLock = this;
}
}
public static ClassLoader getSystemClassLoader() {
// 初始化系统的类加载器
initSystemClassLoader();
if (scl == null) {
return null;
}
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkClassLoaderPermission(scl, Reflection.getCallerClass());
}
return scl;
}
private static synchronized void initSystemClassLoader() {
if (!sclSet) {
if (scl != null)
throw new IllegalStateException("recursive invocation");
sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
if (l != null) {
Throwable oops = null;
// 调用Launcher的getClassLoader方法,返回在类加载器初始化时记录的AppClassLoader
scl = l.getClassLoader();
try {
scl = AccessController.doPrivileged(new SystemClassLoaderAction(scl));
} catch (PrivilegedActionException pae) {
oops = pae.getCause();
if (oops instanceof InvocationTargetException) {
oops = oops.getCause();
}
}
if (oops != null) {
if (oops instanceof Error) {
throw (Error) oops;
} else {
// wrap the exception
throw new Error(oops);
}
}
}
sclSet = true;
}
}
}
4 总结
本文详细介绍了JVM中的类加载器,包括类加载器的类型、初始化过程以及如何通过自定义类加载器来实现特定的类加载需求。通过理解类加载器的工作原理,我们可以更好地掌握JVM的类加载机制,为开发高效、稳定的Java应用程序打下坚实基础。
在接下来的博文中,我们将详细介绍双亲委派模型的工作原理,这是JVM类加载体系的核心设计之一。双亲委派模型确保了Java核心库的稳定性,并避免了类的重复加载,同时我们也将讨论如何通过自定义类加载器来打破这一模型,以及这样做的意义与影响。