JVM之类加载器
前言
说起JVM大家肯定不陌生, 属实是面试八股文常考内容了, 但是身为初级程序员的我对其充满了熟悉和陌生, 由此诞生了本系列文章, 旨在学透JVM
本篇为 JVM基础全概况, 主要讲解 JVM是什么, 类加载机制以及自己实现一个类加载器
1. JVM是什么
jvm可以被理解为独立于当前系统的虚拟机, 是一种规范, 可以执行 class文件, 直接和操作系统进行交互
1.1 java和JVM的关系
比如我们使用 java语言写了以下代码
public static void main(String[] args) {
System.out.println("欢迎观看本篇文章");
}
那么当我们执行当前 java文件时, 该文件会被编译成后缀为 .class的字节码文件, 但是字节码不是机器码, 操作系统无法识别, 这个时候 JVM会识别字节码文件并与操作系统进行交互
所以, java字节码是沟通 java和 JVM之间的桥梁
当然, JVM也不是 java独有的, 有一些语言也是基于 jvm的
2. 类加载器
2.1 类加载器是什么
JVM的类加载是通过 CLassLoader及其子类来完成的
- 启动类加载器(Bootstrap Class-Loader):负责加载 JAVA_HOME/lib 目录的
- 扩展类加载器(Bootstrap Class-Loader): 负责加载 JAVA_HOME/lib/ext 目录的
- 应用程序类加载器(Bootstrap Class-Loader): 负责加载用户路径 classpath上的类库
- 自定义类加载器: 加载应用之外的类文件
2.2 类加载时机
类加载时机:
- 遇到 new, getStatic, putStatic, invokeStatic
- 对类进行反射调用时
- 初始化一个类时, 发现其父类还没有初始化, 要先初始化其父类
- 当虚拟机启动时, 会先将主类加载(main方法)
2.3 类加载的过程
类的生命周期:
- 加载
- JVM在该阶段将字节码从不同数据源转为二进制字节流加载到内存中, 并生成该类的 .class文件
类加载途径
- jar/war
- jsp生成的class
- 数据库中的二进制字节流
- 网络中的二进制字节流
- 动态代理生成的二进制字节流
- JVM在该阶段将字节码从不同数据源转为二进制字节流加载到内存中, 并生成该类的 .class文件
- 验证: 对二进制字节流进行校验, 保证其符合 JVM字节码规范, 主要分为以下几点
- 确保二进制字节流格式符合预期
- 方法调用的参数个数和类型是否正确
- 确保变量在使用之前被初始化
- 检查变量是否被赋予正确的值
- 是否所有方法都遵守访问控制关键字的限定
- 准备: JVM会在该阶段对类变量分配内存并初始化
- 解析: 将常量池中的符号引用转换为直接饮用
- 符号饮用: 是以一组符号来描述所引用的目标(编译时java类并不知道实际的地址, 以符号来替代)
- 直接饮用: 通过对符号饮用进行解析, 找到时机内存地址
- 初始化: 执行类构造器方法的过程
- 使用
- 卸载(销毁)
3. 自定义类加载器
目标: 自定义类加载器, 加载指定路径下的类 步骤:
- 新建一个类 Test.java
- 编译 Test.java到指定 lib目录
- 自定义类加载器 HeroClassLoader继承 ClassLoader;
- 重写 findClass()方法
- 调用 defineClass()方法
- 测试自定义类加载器
3.1 新建 Test类
直接写一个最简单的类, 里面有一个 ningXuan
方法
package com.ningxuan.demo;
public class Test {
public void ningXuan(){
System.out.println("宁轩Blog");
}
}
然后点击 idea右上角的小锤子进行编译
在编译target/class
下, 根据类文件的 package
路径就可以找到我们需要的 Test.class文件了
将这个 Test.class文件放到指定路径下, 也就是一会想让自定义类加载器去找类的路径, 我这边设置为 E:\lib\com\ningxuan\demo
E:\lib 是自定义的, 后面与类文件的包名相同
若不想使用上述方法, 也可以使用命令行 javac Test.java
3.2 自定义类加载器
就是重写ClassLoader
类的findClass
方法
package com.ningxuan.demo.juejin;
import java.io.*;
public class HeroClassLoader extends ClassLoader{
private String classpath;
public HeroClassLoader(String classpath){
this.classpath = classpath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException{
try{
// 输入流, 通过类的全限定名称加载文件到字节数组
byte[] classData = getData(name);
if (classData != null){
// defineClass方法将字节数组数据转为字节码对象
return defineClass(name, classData, 0, classData.length);
}
}catch (IOException ioException){
ioException.printStackTrace();
}
return super.findClass(name);
}
// 加载类的字节码数据
private byte[] getData(String className) throws IOException {
// 根据className生成对应 class路径
String path = classpath + File.separatorChar +
className.replace('.', File.separatorChar) + ".class";
try(InputStream in = new FileInputStream(path);
ByteArrayOutputStream out = new ByteArrayOutputStream()){
byte[] buffer = new byte[2048];
int len = 0;
while((len = in.read(buffer)) != -1 ) {
out.write(buffer, 0, len);
}
return out.toByteArray();
}catch (FileNotFoundException e){
e.printStackTrace();
}
return null;
}
}
3.3 测试自定义类加载器
import java.lang.reflect.Method;
public class TestClassLoader {
public static void main(String[] args) throws Exception{
// 自定义类加载器的加载路径
HeroClassLoader classLoader = new HeroClassLoader("E:\lib");
// 报名加类名
Class c = classLoader.loadClass("com.ningxuan.demo");
if (c != null){
Object o = c.newInstance();
Method method = c.getMethod("ningXuan", null);
method.invoke(o, null);
System.out.println(c.getClassLoader().toString());
}
}
}
执行结果如下, 成功打印
4. 双亲委派模型
双亲委派的意思是如果一个类加载器需要加载类,那么首先它会把这个类请求委派给父类加载器去完成,每一层都是如此。一直递归到顶层,当父加载器无法完成这个请求时,子类才会尝试去加载。这里的双亲其实就指的是父类,没有mother。父类也不是我们平日所说的那种继承关系,只是调用逻辑是这样。
双亲委派模型不是一种强制性约束,也就是你不这么做也不会报错怎样的,它是一种JAVA设计者推荐使用类加载器的方式。
同时使用双亲委派模型有一个很明显的好处, 那就是 java类随着他的类加载器一起具备了一种带有优先级的层次关系, 这对于保证 java程序的稳定运作很重要
本文内容到此结束了
如有收获欢迎点赞👍收藏💖关注✔️,您的鼓励是我最大的动力。
如有错误❌疑问💬欢迎各位大佬指出。
我是 宁轩 , 我们下次再见
转载自:https://juejin.cn/post/7248967382961291325