何为类加载
类加载指的是JVM将class二进制文件读取到内存方法区,在堆内存中生成Class对象。
类加载过程
类加载的过程包含如下步骤:
- 加载
- 验证
- 准备
- 解析
- 初始化
加载(Loading)
在这个阶段,类加载器(ClassLoader)负责查找和导入二进制字节码到 JVM 内存中。它通过类的全限定名(包名+类名)找到对应的 .class 文件或其他包含类定义的数据源(比如 Jar 文件、网络数据流等)。
加载完成后,在内存中创建一个 java.lang.Class 类型的对象,该对象将作为方法区内的类元数据的入口,包含了与类有关的各种信息。
验证(Verification)
验证阶段是对加载的字节码数据进行合法性校验,确保符合 JVM 规范。验证内容包括文件格式验证、元数据验证、字节码验证和符号引用验证等。
可以使用-Xverify:none
或者-noverify
JVM参数,禁用验证步骤,同时也会加快Java应用的启动速度。(JDK13及其之后已废弃此参数)
准备(Preparation)
准备阶段主要是为类变量(static fields)分配内存并设置初始值。这里的初始值通常是指数据类型的零值(例如 int 类型为 0,对象引用为 null),而不是程序员在 Java 源代码中为它们赋予的初始值。对于 static final 常量,如果其值在编译期就可以确定,则会在此阶段直接赋值为常量池中的值。
解析(Resolution)
解析阶段主要是将常量池中的符号引用替换为直接引用的过程。符号引用包括类和接口的全限定名、字段名和方法名等,直接引用则更为具体,可以是内存偏移量或句柄等。此阶段主要针对类或者接口、字段和方法的符号引用进行解析。
初始化(Initialization)
初始化是类加载过程的最后一步,真正执行类初始化语句(即类构造器 <clinit> 方法),为类变量赋初始值或者执行其他初始化逻辑。初始化只会执行一次,且在类首次主动使用时触发,例如创建类的实例、调用类的静态方法或访问类的静态字段时。
Java 类加载器
Java 类加载器是 Java 虚拟机 (JVM) 中负责动态加载 Java 类到 JVM 运行时数据区中的关键组件。在 Java 中,类加载过程是 JVM 实现动态性的重要手段,它通过不同的类加载器协作完成对类的查找、加载、链接(验证、准备、解析)和初始化。
Java 类加载器可分为以下几种类型:
- Bootstrap ClassLoader 是最顶层的类加载器,C语言实现,是 JVM 的一部分,不继承自
java.lang.ClassLoader
类。它负责加载 Java 核心库,即 JDK 的 lib 目录下的 rt.jar、resources.jar 等核心类库,或被-Xbootclasspath
参数指定路径中的class文件。如java.*
开头的类。无法使用loader.getParent()
方法获取。 - ExtClassLoader 由 Java 编写,继承自 ClassLoader 类。负责加载
$JAVA_HOME/lib/ext
目录中的class,以及被java.ext.dirs
系统变量指定路径中的class。如javax.*
开头的类。 - AppClassLoader 负责加载用户类路径(classpath,
java.class.path
系统变量)中的类。如果没有自定义类加载器,应用程序的类默认会被AppClassLoader加载。 - 其他ClassLoader 需要用户在特定的使用场景显式使用。例如从URL加载class,或者说是热部署、模块化框架、加密的类资源等。这些classloader需要继承
ClassLoader
类。
Java的classLoader具有层级(父子)关系,classLoader按照双亲委派模型加载class。双亲委派模型后面说明。上面所属的四种classLoader,前面的classLoader是后面的父classLoader。
Java中所有的Class
都有一个classLoader
属性,用来标明该类是由哪个类加载器加载的。在程序中获取一个类由哪个类加载器加载,可使用如下方式:
SomeKlass.class.getClassLoader();
下面举一个例子:
public class ClassLoaderDemo {
public static void main(String[] args) {
System.out.println(String.class.getClassLoader());
System.out.println(ClassLoaderDemo.class.getClassLoader());
}
}
执行的结果为:
null
sun.misc.Launcher$AppClassLoader@18b4aac2
如果一个类由BootstrapClassLoader加载,那么该类的classLoader属性值为null。如果一个classloader的父classLoader为BootstrapClassLoader,这个classloader的parent属性为null。(反过来也成立,null会被视为BootstrapClassLoader)
类加载器行为
- 加载class时默认使用调用者的classLoader加载。
- classloader自己不加载这个class,将加载工作转交给父加载器去加载(如果父加载器还有父加载器,一直向上传递),如果父加载器找不到这个类无法加载,子加载器才会尝试加载。
- 父加载器加载的class是会共享给所有的子加载器的(都算作已加载,子加载器的
loadClass
方法能够获取到父加载器加载的class)。但是多个平行关系的子加载器加载的class,互相之间不共享。 - 决定两个class是否相同的不仅是包名和class名,还有这个class的类加载器。也就是说如果包名和类名相同的class被两个不存在父子关系的类加载器加载,那么加载后的这两个class是完全不同的。
类加载机制
- 全盘负责:当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入。除非显式使用另外一个类加载器来载入。
- 父类委托:任何类加载器先让父类加载器试图加载该类,只有在父类加载器无法加载该类时自己才尝试从自身的类路径中加载该类。
- 缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这种机制导致了class文件的修改无法实时在JVM中体现出来。如果需要实时体现出修改(称之为热加载),需要放弃Class缓存然后使用类加载器重新加载(例如再次新创建一个自定义加载器,重新加载该类)。
类加载的方式
类加载有三种方式:
1 启动应用时候由JVM初始化加载
2 通过Class.forName()方法动态加载
3 通过ClassLoader.loadClass()方法动态加载
这三种方式的区别为:
- 使用ClassLoader.loadClass()加载类,不会执行初始化块
- 使用Class.forName()加载类,默认会执行初始化块
- 使用Class.forName(),指定类加载器来加载类,不会执行初始化块
双亲委派模型
双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
双亲委派模型目的是确保类的全局唯一性。
ClassLoader
类的loadClass
方法源代码完整实现了双亲委派模型。代码如下所示:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 加锁,每个class对应着不同的lock object
// 确保同一个类不会被同时加载
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// 首先检查这个class是否已经加载过了
// 如果已经被加载过,直接返回加载过的类
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 如该加载器的父加载器存在,则调用父类加载器的loadClass方法。
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 如果parent为null,说明父加载器为bootstrap类加载器,查找并返回使用bootstrap类加载器加载的类
// 如果bootstrap没有加载该类,返回null
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
// 如果类仍未加载(父类加载器没有加载,bootstrap类加载器也没有加载)
// 则调用findClass方法,亲自加载该类
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
// findClass方法是详细的根据类名查找类二进制文件,读取并解析的过程
// findClass方法需要ClassLoader的子类来重写
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
上面代码的主要逻辑为:
- 检查类是否已经被加载。如果已被加载,返回加载过的类。
- 如果类没有被加载,交给该类加载器的父加载器去加载。如果父加载器为BootStrapClassLoader,查找并返回它加载的类。
- 如果父类加载器无法加载该类。自己再亲自去加载这个类。
Thread的contextClassLoader
用于使用父加载器加载的类去显式加载子加载器范围内的类的情况,打破双亲委派模型。例如Java SPI。SPI的ServiceLoader::load
代码如下所示:
public final class ServiceLoader<S> implements Iterable<S> {
public static <S> ServiceLoader<S> load(Class<S> service) {
// 在Launcher类的构造器中被赋值为AppClassLoader
// 可以读取classpath
// ServiceLoader自身默认是BootStrap ClassLoader加载的,如果用这个class loader是无法读取到用户classpath中的类的
ClassLoader cl = Thread.currentThread().getContextClassLoader();
// 这个方法中最终由指定classLoader的Class.forName方法去加载实现类
return ServiceLoader.load(service, cl);
}
}
从上面分析可知获取线程context classLoader的方法为:
ClassLoader loader = Thread.currentThread().getContextClassLoader();
线程的contextClassLoader默认为父线程的contextClassLoader。可以使用setContextClassLoader
方法修改。
主要注意的是,contextClassLoader
除非有意使用,否则永远不会被调用。加载class默认使用的classLoader
永远是调用者的classLoader
。
除此之外Context ClassLoader还可以用于线程间classLoader的共享和不同线程间classLoader的隔离。
自定义classLoader使用场景
自定义classLoader的几个典型的使用场景:
- 解决依赖冲突:使用不同的classLoader加载package和name相同的class,可以实现不同版本的class共存。
- 热加载:检测二进制文件变更(最后修改时间),检测到变更之后创建新的classLoader然后加载这个class(不创建新的classLoader无法再次加载该class)。
- class加密:对编译之后的class文件二进制内容加密。使用时借助自定义classLoader读取加密的二进制内容,解密后再交给JVM解析class。
编写自定义classLoader需要继承ClassLoader
类,重写findClass
方法。自定义加载逻辑位于findClass
方法中。获取到Class内容byte数组之后,将其传递给defineClass
方法,解析为Java的Class。比如说上面的class加密,可以在findClass
的时候对原始class文件内容解密之后再交给defineClass
。
一个最简单的例子,使用自定义classLoader读取项目编译输出目录(使用maven在IDE里执行对应的是target/classes
目录)中指定名字的class二进制文件,然后解析为Java的Class。
class MyClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) {
String className = name.replace(".", "/").concat(".class");
byte[] bytes;
try (InputStream stream = getClass().getClassLoader().getResourceAsStream(className);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
int i;
while ((i = stream.read()) != -1) {
outputStream.write(i);
}
bytes = outputStream.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
}
if (bytes == null) {
throw new RuntimeException("Class not found");
}
return defineClass(name, bytes, 0, bytes.length);
}
}
使用方式:
Class<?> aClass = myClassLoader.loadClass("org.example.ClassLoaderDemo");
Class<?> bClass = myClassLoader.findClass("org.example.ClassLoaderDemo");
System.out.println(aClass.getClassLoader());
System.out.println(bClass.getClassLoader());
输出为:
sun.misc.Launcher$AppClassLoader@18b4aac2
org.example.MyClassLoader@6bc7c054
解释:
loadClass
方法是用双亲委派模型加载,因为ClassLoaderDemo
类位于classpath中,在启动的时候已经被AppClassLoader
加载过了。所以aClass
的classLoader为AppClassLoader
。loadClass
方法间接调用了findClass
方法。实际开发中建议使用loadClass
方法。
findClass
方法是用户自己实现的,如果直接调用的话没有考虑双亲委派模型。这里为了演示直接调用。不建议在项目中直接使用。
网友评论