类加载过程
2020年真是一个灾祸重生的一年,开年的春节假期因为肺炎疫情一直延续,弄得人们都人心惶惶,只能憋在家里写写博客打发时间,还是希望疫情早点结束,武汉加油!中国加油!
下面言归正传,本系列jvm文章主要从类加载过程、jvm内存模型、jvm垃圾收集、jvm优化等讲解本人对于jvm一些了解,希望能帮助到大家,若有不对之处欢迎,留言一起讨论。
上面是JVM虚拟机的一个概览图,本篇博客主要讲解类装载子系统方面的知识。
上图是类的加载过程
多个java文件经过编译打包生成可运行的jar包(例如S pring Boot项目),最终由java命令运行某个主要的main函数启动程序,这里首先需要通过 类加载器 把 主类 加载到JVM。主类在运行过程中如果使用到其他类,会逐步加载这些类。
注意 jar包中的类不是一次性全部加载的,是使用到才加载的。
类加载到jvm的步骤
- 加载
- 验证
- 准备
- 解析
- 初始化
- 使用
- 卸载
加载 : 在硬盘上查找并通过IO流读入字节码文件,使用到类时才会加载,例如 new 对象等等。
验证: 校验字节码文件的正确性。
准备: 给类的静态变量分配内存,并赋予默认值 。
解析: 将符号引用替换为直接引用,该阶段会把一些静态方法替换为指向数据所存内存的指针或句柄(直接引用),这里所谓的静态链接过程(类加载期间完成),动态链接过程是在程序运行期间完成的将符号引用替换为直接引用。
初始化: 对类的 静态变量初始化设定的值,执行静态代码块。
相信单纯的讲一下这个过程还是不是很容易理解,下面对每个步骤在进行详细的说明一下。
加载就不多说了,着重说明一下验证 步骤,验证的是字节码文件(.class文件)的正确性,那么什么是class文件?具体要验证什么呢?首先我们要明白一下几个概念。
1.什么是Class文件
Java字节码类文件(.class)是Java编译器编译Java源文件(.java)产生的“目标文件”。它是一种8位字节的二进制流文件, 各个数据项按顺序紧密的从前向后排列, 相邻的项之间没有间隙, 这样可以使得class文件非常紧凑, 体积轻巧, 可以被JVM快速的加载至内存, 并且占据较少的内存空间(方便于网络的传输)。
class文件中的信息是一项一项排列的, 每项数据都有它的固定长度, 有的占一个字节, 有的占两个字节, 还有的占四个字节或8个字节, 数据项的不同长度分别用u1, u2, u4, u8表示, 分别表示一种数据项在class文件中占据一个字节, 两个字节, 4个字节和8个字节。 可以把u1, u2, u3, u4看做class文件数据项的“类型” 。
2.Class文件结构
下面就是我们的Class文件通过HEX-Editor插件编码后的结果
链接: .class文件具体解释说明参考 理解了什么是class文件,我们继续来说 验证 步骤到底验证什么。
- 文件格式的验证
- 是否以ca fe ba be开头
- 版本号是否合理(编译版本和运行版本是否一致)
- 元数据的验证
- 是否有父类
- 是否继承final类
- 非抽象类实现了所有的抽象方法
- 字节码验证
- 运行检查
- 栈数据类型和操作码数据参数吻合
- 跳转指令指定到合理位置
- 符号引用验证
- 常量池中描述类是否存在
- 访问的方法或字段是否存在且有足够的权限
讲完了验证步骤,准备和初始化步骤都是操作的静态变量,举个例子就很好明白了。
public class Test{
private static User u = new User();
private static int tem = 100;
private static final int v = 1;
}
准备阶段就是将u设置成初始值null,将tem设置成初始值0。
初始化阶段将u 在初始化成 new User(),将tem设置成100。
对于static final类型,在准备阶段就会被赋上正确的值。
什么是类加载器
类的加载过程我们知道,是通过类加载器 把主类加载到jvm中的,那么到底什么是类加载器呢?
顾名思义,类加载器(class loader)用来加载 Java 类到 Java 虚拟机中。一般来说,Java 虚拟机使用 Java 类的方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成 java.lang.Class类的一个实例。每个这样的实例用来表示一个 Java 类。通过此实例的 newInstance()方法就可以创建出该类的一个对象。实际的情况可能更加复杂,比如 Java 字节代码可能是通过工具动态生成的,也可能是通过网络下载的。
基本上所有的类加载器都是 java.lang.ClassLoader类的一个实例。
虚拟机加载类有两种方式,一种方式ClassLoader.loadClass()方法,另一种是使用反射API,Class.forName()方法,其实Class.forName()方法内部也是使用的ClassLoader。
类加载器间的关系
由上图可知:
一个类的加载顺序是:自顶向下
一个类的检查顺序是:自底向上
这几种加载器不是继承关系,而是委托关系。那么类加载器是如何工作的呢?下面我们就从ClassLoader源码的loadClass()方法来进行解析。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
//首先检查这个classsh是否已经加载过了
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// c==null表示没有加载,如果有父类的加载器则让父类加载器加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//如果父类的加载器为空 则说明递归到bootStrapClassloader了
//bootStrapClassloader比较特殊无法通过get获取
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
//如果bootstrapClassLoader 仍然加载完成后没有找到此类,则递归回来,尝试下级加载器调用findClass()方法去加载
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;
}
}
private Class<?> findBootstrapClassOrNull(String name)
{
if (!checkName(name)) return null;
return findBootstrapClass(name);
}
// return null if not found
private native Class<?> findBootstrapClass(String name);
从ClassLoader源码的loadClass()方法中,我们可以看出,类加载器优先调用父类的loadClass()方法,优先使用父类进行加载,父类加载不到在调用子类进行加载。
这里的类加载其实就是一种双亲委派机制,加载某个类时会先委托父类加载器寻找目标类,找不到在委托上层父类加载器加载,如果所有父类加载器在自己的路径下都找不到目标类,则在自己的类路径下查找并载入目标类。
如何实现自定义类加载器
实现自定义的类加载器只需要继承ClassLoader类,并重写findClass方法即可。
public class CustomClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] result = getClassFromCustomPath(name);
if (result == null) {
throw new FileNotFoundException(name);
} else {
// defineClass方法将字节码转化为类
return defineClass(name, result, 0, result.length);
}
} catch (Exception e) {
e.printStackTrace();
}
throw new ClassNotFoundException(name);
}
public static void main(String[] args) {
CustomClassLoader customClassLoader = new CustomClassLoader("D:/ClassLoader");
try {
Class<?> clazz = customClassLoader.loadClass("cn.com.study.classLoader.Log");
Object obj = clazz.newInstance();
Method method = clazz.getDeclaredMethod("sout", null);
method.invoke(obj,null);
System.out.println(clazz.getClassLoader().getClass().getName());
} catch (Exception e) {
e.printStackTrace();
}
}
}
输出结果
这里虽然是自定义了类加载器,但是真正加载Log类使用的加载器是AppClassLoader,因为Log类在classPath下也是存在在,此时调用的loadClass()方法是父类的,所以类加载是存在双亲委派机制的,所以AppClassLoader是优先加载的。
那么如何能实现不通过AppClassLoader加载而使用自己的类加载器加载,这就需要打破双亲委派机制,需要重写loadClass()方法
@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) {
// 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;
}
}
总结:实现自定义类加载器,如果不需要打破双亲委派机制,只需要重写findClass方法,若需要打破双亲委派机制还需要重写loadClass方法。
JVM为什么要实现双亲委派机制
1.沙箱安全机制: 自己写的java.lang.String.class类是不会被加载的,这样可以防止核心API库被随意篡改
2.避免类的重复加载: 当父类已经加载了该类后,就没有必要子ClassLoader再加载一次,保证被加载的类只被加载一次。