JVM 类加载器
一、前言
对于校招来说,jvm 的相关知识总是处在一个尴尬的境地,想要彻底了解jvm的知识,就需要阅读大量的文档,面试的时候,这些知识也就只会浓缩成短短几句话,而且对于学生时代遇到的中小型项目,一般到不来优化 JVM 的地步,也就无从谈起 JVM 实战了,没有实战,之前学习的知识很容易就遗忘了。
所以说,目前 JVM 最好的学习方式,是先通过阅读文档了解 JVM 的各个部分及其运作方式,然后总结成精量化的博客,后期可以通过不断回顾博客加强记忆。(成为文科工程师也没有办法,不这么做别人就把你卷死)
所以,这篇博客是我通过阅读大量文档总结成的册子,我会尽量以问答的形式编写,主要是为了方便应付面试,不建议拿来做 JVM 的入门。想要入门 JVM ,可以去翻阅下面这篇学习笔记:
https://www.pdai.tech/md/java/jvm/java-jvm-classload.html#%E7%B1%BB%E7%9A%84%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F
本篇主要涉及的内容是类加载器相关的知识,其在 JVM 中的位置,我也在下图标出了:
二、类加载器知识点整理
1、谈谈类的生命周期
类的生命周期分为这几个步骤:
- 加载:类加载器加载的,是字节码文件,我们写的 .java 文件,会通过编译器,编译为 .class 文件,即字节码文件。加载的时候,先是使用类的全限定名,获取字节码文件,然后将字节码文件表示的静态结构转换为方法区的运行时数据结构,最后,在堆区生成 对应的 java.lang.Class 对象
- 连接:
连接分为下面几个字部分:
**验证:**验证加载的字节码文件是否合规,确保加载的字节码文件不会破坏虚拟机的安全性
**准备:**这阶段,为 static 变量分配内存空间,并将其初始化为 0 (其具体的数值,是在初始化阶段进行的)
**解析:**将符号引用转换为直接引用(符号引用是用字符表示引用的目标;直接引用就是将符号引用定位到目标了<这就要求一定要加载到内存中>)
- 初始化:
初始化,即对类的静态变量进行初始化,在连接的准备阶段,会将静态变量,先赋予 0 的初值,这里的初始化就是将我们自己定义的值附上去
- 使用:
类访问方法区内的数据结构的接口, 对象是Heap区的数据。
- 卸载:
jvm 生命周期结束的情况如下:
1、执行 System.exit()
2、程序正常执行完毕
3、程序出现异常或错误
4、因为 OS 错误导致 JVM 进程结束
2、谈谈 JVM 类加载器的分类
类加载器可以分为下面三类:
- BootstrapClassloader:
所有以 java.* 开头的文件,都会由其加载
其是使用 c++ 编写的,我们没法直接获取到
- ExtensionCLassLoader:
从java.ext.dirs加载,或从jdk的安装目录的jre/lib/ext子目录(扩展目录)下加载类库,如果程序员将jar包导入到这些目录中,扩展类加载器也会进行加载
- AppliactionCLassloader:
加载我们自己写的 .java
(这里要注意,虽然我们有父子类加载器的称呼,但子类加载器并不是使用继承的方式继承父加载器的,而是使用组合的方式,即子加载器有一个父加载器的属性,其关系可以参考 controller 中调用 service)
3、双亲委派的过程
- AppClassLoader 加载时,先尝试交给 ExtCLassLoader 加载
- ExtCLassLoader 加载时,先尝试交给 BootStrapClassLoader 加载
- BootstrapClassLoader 加载失败,再交给 ExtCLassLoader 加载
- ExtCLassLoader 加载失败,再交给 AppClassLoader 进行加载
总的来说,就是先往上踢皮球,上面解决不了,自己再尝试加载
4、为什么需要双亲委派
1、防止系统中出现多分同样的字节码(举例:我们自己写一个 java.lang.String,不会被加载)
2、保证 java 程序安全稳定的运行(假设通过网路传来一个 java.lang.String 的类,通过双亲委派传到了 BootstrapClassLoader ,这个时候启动类加载器发现这个类已经被加载过了,就不会去加载这个网络传来的类,从而保证核心类库不会被篡改)
5、如何破坏双亲委派机制
继承 ClassLoader
类,重写 findClass()
方法
package top.faroz.jvm.classloader;
import java.io.*;
public class MyClassLoader extends ClassLoader {
// 加载文件的路径
private String root;
// 重写 findCLass 方法
// 在原来的 findClass 方法中,会先查询类有没有被加载
// 如果没有加载的话,就会先交给父类加载器进行加载
// 我们重写 findClass 方法的话,就完全不会去鸟父类加载器了,直接走自己
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = loadClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] loadClassData(String className) {
// 获取类所在的绝对地址
String fileName = root + File.separatorChar
+ className.replace('.', File.separatorChar) + ".class";
// 读取对应的字节码文件
try {
InputStream ins = new FileInputStream(fileName);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 1024;
byte[] buffer = new byte[bufferSize];
int length = 0;
while ((length = ins.read(buffer)) != -1) {
baos.write(buffer, 0, length);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public String getRoot() {
return root;
}
public void setRoot(String root) {
this.root = root;
}
public static void main(String[] args) {
MyClassLoader classLoader = new MyClassLoader();
classLoader.setRoot("D:\\temp");
Class<?> testClass = null;
try {
testClass = classLoader.loadClass("top.faroz.jvm.classloader.Test2");
Object object = testClass.newInstance();s
System.out.println(object.getClass().getClassLoader());
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
6、为什么需要破坏双亲委派?有哪些框架破坏了双亲委派?
为什么:
有的时候,我们加载的字节码文件,可能需要通过网络传输,或者这些字节码文件被进行了加密,这个时候,默认的类加载器就无法加载这些字节码文件,就需要我们自定义类加载器来实现类加载的功能
有那些框架:
JDBC 就是通过破坏双亲委派机制
我们知道,JDBC 是由 java 提供接口,然后由不同的数据库厂商进行实现的
那MySQL举例,DriverManager 类中要加载各个实现了Driver接口的类,然后进行管理,但是DriverManager位于 $JAVA_HOME中jre/lib/rt.jar 包,由BootStrap类加载器加载,而其Driver接口的实现类是位于服务商提供的 Jar 包
又因为**类加载器有如下机制:**当被装载的类引用其他类的时候,虚拟机就会用装载第一个类的类加载器,去装载被引用的类
拿到 JDBC 中,就是会尝试用 BootStrapClassloader 去装载由第三方厂商实现的类,这显然是不能成功的,这里我们必须使用子类加载器去加载,所以破坏了双亲委派模型