浅谈Java自定义类加载器及JVM自带的类加载器之间的交互关系
作者:小萝莉_Lolita 发布时间:2021-09-12 23:37:24
JVM自带的类加载器:
其关系如下:
其中,类加载器在加载类的时候是使用了所谓的“父委托”机制。其中,除了根类加载器以外,其他的类加载器都有且只有一个父类加载器。
关于父委托机制的说明:
当生成 一个自定义的类加载器实例时,如果没有指定它的父加载器,那么系统类加载器将成为该类加载器的父类加载器
下面,自定义类加载器。自定义的类加载器必须继承java.lang.ClassLoader类
import java.io.*;
public class MyClassLoader extends ClassLoader {
private String name; //类加载器的名字
private String path; //加载类的路径
private final String fileType = ".class"; //class文件的扩展名
public MyClassLoader(String name){
super(); //让系统类加载器成为该类加载器的父 类加载器,该句可省略不写
this.name = name;
}
public MyClassLoader(ClassLoader parent,String name){
super(parent); //显示指定该类加载器的父 类加载器
this.name = name;
}
@Override
public String toString() {
return this.name;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
//实现自定义的类加载器必须重写findClass方法,否则ClassLoader类中的findClass()方法是抛出了异常
@Override
public Class findClass(String name)throws ClassNotFoundException{
byte[] data = this.loadClassData(name);
return this.defineClass(name,data,0,data.length);
}
private byte[] loadClassData(String name){
InputStream is = null;
byte[] data = null;
ByteArrayOutputStream baos = null;
try {
this.name = this.name.replace(".","\\"); //com.dream.it---->com\dream\it
is = new FileInputStream(new File(path + name + fileType));
int ch;
while(-1 != (ch = is.read())){
baos.write(ch); //将数据写入到字节数组输出流对象中去
}
data = baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}finally {
try {
is.close();
baos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return data;
}
public static void main(String[] args) throws Exception {
MyClassLoader loader1 = new MyClassLoader("loader1");
loader1.setPath("d:/myapp/serverlib/");
MyClassLoader loader2 = new MyClassLoader(loader1,"loader2"); //loader1作为loader2的父 类加载器
loader2.setPath("d:/myapp/clientlib");
MyClassLoader loader3 = new MyClassLoader(null,"loader3");//父类加载器为null,表明其父类加载器为根类加载器
loader3.setPath("d:/myapp/otherlib");
test(loader2);
test(loader3);
}
public static void test(ClassLoader cl) throws Exception {
Class clazz = cl.loadClass("Sample");
Object object = clazz.newInstance();
}
}
附上findClass()方法的JDK说明
protected Class<?> findClass(String name) throws ClassNotFoundException
Finds the class with the specified binary name.
This method should be overridden by class loader
implementations that follow the delegation model
for loading classes, and will be invoked by the
loadClass method after checking the parent class
loader for the requested class. The default
implementation throws a ClassNotFoundException.
大致说明一下意思:通过指定的name来查找类。该方法应该被类加载器的实现类重写,从而能够保证在加载类的时候可以遵循委托机制模型。在loadClass()方法(该方法是由JVM调用的)中,检查其父类加载器之后,该方法再被调用去加载请求的类。默认该方法的实现是抛出了一个ClassNotFoundException异常。
其实,所谓的加载类,无非就是读取.class文件到内存中,所以在findClass()方法中,loadClassData()方法用于读取.class文件的数据,并返回一个字节数组。然后利用ClassLoader类的defineClass()方法将字节数组转换为Class对象。
上述自定义的类加载器loader1,loader2,loader3及JVM自带的类加载器之间的关系如下:
对于各个类加载器,系统的类加载器是从环境变量classpath中读取.class文件实现类的加载;loader1是从目录d:/myapp/serverlib/下读取.class文件;loader2是从目录d:/myapp/clientlib/下读取.class文件,loader3是从目录d:/myapp/otherlib/下读取.class文件
执行结果:
此处我们分析一下出现这种执行结果的原因:
当执行loader2.loadClass(“Sample”)时先由它上层的所有父类加载器尝试加载Sample类。
loader1从D:\myapp\serverliv目录下成功加载了Sample类,所以loader1是Sample类的定义类加载器,loader1和loader2是Sample类的初始类加载器。
当执行loader3.loadClass(“Sample”)时,先由它上层的所有父类加载器尝试加载Sample类。
loader3的父加载器为根类加载器,它无法加载Sample类,接着loader3从D:\myapp\otherlib目录下成功加载Sample类,所以loader3是Sample类的定义类加载器及初始类加载器。
在Sample类中主动使用了Dog类(new Dog()),当执行Sample类的构造方法中的new Dog()语句时,JVM需要先加载Dog类,到底用哪个类加载器家在呢?
从上述的打印结果中可以看出,加载Sample类的loader1还加载了Dog类,JVM会用Sample类的定义类加载器去加载Dog类,加载过程中也同样采用了父亲委托机制。
为了验证这一点,可以吧D:\myapp\serverlib目录下Dog.class文件删除,然后在D:\myapp\syslib目录下存放一个Dog.class文件,此时打印结果如下:
Sample:loader1
Dog:sun.misc.Launcher$AppClassLoader@1b84c92
Sample:loader3
Dog:loader3
由此可见,当由loader1加载的Sample类首次主动使用Dog类时,Dog类由系统类加载器加载,如果把D:\myapp\serverlib和D:\myapp\syslib目录下的Dog.class文件都删除,然后在D:\myapp\client目录下存放一个Dog.class文件。
此时文件结构如下图所示:
当Loader1加载Sample类首次主动使用Dog类时,由于loader1及其父类加载器都无法加载Dog类,因此test(loader2)会抛出ClassNotFoundExcption.
这又是因为什么原因呢?
这又牵扯到命名空间的问题。
同一个命名空间内的类时相互可见的。
子加载器的命名空间包含所有父类加载器的命名空间,因此由子加载器加载的类能看见父类加载器加载的类。例如系统类加载器加载的类能看见根类加载器加载的类。由父加载器加载的类不能看见子加载器加载的类。
如果两个加载器之间没有直接或间接的父子关系,那么它们各自加载的类相互不可见。
对于上述问题,loader1可以加载Sample类,而Dog类只能由loader2加载Dog类,loader1是Loader2的父类加载器,父加载器loader1加载的类Sample不能看见子加载器loader2加载的类Dog,所以会抛出异常。
对于上述实例中的main方法,我们不调用test方法,换成如下代码
Class clazz = loader1.loadClass("Sample");
Object obj = clazz.newInstance();
Sample sample = (Sample)obj;
System.out.println(sample.v1);
MyClassLoader类由系统类加载器加载,而Sample类由loader1类加载器加载,所以MyClassLoader类看不见Sample类。在MyClassLoader类的main方法中使用Sample类,会导致NoClassFoundError错误。
当两个不同命名空间内的类相互不可见时,可采用Java反射机制来访问对象实例的属性和方法。
将上述代码修改:
Class clazz = loader1.loadClass("Sample");
Object obj = clazz.newInstance();
Field field = clazz.getField("v1");
int v1 = field.getInt(obj);
System.out.println(v1);
此时,可以获取到对象中的v1属性值。利用反射机制,我们可以跨越这种命名空间的限制。
补充:
命名空间:
运行时包:
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。如有错误或未考虑完全的地方,望不吝赐教。
来源:https://blog.csdn.net/MDreamlove/article/details/79212420
猜你喜欢
- Java 内存划分: 在Java内存分配中,java将内存分为:方法区,堆,虚拟机栈,本地方法栈,程序计
- using System; using System.Collections.Generic; using System.Text; usi
- 一、堆排序1、什么是堆排序(1)堆排序:堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构
- Java-关键字:final1 .final可以用来修饰的结构:类、方法、变量2.final 用来修饰一个类:此类不能被其他类所继承比如:S
- 前言最近数据库大作业要连接数据库,看了很多博客文章终于连接好了,但是没有看到一篇博客是能直接连接完成的,所以在这记录一下希望能帮助大家sql
- Nacos简介Nacos 英文全称为 Dynamic Naming and Configuration Service,是一个由阿里巴巴团队
- 一、邮件协议MTA 和 MDA 这样的服务器软件通常是现成的,我们通常不会关心这些邮件服务器的内部是如何运行 的。更多的需求场景,是需要发送
- 引导语线程池我们在工作中经常会用到。在请求量大时,使用线程池,可以充分利用机器资源,增加请求的处理速度,本章节我们就和大家一起来学习线程池。
- class MyThreadScopeData { // 单例 &nbs
- jmap:Java内存映像工具jmap(Memory Map for Java)命令用于生成堆转储快照(一般称为heapdump或dump文
- 为什么使用logback记得前几年工作的时候,公司使用的日志框架还是log4j,大约从16年中到现在,不管是我参与的别人已经搭建好的项目还是
- @ConditionalOnMissingBean,它是修饰bean的一个注解,主要实现的是,当你的bean被注册之后,如果而注册相同类型的
- 背景SpringBoot 版本<parent> <groupId>org.springfr
- 使用Spring data JPA开发已经有一段时间了,这期间学习了一些东西,也遇到了一些问题,在这里和大家分享一下。前言:Spring d
- mybatis-plus 新增/修改 自动填充指定字段1.需要修改的字段在模型类上添加@TableField(fill = FieldFil
- java与JSON数据的转换实例详解JSON与JAVA数据的转换(JSON 即 JavaScript Object Natation,它是一
- SessionFactory在Hibernate中实际上起到了一个缓冲区的作用 他缓冲了HI
- 开篇:我们将前面的springboot整合H2内存数据库,实现单元测试与数据库无关性提供的Restful服务注册到spring cloud的
- 1.线程与进程进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,线程则是一个实体,一个进程中至少有一个线程,是CPU
- 本文实例讲述了Java实现的校验银行卡功能。分享给大家供大家参考,具体如下:步骤:首先区分借记卡和信用卡,然后就是校验卡号,最后根据银联Bi