本文将由浅及深,介绍 Java
类加载的过程和原理,进一步对类加载器的进行源码分析,完成一个自定义的类加载器。
类加载器简言之,就是用于把 .class
文件中的字节码信息转化为具体 的java.lang.Class
对象的过程的工具。
具体过程:
在实际类加载过程中, JVM
会将所有的 .class
字节码文件中的二进制数据读入内存中,导入运行时数据区的方法区中。
当一个类首次被主动加载或被动加载时,类加载器会对此类执行类加载的流程 – 加载、连接(验证、准备、解析)、初始化。
如果类加载成功,堆内存中会产生一个新的 Class
对象, Class
对象封装了类在方法区内的数据结构。
Class
对象的创建过程描述:
类加载的过程分为三个步骤(五个阶段) :加载 -> 连接(验证、准备、解析)-> 初始化。
加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段可以在初始化阶段之后发生,也称为动态绑定或晚期绑定。
类加载的过程描述:
加载:查找并加载类的二进制数据的过程。
通过类的全限定名定位 .class
文件,并获取其二进制字节流。
把字节流所代表的静态存储结构转换为方法区的运行时数据结构。
在 Java
堆中生成一个此类的 java.lang.Class
对象,作为方法区中这些数据的访问入口。
连接:包括验证、准备、解析三步。
验证:确保被加载的类的正确性。验证是连接阶段的第一步,用于确保 Class
字节流中的信息是否符合虚拟机的要求。
具体验证形式:
文件格式验证:验证字节流是否符合 Class
文件格式的规范;例如:是否以 0xCAFEBABE
开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
元数据验证:对字节码描述的信息进行语义分析(注意:对比 javac
编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了 java.lang.Object
之外。
字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
符号引用验证:确保解析动作能正确执行。
准备:为类的静态变量分配内存,并将其初始化为默认值。准备过程通常分配一个结构用来存储类信息,这个结构中包含了类中定义的成员变量,方法和接口信息等。
具体行为:
这时候进行内存分配的仅包括类变量( static
),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在 Java
堆中。
这里所设置的初始值通常情况下是数据类型默认的零值(如 0
、 0L
、 null
、 false
等),而不是被在 Java
代码中被显式赋值。
解析:把类中对常量池内的符号引用转换为直接引用。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符等7类符号引用进行。
初始化:对类静态变量赋予正确的初始值 (注意和连接时的解析过程区分开)。
实现对声明类静态变量时指定的初始值的初始化;
实现对使用静态代码块设置的初始值的初始化。
如果此类没被加载、连接,则先加载、连接此类;
如果此类的直接父类还未被初始化,则先初始化其直接父类;
如果类中有初始化语句,则按照顺序依次执行初始化语句。
创建类的实例( new
关键字);
java.lang.reflect
包中的方法(如: Class.forName(“xxx”)
);
对类的静态变量进行访问或赋值;
访问调用类的静态方法;
初始化一个类的子类,父类本身也会被初始化;
作为程序的启动入口,包含 main
方法(如: SpringBoot
入口类)。
主动引用:在类加载阶段,只执行加载、连接操作,不执行初始化操作。
创建类的实例( new
关键字);
java.lang.reflect
包中的方法(如: Class.forName(“xxx”)
);
对类的静态变量进行访问或赋值;
访问调用类的静态方法;
初始化一个类的子类,父类本身也会被初始化;
作为程序的启动入口,包含 main
方法(如: SpringBoot
入口类)。
代码示例:
public class OptimisticReference0 {
static {
System.out.println(OptimisticReference0.class.getSimpleName() + " is referred!");
}
public static void main(String[] args) {
System.out.println();
}
}
运行结果:
OptimisticReference0 is referred!
代码示例:
public class OptimisticReference1 {
public static class Parent {
static {
System.out.println(Parent.class.getSimpleName() + " is referred!");
}
}
public static class Child extends Parent {
static {
System.out.println(Child.class.getSimpleName() + " is referred!");
}
}
public static void main(String[] args) {
new Child();
}
}
运行结果:
Parent is referred! Child is referred!
代码示例:
public class OptimisticReference2 {
public static class Child {
protected static String name;
static {
System.out.println(Child.class.getSimpleName() + " is referred!");
name = "Child";
}
}
public static void main(String[] args) {
System.out.println(Child.name);
}
}
运行结果:
Child is referred! Child
代码示例:
public class OptimisticReference3 {
public static class Child {
protected static String name;
static {
System.out.println(Child.class.getSimpleName() + " is referred!");
}
}
public static void main(String[] args) {
Child.name = "Child";
}
}
运行结果:
Child is referred!
代码示例:
public class OptimisticReference4 {
public static void main(String[] args) throws ClassNotFoundException {
Class.forName("org.ostenant.jdk8.learning.examples.reference.optimistic.Child");
}
}
运行结果:
Child is referred!
被动引用: 在类加载阶段,会执行加载、连接和初始化操作。
被动引用的几种形式:
通过子类引用父类的的静态字段,不会导致子类初始化;
定义类的数组引用而不赋值,不会触发此类的初始化;
访问类定义的常量,不会触发此类的初始化。
代码示例:
public class NegativeReference0 {
public static class Parent {
public static String name = "Parent";
static {
System.out.println(Parent.class.getSimpleName() + " is referred!");
}
}
public static class Child extends Parent {
static {
System.out.println(Child.class.getSimpleName() + " is referred!");
}
}
public static void main(String[] args) {
System.out.println(Child.name);
}
}
运行结果:
Parent is referred! Parent
代码示例:
public class NegativeReference1 {
public static class Child {
static {
System.out.println(Child.class.getSimpleName() + " is referred!");
}
}
public static void main(String[] args) {
Child[] childs = new Child[10];
}
}
运行结果:
无输出
示例代码:
public class NegativeReference2 {
public static class Child {
public static final String name = "Child";
static {
System.out.println(Child.class.getSimpleName() + " is referred!");
}
}
public static void main(String[] args) {
System.out.println(Child.name);
}
}
运行结果:
Child
类加载器:类加载器负责加载程序中的类型(类和接口),并赋予唯一的名字予以标识。
BootstrapClassloader
是在 Java
虚拟机启动后初始化的。
BootstrapClassloader
负责加载 ExtClassLoader
,并且将 ExtClassLoader
的父加载器设置为 BootstrapClassloader。
BootstrapClassloader
加载完 ExtClassLoader
后,就会加载 AppClassLoader
,并且将 AppClassLoader
的父加载器指定为 ExtClassLoader
。
Class Loader | 实现方式 | 具体实现类 | 负责加载的目标 |
---|---|---|---|
Bootstrap Loader | C++ | 由C++实现 | %JAVA_HOME%/jre/lib/rt.jar 以及 -Xbootclasspath 参数指定的路径以及中的类库 |
Extension ClassLoader | Java | sun.misc.Launcher$ExtClassLoader | %JAVA_HOME%/jre/lib/ext 路径下以及 java.ext.dirs 系统变量指定的路径中类库 |
Application ClassLoader | Java | sun.misc.Launcher$AppClassLoader | Classpath 以及 -classpath 、 -cp 指定目录所指定的位置的类或者是 jar 文档,它也是 Java 程序默认的类加载器 |
层级结构:Java里的类装载器被组织成了有父子关系的层级结构。Bootstrap类装载器是所有装载器的父亲。
代理模式: 基于层级结构,类的代理可以在装载器之间进行代理。当装载器装载一个类时,首先会检查它在父装载器中是否进行了装载。如果上层装载器已经装载了这个类,这个类会被直接使用。反之,类装载器会请求装载这个类
可见性限制:一个子装载器可以查找父装载器中的类,但是一个父装载器不能查找子装载器里的类。
不允许卸载:类装载器可以装载一个类但是不可以卸载它,不过可以删除当前的类装载器,然后创建一个新的类装载器装载。
每个类装载器都有一个自己的命名空间用来保存已装载的类。当一个类装载器装载一个类时,它会通过保存在命名空间里的类全局限定名( FullyQualifiedClassName
) 进行搜索来检测这个类是否已经被加载了。
JVM
及 Dalvik
对类唯一的识别是 ClassLoaderid
+ PackageName
+ ClassName
,所以一个运行程序中是有可能存在两个包名和类名完全一致的类的。并且如果这两个类不是由一个 ClassLoader
加载,是无法将一个类的实例强转为另外一个类的,这就是 ClassLoader
隔离性。
为了解决类加载器的隔离问题, JVM
引入了双亲委托机制。
核心思想:其一,自底向上检查类是否已加载;其二,自顶向下尝试加载类。
当 AppClassLoader
加载一个 class
时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器 ExtClassLoader
去完成。
当 ExtClassLoader
加载一个 class
时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给 BootStrapClassLoader
去完成。
如果 BootStrapClassLoader
加载失败(例如在 %JAVA_HOME%/jre/lib
里未查找到该 class
),会使用 ExtClassLoader
来尝试加载;
如果 ExtClassLoader
也加载失败,则会使用 AppClassLoader
来加载,如果 AppClassLoader
也加载失败,则会报出异常 ClassNotFoundException
。
ClassLoader.class
loadClass():通过指定类的全限定名称,由类加载器检测、装载、创建并返回该类的 java.lang.Class
对象。
ClassLoader
通过loadClass()
方法实现了双亲委托机制,用于类的动态加载。
loadClass()
本身是一个递归向上调用的过程。
先通过 findLoadedClass()
方法从最底端类加载器开始检查类是否已经加载。
如果已经加载,则根据 resolve
参数决定是否要执行连接过程,并返回 Class
对象。
如果没有加载,则通过 parent.loadClass()
委托其父类加载器执行相同的检查操作(默认不做连接处理)。
直到顶级类加载器,即 parent
为空时,由 findBootstrapClassOrNull()
方法尝试到 BootstrapClassLoader
中检查目标类。
如果仍然没有找到目标类,则从 BootstrapClassLoader
开始,通过 findClass()
方法尝试到对应的类目录下去加载目标类。
如果加载成功,则根据 resolve
参数决定是否要执行连接过程,并返回 Class
对象。
如果加载失败,则由其子类加载器尝试加载,直到最底端类加载器也加载失败,最终抛出 ClassNotFoundException
。
findLoadedClass()
查找当前类加载器的缓存中是否已经加载目标类。
findLoadedClass()
实际调用了底层的native
方法findLoadedClass0()
。
findBootstrapClassOrNull()
查找最顶端
Bootstrap
类加载器的是否已经加载目标类。同样,findBootstrapClassOrNull()
实际调用了底层的native
方法findBootstrapClass()
。
findClass()
ClassLoader
是 java.lang
包下的抽象类,也是所有类加载器(除了 Bootstrap
)的基类, findClass()
是 ClassLoader
对子类提供的加载目标类的抽象方法。
注意:
BootstrapClassLoader
并不属于JVM
的层次,它不遵守ClassLoader
的加载规则,BootstrapclassLoader
并没有子类。
defineClass()
defineClass()
是ClassLoader
向子类提供的方法,它可以将.class
文件的二进制数据转换为合法的java.lang.Class
对象。
通过命令行启动时由 JVM
初始化加载;
通过 Class.forName()
方法动态加载;
通过 ClassLoader.loadClass()
方法动态加载。
Class.forName():把类的 .class
文件加载到 JVM
中,对类进行解释的同时执行类中的 static
静态代码块;
ClassLoader.loadClass():只是把.class文件加载到 JVM
中,不会执行 static
代码块中的内容,只有在 newInstance
才会去执行。
静态变量/静态代码块 -> 普通代码块 -> 构造函数
父类静态变量和静态代码块(先声明的先执行);
子类静态变量和静态代码块(先声明的先执行);
父类普通成员变量和普通代码块(先声明的先执行);
父类的构造函数;
子类普通成员变量和普通代码块(先声明的先执行);
子类的构造函数。
Parent.java
Children.java
Tester.java
测试结果:
测试结果表明: JVM
在创建对象时,遵守以上对象的初始化顺序。
在源码分析阶段,我们已经解读了如何实现自定义类加载器,现在我们开始怼自己的类加载器。
Step 1:定义待加载的目标类
Parent.java
和Children.java
。
Parent.java
package org.ostenant.jdk8.learning.examples.classloader.custom;
public class Parent {
protected static String CLASS_NAME;
protected static String CLASS_LOADER_NAME;
protected String instanceID;
// 1.先执行静态变量和静态代码块(只在类加载期间执行一次)
static {
CLASS_NAME = Parent.class.getName();
CLASS_LOADER_NAME = Parent.class.getClassLoader().toString();
System.out.println("Step a: " + CLASS_NAME + " is loaded by " + CLASS_LOADER_NAME);
}
// 2.然后执行变量和普通代码块(每次创建实例都会执行)
{
instanceID = this.toString();
System.out.println("Step c: Parent instance is created: " + CLASS_LOADER_NAME + " -> " + instanceID);
}
// 3.然后执行构造方法
public Parent() {
System.out.println("Step d: Parent instance:" + instanceID + ", constructor is invoked");
}
public void say() {
System.out.println("My first class loader...");
}
}
Children.java
package org.ostenant.jdk8.learning.examples.classloader.custom;
public class Children extends Parent {
static {
CLASS_NAME = Children.class.getName();
CLASS_LOADER_NAME = Children.class.getClassLoader().toString();
System.out.println("Step b: " + CLASS_NAME + " is loaded by " + CLASS_LOADER_NAME);
}
{
instanceID = this.toString();
System.out.println("Step e: Children instance is created: " + CLASS_LOADER_NAME + " -> " + instanceID);
}
public Children() {
System.out.println("Step f: Children instance:" + instanceID + ", constructor is invoked");
}
public void say() {
System.out.println("My first class loader...");
}
}
Step 2:实现自定义类加载器
CustomClassLoader
CustomClassLoader.java
public class CustomClassLoader extends ClassLoader {
private String classPath;
public CustomClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class<?> c = findLoadedClass(name); // 可省略
if (c == null) {
byte[] data = loadClassData(name);
if (data == null) {
throw new ClassNotFoundException();
}
return defineClass(name, data, 0, data.length);
}
return null;
}
protected byte[] loadClassData(String name) {
try {
// package -> file folder
name = name.replace(".", "//");
FileInputStream fis = new FileInputStream(new File(classPath + "//" + name + ".class"));
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int len = -1;
byte[] b = new byte[2048];
while ((len = fis.read(b)) != -1) {
baos.write(b, 0, len);
}
fis.close();
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
Step 3:测试类加载器的加载过程
CustomerClassLoaderTester.java
测试程序启动时,逐一拷贝并加载待加载的目标类源文件。
private static final String CHILDREN_SOURCE_CODE_NAME = SOURCE_CODE_LOCATION + "Children.java";
private static final String PARENT_SOURCE_CODE_NAME = SOURCE_CODE_LOCATION + "Parent.java";
private static final List<String> SOURCE_CODE = Arrays.asList(CHILDREN_SOURCE_CODE_NAME, PARENT_SOURCE_CODE_NAME);
static {
SOURCE_CODE.stream().map(path -> new File(path))
// 路径转文件对象
.filter(f -> !f.isDirectory())
// 文件遍历
.forEach(f -> {
// 拷贝后源代码
File targetFile = copySourceFile(f);
// 编译源代码
compileSourceFile(targetFile);
});
}
拷贝单一源文件到自定义类加载器的类加载目录。
protected static File copySourceFile(File f) {
BufferedReader reader = null;
BufferedWriter writer = null;
try {
reader = new BufferedReader(new FileReader(f));
// package ...;
String firstLine = reader.readLine();
StringTokenizer tokenizer = new StringTokenizer(firstLine, " ");
String packageName = "";
while (tokenizer.hasMoreElements()) {
String e = tokenizer.nextToken();
if (e.contains("package")) {
continue;
} else {
packageName = e.trim().substring(0, e.trim().length() - 1);
}
}
// package -> path
String packagePath = packageName.replace(".", "//");
// java file path
String targetFileLocation = TARGET_CODE_LOCALTION + "//" + packagePath + "//";
String sourceFilePath = f.getPath();
String fileName = sourceFilePath.substring(sourceFilePath.lastIndexOf("\\") + 1);
File targetFile = new File(targetFileLocation, fileName);
File targetFileLocationDir = new File(targetFileLocation);
if (!targetFileLocationDir.exists()) {
targetFileLocationDir.mkdirs();
}
// writer
writer = new BufferedWriter(new FileWriter(targetFile));
// 写入第一行
writer.write(firstLine);
writer.newLine();
writer.newLine();
String input = "";
while ((input = reader.readLine()) != null) {
writer.write(input);
writer.newLine();
}
return targetFile;
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
reader.close();
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
对拷贝后的 .java
源文件执行手动编译,在同级目录下生成 .class
文件。
protected static void compileSourceFile(File f) {
try {
JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager standardFileManager = javaCompiler.getStandardFileManager(null, null, null);
Iterable<? extends JavaFileObject> javaFileObjects = standardFileManager.getJavaFileObjects(f);
// 执行编译任务
CompilationTask task = javaCompiler.getTask(null, standardFileManager, null, null, null, javaFileObjects);
task.call();
standardFileManager.close();
} catch (Exception e) {
e.printStackTrace();
}
}
通过自定义类加载器加载 Children
的 java.lang.Class<?>
对象,然后用反射机制创建 Children
的实例对象。
@Test
public void test() throws Exception {
// 创建自定义类加载器
CustomClassLoader classLoader = new CustomClassLoader(TARGET_CODE_LOCALTION); // E://myclassloader//classpath
// 动态加载class文件到内存中(无连接)
Class<?> c = classLoader.loadClass("org.ostenant.jdk8.learning.examples.classloader.custom.Children");
// 通过反射拿到所有的方法
Method[] declaredMethods = c.getDeclaredMethods();
for (Method method : declaredMethods) {
if ("say".equals(method.getName())) {
// 通过反射拿到children对象
Object children = c.newInstance();
// 调用children的say()方法
method.invoke(children);
break;
}
}
}
保留 static
代码块,把目标类 Children.java
和 Parent.java
拷贝到类加载的目录,然后进行手动编译。
保留测试项目目录中的目标类 Children.java
和 Parent.java
。
测试结果输出:
测试结果分析:
我们成功创建了
Children
对象,并通过反射调用了它的say()
方法。 然而查看控制台日志,可以发现类加载使用的仍然是AppClassLoader
,CustomClassLoader
并没有生效。
查看 CustomClassLoader
的类加载目录:
类目录下有我们拷贝并编译的
Parent
和Chidren
文件。
分析原因:
由于项目空间中的
Parent.java
和Children.java
,在拷贝后并没有移除。导致AppClassLoader
优先在其Classpath
下面找到并成功加载了目标类。
注释掉 static
代码块(类目录下有已编译的目标类 .class
文件)。
移除测试项目目录中的目标类 Children.java
和 Parent.java
。
测试结果输出:
测试结果分析:
我们成功通过自定义类加载器加载了目标类。创建了
Children
对象,并通过反射调用了它的say()
方法。
至此,我们自己的一个简单的类加载器就完成了!
周志明,深入理解Java虚拟机:JVM高级特性与最佳实践,机械工业出版社
欢迎关注技术公众号: 零壹技术栈
本帐号将持续分享后端技术干货,包括虚拟机基础,多线程编程,高性能框架,异步、缓存和消息中间件,分布式和微服务,架构学习和进阶等学习资料和文章。