java虚拟机类加载机制

本文主要讲解了java类加载机制

类加载时机

我们编写的“.java”类文件最终都会被编译器编译为“.class”的拓展名文件,而“.class”文件中保存着Java代码经转换后的虚拟机指令,当我们需要使用一个类的时候,就需要将这个文件加载进内存中保存为我们所说的class对象。而将.class文件加载到虚拟机内存中的过程称为类加载。类加载过程如下:
类加载的过程

  • 加载:类加载过程的一个阶段,需要经过以下阶段:通过一个类的全限定名来获取定义此类的二进制流,然后将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,之后再内存中生成一个代表这个类的Class对象,作为方法区这个类的各种数据访问入口。

  • 验证:目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。

  • 准备:为类变量(即static修饰的字段变量)分配内存并且设置该类变量的初始值即0(如static int i = 123;这里只将 i 初始化为 0,至于 123 的值将在初始化时赋值),这里不包含用final修饰的static,因为final在编译的时候就会分配了,注意这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。

  • 解析:主要将常量池中的符号引用替换为直接引用的过程。符号引用就是一组符号来描述目标,可以是任何字面量,而直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。有类或接口的解析,字段解析,类方法解析,接口方法解析。

  • 初始化:类加载最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量(如前面只初始化了默认值的static变量将会在这个阶段赋值,成员变量也将被初始化)。

类加载器

启动(Bootstrap)类加载器

启动类加载器主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的,是虚拟机自身的一部分,它负责将/lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中,注意必由于虚拟机是按照文件名识别加载jar包的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包丢到lib目录下也是没有作用的(出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类)。

扩展(Extension)类加载器

扩展类加载器由sun.misc.Launcher$ExtClassLoader实现,是Launcher的静态内部类,它负责加载/lib/ext目录下或者由系统变量java.ext.dir指定位路径中的类库,开发者可以直接使用标准扩展类加载器。

应用程序(Application)类加载器

此加载器是ClassLoader.getSystemClassLoader()方法的返回值,因此又称系统类加载器,是由sun.misc.Launcher$AppClassLoader实现。它负责加载系统类路径java -classpath或-D java.class.path 指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,方法可以获取到该类加载器。

在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器。在加载某个类的class文件时,Java虚拟机采用的是双亲委派模式即把请求交由父类处理,它一种任务委派模式,下面我们进一步了解它。

双亲委派模型

双亲委派模式要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器,请注意双亲委派模式中的父子关系并非通常所说的类继承关系,而是采用组合关系来复用父类加载器的相关代码。类加载器间的关系如下:
双亲委派模型
双亲委派模式是在Java 1.2后引入的,其工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。双亲委派的实现集中在 java.lang.ClassLoader 的 loadClass() 方法中,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
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) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
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;
}
}

双亲委派模型的优势

采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。例如类 java.lang.Object 他存放在rt.java中,无论哪一个类加载器加载这个类,最终都会委派给最顶层的启动类加载器进行加载。我们想象一下,如果没有双亲委派,那么用户自己编写了一个 java.lang.Object 类,并存放在ClassPath中,那系统将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序将变得一片混乱。

如何破坏双亲委派模型

既然我们知道,双亲委派模型是通过loadClass方法实现,那么我们只要自定义类加载器继承ClassLoader并覆写起loadClass方法。具体代码示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class ClassLoaderTest {

public static void main(String[] args) throws Exception {
ClassLoader classLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String rootDic = "D:\\IdeaProjects\\test\\src\\main\\java\\";
String fileName = rootDic + name.replace(".", File.separator) + ".class";
FileInputStream fis = new FileInputStream(fileName);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024 * 4];
int n = -1;
while ((n = fis.read(buffer)) != -1) {
baos.write(buffer, 0, n);
}
byte[] byteArray = baos.toByteArray();
return defineClass(name, byteArray, 0, byteArray.length);
} catch (IOException e) {
return super.loadClass(name);
}
}
};
Object obj = classLoader.loadClass("com.ovvow.test.ClassLoaderTest").newInstance();
System.out.println(obj.getClass());
System.out.println(obj instanceof ClassLoaderTest);
}
}

如下,我们改写了ClassLoader的loadClass方法,直接使用 defineClass() 强行加载 ClassLoaderTest,结果输出如下:

1
2
class com.ovvow.mybatis.utils.ClassLoaderTest
false

我们发现第二个输出为 false,说明判断两个类是否是同一个类,除了加载的类文件一样,还必须由同一个类加载器加载。这时候有人会想到,如果我在自己文件中创建一个 java.lang.String 文件,那是否可以通过自定义类加载器加载进我们的虚拟机中呢?答案是:不行。如果你通过百度去查的话,或许你能看到有些同学说可以。但这其实是因为有些人想当然的结果,他们并没有去验证结果,同时也没有阅读过源码,就导致一个错误的知识点慢慢就扩散开了。所以学知识,百度只是参考,需要自己去验证。

我们来看看 defineClass() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
protected final Class<?> defineClass(String name, byte[] b, int off, int len, 
ProtectionDomain protectionDomain) throws ClassFormatError {
protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}

private ProtectionDomain preDefineClass(String name, ProtectionDomain pd) {
if (!checkName(name))
throw new NoClassDefFoundError("IllegalName: " + name);

// Note: Checking logic in java.lang.invoke.MemberName.checkForTypeAlias
// relies on the fact that spoofing is impossible if a class has a name
// of the form "java.*"
if ((name != null) && name.startsWith("java.")) {
throw new SecurityException
("Prohibited package name: " +
name.substring(0, name.lastIndexOf('.')));
}
if (pd == null) {
pd = defaultDomain;
}

if (name != null) checkCerts(name, pd.getCodeSource());

return pd;
}

我们所有的类加载最终都会走到这个方法去加载类文件, 我们看到它是个final方法,是无法被我们重写的,其中调用了 preDefineClass() 方法进行了前置验证,而前置验证中直接就把 “java”开头的所有类文件给拒绝加载了,直接抛出 SecurityException 异常,因此不管你这个类是通过何种途径加载的,只要你是 java 开头,就会直接抛出一个异常。(ps:在《深入理解java虚拟机》第 255 页最底下的注解也说明了,加载java.lang.String是会抛异常的)

结语

其实还有很多没有讲完,还有很多可以拓展的,比如说各种加载器parent的隐藏关系,JDBC是如何破坏的双亲委派实现驱动加载等等。但是目前水平就到这了,后续学习理解完再补充。