一、前言
在java中,无论是普通类、抽象类、接口、纪录类还是枚举类,它们在被编译之后,都会生成以.class结尾的字节码文件。而我们所说的类加载,其实就是把这些字节码文件,加载进内存以供虚拟机使用。然而,.class文件从加载到使用经过了一系列复杂的过程,它们分别是加载,连接和初始化,接下来,笔者将对这三个过程进行详细的介绍,让每个读者能够更加清晰的认识java的类加载过程。
二、类加载过程
上文提到过,java的类加载机制主要为加载,链接和初始化三个阶段,而其中的连接还可以细分为验证,准备和解析三个阶段,因此,详细来说,类的加载机制由加载,验证,准备,解析,初始化这几个阶段组成。
这里需要提前说明一下,链接中的解析阶段并不是一定在准备之后,初始化之前完成,在一些特殊的情况下,解析可能会在初始化之后进行,这个我会在介绍解析的部分详细介绍。
1.加载
加载是类加载过程的第一个阶段,该阶段的主要任务就是把.class字节码文件转化为二进制字节流然后加载进内存,并将其转为一种静态数据结构存储在方法区内,并在java堆中生成一个java.lang.Class的对象。需要注意的是,.class文件的来源不一定都来自本地文件,jvm在加载类时候并不会限制二进制流的来源,.class文件的字节流还可以来自于网络、数据库甚至可以运行时即时生成。
2.连接
jvm完成类的加载之后就会进行类的连接,而类的连接又会分为验证,准备和解析阶段。
1)验证
验证阶段的主要任务就是对class静态结构进行语法和语义上的分析,确保其内容符合jvm的规范,而不会产生危害jvm的行为。其验证内容主要有以下几点:
①文件格式验证,确保该字节流是否为真正的.class文件。
②元数据和字节码的验证,确保内部的元数据和方法体是否符合jvm规范。
③符号引用验证,当符号转为直接引用时,会进行该验证(在解析阶段发生)
2)准备
完成验证后类加载就进入了准备阶段。准备阶段用一句话来简单概括就是给类中的静态变量(static修饰)分配内存,并赋初值。这里有几个点需要注意:
①准备阶段赋初值,只包括静态的变量,而不包括静态实例对象。
②这里的赋初值需要分情况讨论,如果静态变量被final修饰,那么赋予的初始值就是你编写代码时,显示赋予的值,如果没有final修饰,那么赋予的值就是默认值(一般为0),而不是代码中显示声明的值。
3)解析
连接的最后一个阶段就是解析。该阶段同样可以用一句话简单概括。那就是将符号引用转为直接引用。
这里解释以下什么是符号引用,java文件编译为.class文件之后,是并不知道其依赖的类会在内存中的哪个位置的,这时就会用一个jvm规范中的符号来指代该类,当进入解析阶段时,就会把类中的这个符号替换为所依赖的类的直接引用。如果依赖的类还没有被加载进内存,就会触发该类的类加载。当然,java中存在多态,这也就解释了上文提及的解析阶段可能发生在初始化之后,因为,类中成员引用的对象应用可能指向的是子类,而这需要在运行时(也就是初始化之后)才能确认,这也是我们常说的的动态绑定(或晚绑定)。
3.初始化
连接阶段完成,代表类已经正式加载进了虚拟机,下一个阶段就是初始化。
初始化阶段的主要任务就是将类中成员变量进行初始化,也就是给类中成员变量赋上你在编写代码时显示初始化的值,这里同样需要注意几个点;
①初始化是类层面的事,初始化会给成员变量,静态变量赋初值,也会执行静态代码块,但不会执行构造方法的逻辑,因为构造方法是相对于对象。
②类加载进内存不一定会进行初始化。
//初始化时:
class Test {
private int a = 0; //执行
public static int b = 6; //执行
private static String s; //执行
static {
s = "a"; //执行
}
public Test() {} //不会执行
}
三、双亲委派
在介绍双亲委派模型之前,需要先了解java中的三种类加载器。
①Bootstrap ClassLoader,加载<JAVA_HOME>/lib中的类
②Extension ClassLoader,加载<JAVA_HOME>/lib/ext路径下的类
③Application ClassLoader,加载classpath/下的类,包括第三方类库
③User ClassLoader,由用户实现,可加载任意来源的类
这里需要注意两个点:
①除了Bootstrap ClassLoader是由c++进行实现外,其余三个类加载器均由java实现,也就是说,其余的三个类加载器可以在代码中引用,如果你试图获取由Bootstrap ClassLoader加载的类的加载器的话,只会得到null。
②类加载器拥有自己的命名空间,也就是说用不同的类加载器加载同一个限定名的同一个类,jvm会把它当成两个不同的类
了解了类加载器之后,我们就可以谈谈双亲委派模型了。
java引入双亲委派模型,主要是为了解决类加载时的安全性和可靠性的问题,再清晰点来说,双亲委派就是为了让同一个限定名的类只会被一个类加载器加载一次,保证这个类在内存中的唯一性。
双亲委派模型的机制其实并没有多复杂,简单来说,当一个类加载器收到加载类的请求时,自己不会立即加载,而是会传递给自己的父加载器,一直向上传递,当父亲加载器无法完成加载时,才会将其交给子加载器加载。这里尤其需要注意,加载器之间的父子关系并不是继承的关系,这里使用“父子”一词只是为了方便理解。
这样把类的加载委派给自己的双亲加载器的行为就是双亲委派。这样做的好处在于,因为一开始就把类加载的委托给上层加载器,如果上层类加载器能够加载就不会在让下层加载器加载,不会出现重复加载类的现象。
这里带大家看一看ClassLoader中loadClass的源码。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 查看该类是否已经加载
Class<?> c = findLoadedClass(name);
//如果该类没有加载则执行加载逻辑
if (c == null) {
long t0 = System.nanoTime();
try {
//查看自己有没有父加载器
if (parent != null) {
//有父加载器就交给父加载器加载
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// 如果父加载器不能加载就调用findClass自己加载
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
PerfCounter.getParentDelegationTime().addTime(t1 - t0);
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
四、双亲委派的破坏
我们上文提到过,双亲委派的一大作用就是为了防止同一限定名的类被不同类加载器重复加载,导致其在内存中不唯一。但是我们可以通过自己实现用户自定义类加载器重写loadClass方法来自定义类的加载过程,这样其实可以破坏类的双亲委派机制。举个例子,这里继承ClassLoader实现了一个自定义类加载器,不委托给父加载器自己直接加载类:
public static void main(String[] args) throws Exception {
//自定义类加载器
ClassLoader classLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
if (name.startsWith("java.")) return super.loadClass(name);
String filePath = "/" + name.replace(".", "/") + ".class";
System.out.println(filePath);
InputStream inputStream = GetSrcApplication.class.getResourceAsStream(filePath);
if (inputStream == null) return super.loadClass(name);
try {
//直接加载二进制字节流
byte[] buffer = new byte[inputStream.available()];
int len = inputStream.read(buffer);
return this.defineClass(name, buffer, 0, buffer.length);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
};
//用自定义类加载器加载类
Class<?> clazz = classLoader.loadClass("com.example.getsrc.Components.Bean1");
Object bean1 = clazz.getConstructor().newInstance();
System.out.println(bean1 instanceof com.example.getsrc.Components.Bean1);
}
结果:
发现最后打印的结果为false,也就是说,虽然类的限定名完全相同但jvm却不认为是同一个类。这就是对双亲委派的一种破坏。也正因为此,java建议重写ClassLoader的findClass而不是loadClass。当然对双亲委派的破坏行为不止这一种,这里就不多做赘述了。
五、总结
总之,java的类加载机制包括加载,连接(验证,准备,解析),初始化这几个阶段,而双亲委派是类加载中的重要机制,为了保证类的正常有序加载。