类加载过程
类加载过程主要为 加载->连接->初始化。连接过程分为 验证->准备->解析。
加载是类加载过程的第一步,主要完成下面3件事情
- 通过全类名获取定义此类的二进制字节流
- 将字节流所代表的静态存储结构
(.class文件中静态存储的字节码格式,包含了类的元数据和具体的字节码指令)
转换为方法区的运行时数据结构 - 在内存中生成一个代表该类的Class对象,作为方法区这些数据的访问入口。
类加载器
类加载器介绍
类加载器是一个负责加载类的对象,ClassLoader是一个抽象类。给定类的二进制名称,类加载器尝试定位或生成构成类定义的数据。典型的策略是将名称转换为文件名,然后从文件系统中读取该名称的“类文件”。
每个Java类都有一个引用指向加载他的ClassLoader,不过,数组类不是通过ClassLoader创建的,而是JVM在需要的时候自动创建的,数组类通过getClassLoader()方法获取ClassLoader的时候和该数组的元素类型的ClassLoader是一致的。
总结:
- 类加载器是一个负责加载类的对象,用于实现类加载过程中的加载这一步。
- 每个Java类都有一个引用指向加载他的ClassLoader。
- 数组类不是通过ClassLoader创建的,是由JVM直接生成的。
class Class<T> {
...
private final ClassLoader classLoader;
@CallerSensitive
public ClassLoader getClassLoader() {
//...
}
...
}
简单来说,类加载器的主要作用就是加载Java类的字节码(.class文件)到JVM中(在内存中生成一个代表该类的Class对象)。
类加载器加载规则
JVM启动的时候,并不会一次性加载所有的类,而是根据需要去动态加载。也就是说,大部分类在具体用到的时候才会去加载,这样对内存更加友好。
对于已经加载的类会被放在ClassLoader中。在类加载的时候,系统会首先判断当前类是否被加载过,已经被加载的类会直接返回,否则才会尝试加载。也就是说,对于一个类加载器来说,相同二进制名称的类只会被加载一次。
类加载器总结
JVM中内置了三个重要的ClassLoader:
- BootStrapClassLoader(启动类加载器):最顶层的加载类,由C++实现,通常表示为null,并且没有父级。主要用来加载JDK内部的核心类库(%JAVA_HOME%/lib目录下的 rt.jar、resources.jar、charsets.jar等 jar 包和类)以及被 -Xbootclasspath参数指定的路径下的所有类。
- ExtensionClassLoader(扩展类加载器):主要负责加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类以及被 java.ext.dirs 系统变量所指定的路径下的所有类。
- AppClassLoader(应用程序类加载器):面向我们用户的加载器,负责加载当前应用classpath下的所有jar包和类。
除此之外,用户还可以加入自定义的类加载器来进行扩展,以满足自己的特殊需求,如:我们可以对Java类的字节码(.class文件)进行加密,加载时利用自定义类加载器对其解密。
除了BootstrapClassLoader是JVM自身的一部分之外,其他所有类加载器都是在JVM外部实现的,并且全都继承自ClassLoader抽象类。这样做的好处是用户可以自定义类加载器,以便让应用程序自己决定如何去获取所需要的类。
自定义类加载器
ClassLoader类有两个关键的方法:
- protected Class loadClass(String name, boolean resolve): 加载指定的二进制名称的类,实现了双亲委派机制。name为类的二进制名称,resolve如果为true,在加载时调用resolveClass(Class<?> c) 方法解析该类。
- protected Class findClass(String name):根据类的二进制名称来查找类,默认实现是空方法。
如果我们不想打破双亲委派模型,就重写ClassLoader类中的findClass()方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写loadClass()方法。
双亲委派模型
双亲委派模型是Java类加载机制的一种设计模式,用于解决类加载过程中类加载顺序和安全问题,他定义了类加载器间的协作关系,确保Java核心库的类优先被加载,同时避免类的重复加载。
基本概念
- 类加载器:JVM中负责加载类文件的组件,每个类加载器都有父类加载器(除了BootStrapClassLoader)
- 双亲委派
- 当一个类加载器加载类时,首先将加载请求委派给父类加载器,父类加载器继续向上委派,直到请求达到顶层的跟加载器。
- 如果父类加载器无法完成加载请求,则子类加载器才会尝试自己加载类。
工作流程
- 加载类请求:当一个类加载器接收到类加载请求时(如ClassLoader.loadClass(String name)),他不会立即尝试加载该类,而是首先将请求委派给父类加载器。
- 委派过程:父类加载器重复这个过程,继续请求向上委派,直到到达顶层的根加载器。
- 类加载:根加载器尝试加载类,如果成功,返回类的应用,失败,则抛出ClassNotFoundException。如果根加载器无法加载类,控制权返回给子类加载器,子类加载器再尝试自己加载类。
代码解析
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
//首先,检查该类是否已经加载过
Class c = findLoadedClass(name);
if (c == null) {
//如果 c 为 null,则说明该类没有被加载过
long t0 = System.nanoTime();
try {
if (parent != null) {
//当父类的加载器不为空,则通过父类的loadClass来加载该类
c = parent.loadClass(name, false);
} else {
//当父类的加载器为空,则调用启动类加载器来加载该类
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//非空父类的类加载器无法找到相应的类,则抛出异常
}
if (c == null) {
//当父类加载器无法加载时,则调用findClass方法来加载该类
//用户可通过覆写该方法,来自定义类加载器
long t1 = System.nanoTime();
c = findClass(name);
//用于统计类加载器相关的信息
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
//对类进行link操作
resolveClass(c);
}
return c;
}
}
每当一个类加载器接收到加载请求时,会先将请求转发给父类加载器,在父类加载器没有找到所请求的类的情况下,该类加载器才会尝试去加载。
结合上述分析,再梳理一次双亲委派流程
- 在类加载的时候,系统会首先判断当前类是否被加载过,已经被加载的类会直接返回,否则才会尝试加载(每个父类加载器都会走一遍这个流程)。
- 类加载器在进行类加载的时候,他首先不会自己去尝试加载这个类,而是把请求委派给父类加载器来完成(调用父类加载器loadClass()方法来加载类),这样的话,所有请求最终都会传送到顶层的启动类加载器。
- 只有当父类加载器返回自己无法完成这个加载请求,子加载器才会尝试自己去加载(调用自己的finaClass()方法来加载类)。
- 如果子类加载器也无法加载这个类,那么他会抛出一个ClassNotFoundException 异常。
双亲委派模型的好处
双亲委派保证了Java程序的稳定运行,可以避免类的重复加载(JVM区分不同类的方法不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类)。保证了Java的核心API不被篡改。
如果没有使用双亲委派模型,假如我们编写了一个成为java.lang.Object类的话,那么程序运行的时候,系统就会出现两个不同的Object类。双亲委派模型可以保证加载的是 JRE 里的那个 Object 类,而不是你写的 Object 类。这是因为 AppClassLoader 在加载你的 Object 类时,会委托给 ExtClassLoader 去加载,而 ExtClassLoader 又会委托给 BootstrapClassLoader,BootstrapClassLoader 发现自己已经加载过了 Object 类,会直接返回,不会去加载你写的 Object 类。
打破双亲委派模型
尽管双亲委派模型在 Java 类加载机制中提供了安全性和一致性,但在某些特殊情况下,开发者可能需要打破这个模型。例如,当需要在某个类加载器中加载特定版本的类而不依赖于父类加载器时,可以采取以下几种方法:
1. 自定义类加载器
public class CustomClassLoader extends ClassLoader {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
if (name.startsWith("com.example")) {
// 自定义加载指定包下的类
return findClass(name);
}
// 否则,使用默认的双亲委派机制
return super.loadClass(name);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
String fileName = name.replace('.', '/') + ".class";
InputStream is = getClass().getClassLoader().getResourceAsStream(fileName);
byte[] bytes = new byte[is.available()];
is.read(bytes);
return defineClass(name, bytes, 0, bytes.length);
} catch (Exception e) {
throw new ClassNotFoundException(name, e);
}
}
}
public class TestCustomClassLoader {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
CustomClassLoader customClassLoader = new CustomClassLoader();
Class<?> clazz = customClassLoader.loadClass("com.example.MyClass");
Object instance = clazz.newInstance();
System.out.println(instance.getClass().getClassLoader()); // 打印自定义类加载器
}
}
- 通过反射调用私有方法
通过反射调用 ClassLoader 的私有方法 findClass,直接加载类而不通过双亲委派模型。
public class ReflectionClassLoader {
public static void main(String[] args) throws Exception {
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
Class<?> clazz = loadClassWithoutDelegation(systemClassLoader, "com.example.MyClass");
Object instance = clazz.newInstance();
System.out.println(instance.getClass().getClassLoader()); // 打印系统类加载器
}
private static Class<?> loadClassWithoutDelegation(ClassLoader classLoader, String className) throws Exception {
// 反射获取 findClass 方法
java.lang.reflect.Method method = ClassLoader.class.getDeclaredMethod("findClass", String.class);
method.setAccessible(true);
return (Class<?>) method.invoke(classLoader, className);
}
}
- 修改类加载器的加载路径
通过修改类加载器的加载路径,使得自定义类路径优先于父类加载器加载路径
import java.net.URL;
import java.net.URLClassLoader;
public class CustomPathClassLoader {
public static void main(String[] args) throws Exception {
// 自定义类路径
URL[] urls = {new URL("file:/path/to/classes/")};
URLClassLoader customClassLoader = new URLClassLoader(urls, null); // 指定 null 作为父加载器,打破双亲委派
Class<?> clazz = customClassLoader.loadClass("com.example.MyClass");
Object instance = clazz.newInstance();
System.out.println(instance.getClass().getClassLoader()); // 打印自定义 URLClassLoader
}
}
在这个示例中,URLClassLoader 被用来加载自定义路径下的类,并通过将父加载器设置为 null 来打破双亲委派模型。