很长一段时间里,我对 Java 的类加载机制都很是的抗拒,由于我以为太难理解了。但为了成为一名优秀的 Java 工程师,我决定硬着头皮研究一下。java
在聊 Java 类加载机制以前,须要先了解一下 Java 字节码,由于它和类加载机制息息相关。程序员
计算机只认识 0 和 1,因此任何语言编写的程序都须要编译成机器码才能被计算机理解,而后执行,Java 也不例外。安全
Java 在诞生的时候喊出了一个很是牛逼的口号:“Write Once, Run Anywhere”,为了达成这个目的,Sun 公司发布了许多能够在不一样平台(Windows、Linux)上运行的 Java 虚拟机(JVM)——负责载入和执行 Java 编译后的字节码。bash
到底 Java 字节码是什么样子,咱们借助一段简单的代码来看一看。网络
源码以下:学习
package com.cmower.java_demo;
public class Test {
public static void main(String[] args) {
System.out.println("沉默王二");
}
}
复制代码
代码编译经过后,经过 xxd Test.class
命令查看一下这个字节码文件。spa
xxd Test.class
00000000: cafe babe 0000 0034 0022 0700 0201 0019 .......4."...... 00000010: 636f 6d2f 636d 6f77 6572 2f6a 6176 615f com/cmower/java_ 00000020: 6465 6d6f 2f54 6573 7407 0004 0100 106a demo/Test......j 00000030: 6176 612f 6c61 6e67 2f4f 626a 6563 7401 ava/lang/Object. 00000040: 0006 3c69 6e69 743e 0100 0328 2956 0100 ..<init>...()V.. 00000050: 0443 6f64 650a 0003 0009 0c00 0500 0601 .Code........... 00000060: 000f 4c69 6e65 4e75 6d62 6572 5461 626c ..LineNumberTabl 复制代码
感受有点懵逼,对不对?3d
懵就对了。调试
这段字节码中的 cafe babe
被称为“魔数”,是 JVM 识别 .class 文件的标志。文件格式的定制者能够自由选择魔数值(只要没用过),好比说 .png 文件的魔数是 8950 4e47
。code
至于其余内容嘛,能够选择忘记了。
了解了 Java 字节码后,咱们来聊聊 Java 的类加载过程。
Java 的类加载过程能够分为 5 个阶段:载入、验证、准备、解析和初始化。这 5 个阶段通常是顺序发生的,但在动态绑定的状况下,解析阶段发生在初始化阶段以后。
1)Loading(载入)
JVM 在该阶段的主要目的是将字节码从不一样的数据源(多是 class 文件、也多是 jar 包,甚至网络)转化为二进制字节流加载到内存中,并生成一个表明该类的 java.lang.Class
对象。
2)Verification(验证)
JVM 会在该阶段对二进制字节流进行校验,只有符合 JVM 字节码规范的才能被 JVM 正确执行。该阶段是保证 JVM 安全的重要屏障,下面是一些主要的检查。
cafe bene
开头)。3)Preparation(准备)
JVM 会在该阶段对类变量(也称为静态变量,static
关键字修饰的)分配内存并初始化(对应数据类型的默认初始值,如 0、0L、null、false 等)。
也就是说,假若有这样一段代码:
public String chenmo = "沉默";
public static String wanger = "王二";
public static final String cmower = "沉默王二";
复制代码
chenmo 不会被分配内存,而 wanger 会;但 wanger 的初始值不是“王二”而是 null
。
须要注意的是,static final
修饰的变量被称做为常量,和类变量不一样。常量一旦赋值就不会改变了,因此 cmower 在准备阶段的值为“沉默王二”而不是 null
。
4)Resolution(解析)
该阶段将常量池中的符号引用转化为直接引用。
what?符号引用,直接引用?
符号引用以一组符号(任何形式的字面量,只要在使用时可以无歧义的定位到目标便可)来描述所引用的目标。
在编译时,Java 类并不知道所引用的类的实际地址,所以只能使用符号引用来代替。好比 com.Wanger
类引用了 com.Chenmo
类,编译时 Wanger 类并不知道 Chenmo 类的实际内存地址,所以只能使用符号 com.Chenmo
。
直接引用经过对符号引用进行解析,找到引用的实际内存地址。
5)Initialization(初始化)
该阶段是类加载过程的最后一步。在准备阶段,类变量已经被赋过默认初始值,而在初始化阶段,类变量将被赋值为代码指望赋的值。换句话说,初始化阶段是执行类构造器方法的过程。
oh,no,上面这段话说得很抽象,很差理解,对不对,我来举个例子。
String cmower = new String("沉默王二");
复制代码
上面这段代码使用了 new
关键字来实例化一个字符串对象,那么这时候,就会调用 String 类的构造方法对 cmower 进行实例化。
聊完类加载过程,就不得不聊聊类加载器。
通常来讲,Java 程序员并不须要直接同类加载器进行交互。JVM 默认的行为就已经足够知足大多数状况的需求了。不过,若是遇到了须要和类加载器进行交互的状况,而对类加载器的机制又不是很了解的话,就不得不花大量的时间去调试 ClassNotFoundException
和 NoClassDefFoundError
等异常。
对于任意一个类,都须要由它的类加载器和这个类自己一同肯定其在 JVM 中的惟一性。也就是说,若是两个类的加载器不一样,即便两个类来源于同一个字节码文件,那这两个类就一定不相等(好比两个类的 Class 对象不 equals
)。
站在程序员的角度来看,Java 类加载器能够分为三种。
1)启动类加载器(Bootstrap Class-Loader),加载 jre/lib
包下面的 jar 文件,好比说常见的 rt.jar。
2)扩展类加载器(Extension or Ext Class-Loader),加载 jre/lib/ext
包下面的 jar 文件。
3)应用类加载器(Application or App Clas-Loader),根据程序的类路径(classpath)来加载 Java 类。
来来来,经过一段简单的代码了解下。
public class Test {
public static void main(String[] args) {
ClassLoader loader = Test.class.getClassLoader();
while (loader != null) {
System.out.println(loader.toString());
loader = loader.getParent();
}
}
}
复制代码
每一个 Java 类都维护着一个指向定义它的类加载器的引用,经过 类名.class.getClassLoader()
能够获取到此引用;而后经过 loader.getParent()
能够获取类加载器的上层类加载器。
这段代码的输出结果以下:
sun.misc.Launcher$AppClassLoader@73d16e93
sun.misc.Launcher$ExtClassLoader@15db9742
复制代码
第一行输出为 Test 的类加载器,即应用类加载器,它是 sun.misc.Launcher$AppClassLoader
类的实例;第二行输出为扩展类加载器,是 sun.misc.Launcher$ExtClassLoader
类的实例。那启动类加载器呢?
按理说,扩展类加载器的上层类加载器是启动类加载器,但在我这个版本的 JDK 中, 扩展类加载器的 getParent()
返回 null
。因此没有输出。
若是以上三种类加载器不能知足要求的话,程序员还能够自定义类加载器(继承 java.lang.ClassLoader
类),它们之间的层级关系以下图所示。
这种层次关系被称做为双亲委派模型:若是一个类加载器收到了加载类的请求,它会先把请求委托给上层加载器去完成,上层加载器又会委托上上层加载器,一直到最顶层的类加载器;若是上层加载器没法完成类的加载工做时,当前类加载器才会尝试本身去加载这个类。
PS:双亲委派模型忽然让我联想到朱元璋同志,这个同志当上了皇帝以后连宰相都不要了,全部的事情都亲力亲为,只有本身没精力没时间作的事才交给大臣们去干。
使用双亲委派模型有一个很明显的好处,那就是 Java 类随着它的类加载器一块儿具有了一种带有优先级的层次关系,这对于保证 Java 程序的稳定运做很重要。
上文中曾提到,若是两个类的加载器不一样,即便两个类来源于同一个字节码文件,那这两个类就一定不相等——双亲委派模型可以保证同一个类最终会被特定的类加载器加载。
硬着头皮翻看了大量的资料,而且动手去研究之后,我发现本身居然对 Java 类加载机制(JVM 将类的信息动态添加到内存并使用的一种机制)不那么抗拒了——真是蛮奇妙的一件事啊。
也许学习就应该是这样,只要你勇于挑战本身,就能收获知识——就像山就在那里,只要你肯攀登,就能到达山顶。