在开篇我们先回忆一下Java程序的运行过程,首先将Java源代码编译为字节码,然后类加载器将字节码加载到内存,然后解释器解析字节码并转换为机器码,融合原生库,提交给操作系统执行。
上述过程中,"类加载器将字节码加载到内存"可以称之为类加载阶段,我们之所以能够反射调用JVM内存中的Class对象,依靠的便是类加载机制。类加载的流程主要分为三个步骤:加载、链接、初始化。下面是每个步骤的详细描述...
类加载流程
①加载
首先是加载阶段,通过双亲委派机制找出对应的ClassLoader将.class字节码(这里加载的数据实质上是二进制字节流,具体可以参考ClassLoader源码中的defineClass方法)从不同的数据源读取到JVM内存中,并将类的数据结构在堆中映射为Class对象,我们在反射中调用的Class对象即该对象。不同的数据源指的是我们可以用不同的方式获取字节码数据,如jar文件、class文件、网络数据源等,但如果加载的数据并非ClassFile的结构,则会抛出ClassFormatError。
不同类型的文件由不同的类加载器进行加载,我们也可以自定义类加载器,JVM为了安全和性能方面的考虑也设置了双亲委托机制,这部分的内容一会儿再说,这里先大致过一遍类加载的流程。
②链接
把原始的类定义信息平滑地转换到JVM运行的过程中,但链接这个流程是非必须的,具体在下面的源码分析中会提到。链接又分为三个子步骤:
验证:这是虚拟机安全的重要保障,JVM需要检验加载的二进制字节流是否符合规范,若不符合则认为是VerifyError,这样就能够防止恶意信息或者不合规的信息危害JVM的运行。例如我们自己创建一个java.lang.String类去加载,JVM会识别并抛出异常。
准备:为类中的静态变量分配内存,并设置初始值,这里注意是赋予初始值!
解析:这一步会把常量池中的符号引用替换为直接引用。
在执行完链接步骤后,内存中存在着一个Class对象的雏形,它只具备基本的数据结构,但并未被赋值。
③初始化
初始化阶段真正执行了类初始化的代码逻辑,首先执行类的构造器方法(注意这里不是我们常说的构造方法),该方法由编译器自动收集类中的所有静态变量赋值动作和静态语句块合并产生。在执行完初始化步骤后,加载文件对应Class对象就成型了。
在初始化一个类的时候,若父类未被初始化,则会优先初始化父类。也就是说,无论我们加载任何类,首先被初始化的都是Object类。可以通过一个简单的程序对初始化流程进行验证:
public class TestClintFather {
public static int juju = 5;
static {
System.out.println("测试father" + juju);
}
}
public class TestClint extends TestClintFather{
static {
System.out.println("测试" + juju);
}
}
public class Demo{
public static void main(String[] args) throws ClassNotFoundException{
Class clz = Class.forName("com.whl.reflect.TestClint");
}
}
结果如下,这就说明了初始化阶段,若父类未被初始化,则会优先初始化父类,且会执行类的静态块并对静态变量赋值。
测试father5
测试5
若注意观察的话,可以发现我在main方法中调用的是forName(),思考一下若调用的是loadClass()方法,会有什么不同?
为此,我们可以先去观察一下forName()的源码
public static Class<?> forName(String className) throws ClassNotFoundException {
Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
private static native Class<?> forName0(String name,
boolean initialize, ClassLoader loader, Class<?> caller) throws ClassNotFoundException;
可以看到该方法底层调用的是原生方法,通过 boolean initialize = true 我们不难得知,forName()在执行后是经过了初始化阶段的。
再观察loadClass()方法的部分源码,只需要关注resolve参数即可,它默认是false,也就是说调用loadClass()后只经历了类加载流程的加载阶段。二者区别不言而喻
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
....
if (resolve) {
resolveClass(c);
}
return c;
}
}
那么二者分别适用于什么场景呢?
forName()最典型最常见的使用场景,就是用于获取jdbc.driver的Class对象了
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}
static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
}
而loadClass()比较典型的则是在spring-ioc中,读取bean配置文件的时候,如果是以延迟加载Lazy-load的方式,则需要采用loadClass()进行类加载,但ClassLoader不需要执行链接和初始化步骤,类的初始化工作会留到使用的时候再完成,这样就可以加快加载速度。
双亲委托机制
类加载器主要分为以下四种,其分类与工作机制如下图所示,它们首先会自底而上检查类是否已经被加载。然后自顶而下在各自指定的位置寻找对应的字节码并加载。
我们可以通过ClassLoader的源码对双亲委托机制进行解读:
需要注意是:类加载器之间虽然存在父子关系,但并不是依赖继承实现的。我们可以通过调用类加载器的getParent()方法查看父加载器。
public abstract class ClassLoader {
//指向父ClassLoader
//这个属性也说明了类加载流程是采用双亲委托机制的
private final ClassLoader parent;
//通过指定的className 返回一个对应的Class对象
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
//
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
//有可能存在多个线程调用ClassLoader去加载同一个类,为此需要对加载的类加上同步锁
synchronized (getClassLoadingLock(name)) {
// First, 检查Class是否曾被加载过
Class<?> c = findLoadedClass(name);
//若未被加载过
if (c == null) {
long t0 = System.nanoTime();
try {
//判断parent ClassLoader是否为空
if (parent != null) {
//若parent不为空,交给parent ClassLoader执行该方法
c = parent.loadClass(name, false);
} else {
//若parent为空 则需要在BootStrapClassLoader中检查
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
//若BootStrapClassLoader中也未加载过该类
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
//则通过findClass方法去寻找指定路径下是否存在该类
//一般该方法是会被ClassLoader重写的
//BootStrap会去核心类库中检查
//Ext会去扩展类库中检查
//App会去classpath下检查
//自定义ClassLoader会通过自己重写findClass指定路径进行检查
//若直到自定义ClassLoader中都未找到,则抛出ClassNotFoundException
c = findClass(name);
....
}
}
//若曾被加载过 resolve默认为false
if (resolve) {
resolveClass(c);
}
//直接返回Class
return c;
}
}
//根据名称去查找调用的字节码
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
//解析字节流
protected final Class<?> defineClass(String name, byte[] b, int off, int len)throws ClassFormatError{
return defineClass(name, b, off, len, null);
}
}
那么为什么要采用双亲委托机制呢?
其目的主要是为了避免重复加载造成的资源浪费。就比如最常见的System.out.println语句需要加载System静态类的字节码,若不采用双亲委托机制,类A打印时加载一份System,类B打印时又加载了一份System,很显然是不合理的。若采用双亲委托机制,类A在加载了System后,类B通过双亲委托机制发现System已经被加载过了,那么直接调用已经加载过的即可。
以上,就是个人关于类加载部分的总结