类加载器的作用与位置
在java字节码文件交给JVM运行时,需要类加载器子系统进行加载,类加载器子系统主要分为
三个阶段加载 链接 初始化具体可以看我前两篇博客这里就不再赘述
加载阶段主要有三个加载器引导类加载器 扩展类加载器 系统类加载器完成
他们的作用就是加载class文件到内存中,具体来说是把数据结构等信息存放在方法区,生成Class模板存放在堆区
三个加载器的关系
引导类加载器由c/c++代码编写,扩展类加载器和系统类加载器由java编写都继承于ClassLoader,他们三者属于层级关系,并不是继承
ClassLoader classLoader = son.class.getClassLoader();
System.out.println(classLoader);
System.out.println(classLoader.getParent());
Parent只是类的属性用于实现双亲委派并非是使用extends的继承关系
这里为类的加载器的parent字段赋值
扩展类和系统类加载器的加载
在 Java 虚拟机中,扩展类加载器和系统类加载器都是由启动类加载器加载的。
启动类加载器(Bootstrap Class Loader)是 Java 虚拟机的一部分,负责加载 Java 的核心类库,如 java.lang.Object
和 java.lang.String
等。启动类加载器通常是由 Java 虚拟机的实现提供,它是用本地代码实现的,不是一个普通的 Java 类。
扩展类加载器(Extension Class Loader)和系统类加载器(System Class Loader)都是普通的 Java 类,它们实际上是由启动类加载器加载的。启动类加载器在启动 Java 虚拟机时就已经存在,它会负责加载扩展类加载器和系统类加载器。这样就形成了一个层次结构,启动类加载器位于最顶层,扩展类加载器和系统类加载器位于其下。
扩展类加载器初始化过程
扩展类加载器的初始化参数ClassLoader为null这也就是为什么通过扩展类加载器获得(父类)加载器结果为null
系统类加载器初始化过程
可以看到var1就是上一步创建的扩展类加载器,这也是为什么系统类加载器的(父类)是扩展类加载器
类的加载过程
一个类加载器只会加载一次类,每个加载器会存放加载过的类的全类名,这样保证每个类只加载一次,以自定义类为例子,当使用new 关键字时,先由系统类加载器向上访问扩展类加载器,然后由扩展类加载器向上查看系统类加载器能否加载,当系统类加载器发现不能加载后就由扩展类加载器尝试加载,由于是自定义类,扩展类加载器也不能加载,接着再由系统类加载器加载。每次加载类时都是先判断上级加载器是否可以加载,如果不能再一次向下直到找到合适的类加载器。
这就是双亲委派
ClassLoader的loadClass方法
这就是上述双亲委派的实现机制
如果resolve为true就会进行解析,把符号引用转为直接引用
双亲委派机制
优势:
避免了类的重复加载
保护了java的核心API避免被篡改
劣势
-
灵活性受限:双亲委派机制对于自定义类加载器的灵活性有一定的限制。由于双亲委派机制会优先委派给父类加载器加载类,自定义类加载器无法覆盖或修改父类加载器加载的类。这在某些特定的场景下可能会限制了类加载的灵活性和自定义性。
-
安全性问题:尽管双亲委派机制可以提供一定的安全性,防止恶意类的加载,但在某些情况下也可能引发安全性问题。如果恶意代码能够伪装成一个被父类加载器加载的类,它可能会绕过子类加载器的安全检查,从而在应用程序中执行恶意操作。
-
类加载器冲突:在某些情况下,双亲委派机制可能会导致类加载器冲突。当多个类加载器同时加载同一个类时,由于双亲委派机制的存在,可能会导致类的多次加载或类加载器之间的冲突。这可能会引发类的不兼容性或其他运行时问题。
-
动态更新的困难:双亲委派机制在类加载时会优先从父类加载器中寻找已加载的类,这可能会导致动态更新的困难。即使应用程序中存在新版本的类文件,由于父类加载器已加载了旧版本的类,新版本的类可能无法被加载和使用,除非重新启动应用程序或使用特殊的技术来实现动态更新。
-
检查类是否加载的过程是单向的
双亲委派的破坏
第一次:
由于jdk1.2之前没有实现双亲委派机制,在1.2时引入双亲委派代码的实现但是ClassLoader是一个抽象类,如果实现的时候重写loadClass方法就会导致双亲委派的失效,所以又定义了一个finClass方法,在保持双亲委派的同时用户还能自己定义findClass方法
第二次
主要为了解决双亲委派模型的缺陷--类的单项加载,引入了线程上下文类加载器;
这个加载器可以通过Thread类的setContextClassLoader方法进行设置,如果创建线程时未设置就继承自父线程,如果都没设置就默认是应用程序类加载器
第三次
主要为了解决java代码无法代码热替换,模块部署,动态更新问题。由IBM主导的OSGI实现模块化热部署的关键就是类的加载器的实现每个模块都有自己的加载器,当更新时连同模块和类的加载器一起换掉就实现了代码的热部署
类加载器的重写
一:如果要改变双亲委派就重写loadClass方法
二:使用双亲委派就重写findClass方法
public class MyClassLoad extends ClassLoader{
String byteCode;
public MyClassLoad(ClassLoader parent , String byteCode){
super(parent);
this.byteCode = byteCode;
}
public MyClassLoad(String byteCode){
this.byteCode = byteCode;
}
@Override
protected Class findClass(String name) throws ClassNotFoundException {
String url = byteCode+name+".class";
Class aClass = null;
BufferedInputStream ios = null;
ByteArrayOutputStream ots = null;
try {
ios = new BufferedInputStream(new FileInputStream(url));
byte[] bytes = new byte[1024];
int led = 0;
ots = new ByteArrayOutputStream();
while ((led =ios.read(bytes)) != -1) {
ots.write(bytes, 0, led);
}
byte[] bytes1 = ots.toByteArray();
aClass = defineClass(null,bytes1, 0, bytes1.length);
}catch (Exception e){
e.printStackTrace();
}finally {
if(ios!=null) {
try {
ios.close();
ots.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
return aClass;
}
}
接下来解决代码热部署
主方法不停的加载类并创建r的实例对象,要先编译r.java的文件
public class r {
public r(){
System.out.println("开始");
}
}
此时控制台结果
此时修改r的构造方法输出“开始zhong”
此时在使用前端编译器javac编译r.java文件( javac -encoding UTF-8 r.java解决编码问题)
这样就实现了代码的热部署