深入学习JVM: (1) 类的加载机制
一. 前言
开始写文章的第一个系列, 为了让自己学到的知识以及技术能有一个总结, 同时也希望能帮助到一些人. 其实关于jvm中类的加载机制, 相信大家早已耳熟能详, 本文仅是将自身的理解发表出来, 如有不对, 还请指正.
二. 类的加载过程
说到类加载, 就肯定是从java程序的启动开始的, 最常见的程序入口就是main方法, 我们的tomcat启动也是从main方法开始的. 那main方法都做了些什么呢?
public class Test {
public static viod main(String[] args) {
Test test = new Test();
}
}
如上面这一小段代码, 运行后, 会先由C++代码创建java虚拟机, 然后创建一个引导类加载器, 接着跨语言调用java代码创建Jvm启动器实例sun.misc.Launcher, 该类由引导类加载器加载, Launcher类负责创建其它的类加载器(本文会写到), 最后使用"其它"的类加载器将Test类加载到内存中, 当然. 这些都是C++实现的, 这里不作深究, 大概了解即可.
简洁说明就是:
Launcher构造方法部分源码.png
- 启动
- C++创建java虚拟机
- C++创建引导类加载器
- 引导类加载器加载Launcher类
- Launcher类加载其它类加载器(诸如: 扩展类加载器, 应用程序类加载器)
- 应用程序类加载器加载Test类
上面说的仅是宏观上的类加载过程, 作为一名Java工程师, 我们往往更关心classLoader.loadClass("com.xx.xx.Test"); 这行代码加载类的过程.
也就是面试题背到头晕的那五步: 加载, 验证, 准备, 解析以及初始化
那么这五步分别做了什么事情呢?
- 加载: 将java字节码加载到内存.
因为我的java类被编译成class文件后依然是在磁盘上的, 所以肯定需要先将这些字节码都通过I/O流加载到Jvm内存中才能运行. 在加载阶段会在内存中生成一个该类的java.lang.Class对象, 作为这个类的各类数据的访问入口.- 验证: 校验java字节码的正确性.
相信大部分人都使用过类似sublimeText的文本编辑器打开过java的class文件, 然后看到开头规整的cafe babe以及规整的16进制编码. 但如果你胡乱修改或删掉两个字符呢? 这时产生的后果就是这份代码跑不起来了. 所以, Jvm可能为了防止你扔一篇泰戈尔的散文诗让它加载, 就有了验证这一步.- 准备: 给类的静态变量分配内存, 并赋予默认值.
这里的默认值是数据类型的默认值, 并不是声明时给的默认值, 例如: int的默认值是0, boolean的默认值是false.- 解析: 将符号引用替换为直接引用.
当一个类被加载到内存后, 它的一切都还只是符号, 例如一个完整方法调用: test.test(), 加载后它可能是这样的, 符号1: test 符号2: . 符号3: () 当然它实际加载后并不是. 我想要表达的是, 这些符号计算机仍然看不懂, 还需要赋予意义. 什么是赋予意义呢? 比如test方法在内存中的地址, 虚拟机需要调用它的时候, 如何找到这个方法的字节码等等. 这一过程又叫做静态链接, 因为它是在类加载的时候完成的, 与之相对的还有动态链接, 这是在程序运行时完成的.- 初始化: 为静态变量赋予指定的值, 执行静态代码块.
这一步就很好理解了, 不过多解释.
另外, 除了支撑java运行的核心类库之外(如rt.jar等), 其余我们自己写的类大多是用到了之后才会加载到内存的.
怎样才算用到呢?
/**
* 印证下面代码很简单, 给Test02类添加一段静态代码块, 看是否执行即可;
* 因为类加载的最后一步初始化时, 会执行静态代码块
*/
// 不会加载
Test02 test = null;
// 会加载
Test02 test = new Test02();
三. 类加载器
Java中的类加载器分为如下几种
1. 引导类加载器(C++实现) : 负责加载支撑Java运行的位于jre/lib目录下的核心类库, 如rt.jar、charsets.jar等.
2. 扩展类加载器: 负责加载位于jre/lib/ext目录下的扩展类库.
3. 应用程序类加载器: 负责加载ClassPath路径下的类, 主要就是我们自己写的类.
4. 自定义类加载器: 通常是用来加载我们自定义路径下的类
乍一看, 哎? 这么多类加载器, 最根本的区别无非就是它们加载的类路径不同而已嘛. 至于为什么要这么做, 我觉得原因是很多的, 最显而易见的就是下面说的双亲委派机制
四. 双亲委派机制
双亲委派机制.png当一个类加载时, 采用哪个类加载器来加载呢? Jvm实现了一种由下到上, 再由上到下的加载机制, 也就是双亲委派机制.
由上图可以看出, 一个类加载时, 类加载器会先查看是否加载过这个类, 如果没有, 它不会直接加载, 而是直接扔给上层类加载器加载, 而上层类加载器仍然会先看是否加载过此类...直到委托到顶层的引导类加载器才会真正尝试到类加载器的类路径下查找该类, 如果没找到, 还给下层的类加载器加载. 如果一直到最底层的类加载仍然没有找到该类, 则会抛出一个ClassNotFoundException异常.
ClassLoader.loadClass部分源码.png一句话说明就是: 先给父类加载器加载, 加载不到, 再由子类加载器加载.
那为什么要有双亲委派机制呢?
- 保证沙箱安全:自己写的java.lang.String.class类不会被加载,防止核心API库被随意篡改
- 避免类的重复加载:当父层类加载器已经加载了该类时,就没有必要再由子类加载器再加载一次,保证被加载类的唯一性
此外还有一个全盘负责机制:
全盘负责机制是指: 当一个类加载器加载一个类时, 该类中所有引用的其它类, 也将由这个类加载器加载, 除非显式的指定了这个类的类加载器.
五. 自定类加载器并打破双亲委派机制
要实现自定义的类加载器其实很简单, 只需要两步即可:
- 继承ClassLoader类
ClassLoader类中有3个核心方法, 分别是: loadClass、findClass以及defineClass
loadClass(String name, boolean resolve):
实现了双亲委派机制
findClass(String name):
默认是空实现, 需要我们自己实现该方法
defineClass(String name, byte[] b, int off, int len):
实现了类加载的后四步: 验证 > 准备 > 解析 > 初始化- 重写findClass方法
基于上述ClassLoader类的介绍, 所以自定义类加载器主要是重写findClass方法
- 另外, 如果还需要打破双亲委派机制, 则需要第三步:
- 重写loadClass方法
说了这么多, 上个示例代码吧.
package jvm;
import java.io.FileInputStream;
import java.io.IOException;
/**
* 自定义类加载器, 简单实现
*
* @Author: Code养牧人
* @Date: 2021-02-21 19:15
*/
public class MyClassLoader extends ClassLoader {
// 类的根路径
private String classpath = "E:/test";
/**
* 将class字节通过io读取到内存中
*
* @param clazz 全类路径名
* @return 类的字节数组
*/
private byte[] loadClazz(String clazz) throws IOException {
// 将类路径转换为文件路径
String path = clazz.replaceAll("\\.", "/");
FileInputStream fileInputStream = new FileInputStream(classpath + "/" + path + ".class");
// 读取成字节数据组后返回
int length = fileInputStream.available();
byte[] bytes = new byte[length];
fileInputStream.read(bytes);
fileInputStream.close();
return bytes;
}
/**
* 重写findClass, 使其到我们自己指定的路径下加载类
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] clazz = loadClazz(name);
// 调用defineClass做完类加载的后四步: 验证, 准备, 解析, 初始化
return defineClass(name, clazz, 0, clazz.length);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
/**
* 打破双亲委派机制, 此方法直接复制了源码, 且尽可能只修改小部分, 目的在于提供一个思路
*/
@Override
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();
// 如果是我们自己的类路径, 直接findClass, 否则调用父加载器的loadClass
if (name.startsWith("jvm")) {
c = findClass(name);
} else {
c = this.getParent().loadClass(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;
}
}
/**
* 测试
*/
public static void main(String[] args) throws Exception {
MyClassLoader myClassLoader = new MyClassLoader();
// 要确保, 指定的磁盘路径下有该类的class文件
Class clazz = myClassLoader.loadClass("jvm.Test"); // Test为一个空类
Object obj = clazz.newInstance();
// 当应用程序类加载器的类路径以及自定义类路径下都存在Test类时, 如果输出的类加载器是jvm.MyClassLoader, 则打破了双亲委派机制
System.out.println(obj.getClass().getClassLoader());
}
}
好了, 今天的分享就到这里, 如果有写的不对的地方, 还望大家不吝赐教, 谢谢.