文章目录
一、类加载器的分类【理解】
二、双亲委派模型【理解】
三、小节
还记得类加载器的定义、作用、类加载的完整过程吗?如果忘记可以到这里重新温习: 类加载器 超详解:什么是类加载器,类加载器作用及应用场景,类加载时机,类加载的完整过程,类加载器分类 ,此处重点讲述类加载器的分类。
类加载器分为两类,一类是Java代码中实现的,一类是Java虚拟机底层源码实现的。
类加载器的设计,JDK8和8之后的版本差别较大(JDK9之后,出现了模块化设计)。
首先来看JDK8及之前的版本,JDK8及之前的版本中默认的类加载器有如下几种:
代码演示
public class ClassLoaderClassDemo1 {
public static void main(String[] args) {
//获取应用程序类加载器/系统类加载器 sun.misc.Launcher$AppClassLoader@18b4aac2
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
//获取应用程序类加载器的父加载器 --- 扩展类加载器 sun.misc.Launcher$ExtClassLoader@6a6824be
ClassLoader classLoader1 = systemClassLoader.getParent();
//获取扩展类加载器的父加载器 --- 启动类加载器 null
ClassLoader classLoader2 = classLoader1.getParent();
System.out.println("应用程序类加载器" + systemClassLoader);
System.out.println("扩展类加载器" + classLoader1);
System.out.println("启动类加载器" + classLoader2);
}
}
补充:Arthas中类加载器相关的功能
Arthas是程序员开发运维必不可少的一个工具,还记得如何使用吗?忘记的话,可以参考 Java字节码文件、组成、详解、分析;jclasslib插件、阿里arthas工具;Java注解
类加载器的详细信息可以通过classloader命令查看:
classloader
查看classloader的继承树,urls,类加载信息,使用classloader去getResource
第1列为类加载名称,第2列为当前类加载器在内存中实例个数,第3列为当前类加载器加载了多少个类。
在IDEA项目右侧External Libraries中也能找到对应jar包,这就是启动类加载器所加载的。
/**
* 启动类加载器案例
*/
public class BootstrapClassLoaderDemo {
public static void main(String[] args) throws IOException {
//通过String类获取到它的类加载器。String.class 取到当前堆上的class对象
ClassLoader classLoader = String.class.getClassLoader();
System.out.println(classLoader); //输出null
//让程序不再退出
System.in.read();
}
}
这段代码通过String类获取到它的类加载器并且打印,本来以为是Bootstrap ClassLoader
,结果是null
。这是因为启动类加载器在JDK8中是由C++语言来编写的,在Java代码中去获取既不适合也不安全,所以才返回null
(String类确实是由启动类加载器加载的,但是启动类加载器由虚拟机底层实现、没有存在Java代码中,无法通过Java代码获取底层的虚拟机启动类加载器)
在Arthas中可以通过sc -d 类名
的方式查看加载这个类的类加载器详细的信息,如
通过上图可以看到,java.lang.String类的类加载器是空的,Hash值也是null。所以只要看到class-loader为null,就知道这是启动类加载器
通过启动类加载器去加载用户jar包:
如果用户想扩展一些比较基础的jar包,让启动类加载器加载,有两种途径:
-Xbootclasspath/a:jar包目录/jar包名
进行扩展,参数中的/a代表新增。下面展示方式二实现流程:
先创建第一个项目,mvn package打包成jar包,把jar包重命名放到D:/jvm/jar目录下,即D:/jvm/jar/classloader-test.jar;
再创建第二个项目,在第二个项目的IDEA配置中添加虚拟机参数,就可以加载D:/jvm/jar/classloader-test.jar
这个jar包了
希望启动类加载帮我们加载A类,在另一个项目中获取A类并初始化:使用Class.forName
获取Jar包的类,可以正常执行初始化,说明自己拓展的Jar包被加载了
应用场景:在企业中开发一些偏底层的基础类,所有用到jdk的项目都需要使用这些基础类,此时就通过启动类加载器去加载用户jar包
继承关系图如上:
扩展类加载器和应用类加载器继承自URLClassLoader,获得了上述的三种能力。
扩展类加载器(Extension Class Loader)是JDK中提供的、使用Java编写的类加载器。默认加载Java安装目录/jre/lib/ext下的类文件。
如下代码会打印ScriptEnvironment类的类加载器。ScriptEnvironment是nashorn框架中用来运行javascript语言代码的环境类,他位于nashorn.jar包中被扩展类加载器加载。这些类我们很少用,所以被放到了扩展类加载器中。
/**
* 扩展类加载器
*/
public class ExtClassLoaderDemo {
public static void main(String[] args) throws IOException {
ClassLoader classLoader = ScriptEnvironment.class.getClassLoader();
System.out.println(classLoader);
}
}
通过扩展类加载器去加载用户jar包:
-Djava.ext.dirs=jar包目录
进行扩展,这种方式会覆盖掉原始目录(jre-xx/lib/ext),可以追加上原始目录,并使用 ;(windows系统所用符号) :(macos/linux) 进行分隔确保自己写的类由扩展类加载器加载(上述A类),ScriptEnvironment仍由扩展类加载器加载、不受影响
使用引号
将整个地址包裹起来,这样路径中即便是有空格也不需要当做特殊字符额外处理。路径中要包含原来ext文件夹,同时在最后加上扩展的路径。
应用程序类加载器会加载classpath下的类文件,默认加载的是项目中的类以及通过maven引入的第三方jar包中的类。
如下案例中,打印出Student
(自己写的)和FileUtils
(引入的)的类加载器:
/**
* 应用程序类加载器案例
*/
public class AppClassLoaderDemo {
public static void main(String[] args) throws IOException, InterruptedException {
//当前项目中创建的Student类
Student student = new Student();
ClassLoader classLoader = Student.class.getClassLoader();
System.out.println(classLoader);
//maven依赖中包含的类
ClassLoader classLoader1 = FileUtils.class.getClassLoader();
System.out.println(classLoader1);
Thread.sleep(1000);
//由于使用Arthas监控该程序,故加上SYstem.in.read()让主方法不退出
System.in.read();
}
}
输出结果如下,这两个类均由应用程序类加载器加载:
Arthas中类加载器相关功能
类加载器的加载路径可以通过classloader –c hash值
查看:
查看应用程序类加载器所加载的jar包
由于JDK9引入了module的概念,类加载器在设计上发生了很多变化
1)启动类加载器使用Java编写,位于jdk.internal.loader.ClassLoaders类中。Java中的BootClassLoader继承自BuiltinClassLoader实现从模块中找到要加载的字节码资源文件。
启动类加载器依然无法通过java代码获取到,返回的仍然是null,保持了统一
2)扩展类加载器被替换成了平台类加载器(Platform Class Loader)。平台类加载器遵循模块化方式加载字节码文件,所以继承关系从URLClassLoader变成了BuiltinClassLoader,BuiltinCLassLoader实现了从模块中加载字节码文件。平台类加载器的存在更多的是为了与老版本的设计方案兼容,自身没有特殊的逻辑
方法名 | 说明 |
---|---|
public static ClassLoader getSystemClassLoader() | 获取系统类加载器 |
public InputStream getResourceAsStream(String name) | 加载某一个资源文件 |
public class ClassLoaderDemo2 {
public static void main(String[] args) throws IOException {
//static ClassLoader getSystemClassLoader() 获取系统类加载器
//InputStream getResourceAsStream(String name) 加载某一个资源文件
//获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
//利用加载器去加载一个指定的文件
//参数:文件的路径(放在src的根目录下,默认去那里加载)
//返回值:字节流。
InputStream is = systemClassLoader.getResourceAsStream("prop.properties");
Properties prop = new Properties();
prop.load(is);
System.out.println(prop);
is.close();
}
}
上文已经介绍过类加载器分类,在实际Java代码中,我们可能会遇到一个JAR包同时存在于多个类加载器加载范围的情况,此时我们就需要双亲委派机制来解决这个问题。
双亲委派机制(Parent Delegation Model)是Java类加载器的一种工作方式,用于保证类的加载安全性和一致性。
根据双亲委派机制,加载某一个类,先委托上一级的加载器进行加载,如果上级加载器也有上级,则会继续向上委托,如果该类委托上级没有被加载,子加载器尝试加载该类
简单来讲:双亲委派机制的核心是解决一个类到底由谁进行加载的问题。当一个类加载器接收到加载类的任务时,会自底向上查找是否加载过,在自顶向下进行加载。
具体介绍:如果一个类加载器收到了类加载请求、需要加载某个类时,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式
双亲委派机制-问题
在Java中如何使用代码的方式去主动加载一个类呢?
public class String {
static {
System.out.println("自己写的String类被加载了...");
}
}
public class Demo4 {
public static void main(String[] args) throws ClassNotFoundException {
//获取main方法所在类的类加载器,应用程序类加载器
ClassLoader classLoader = Demo4.class.getClassLoader();
System.out.println(classLoader); //sun.misc.Launcher$AppClassLoader@18b4aac2 启动类加载器
//使用应用程序类加载器加载 手写的com.lang.String
Class<?> stringClazz = classLoader.loadClass("java.lang.String");
System.out.println(stringClazz.getClassLoader()); //null 扩展类加载器
}
}
1)当一个类加载器去加载某个类的时候,会自底向上查找是否加载过,如果加载过就直接返回,如果一直到最顶层的类加载器都没有加载,再由顶向下进行加载。
2)应用程序类加载器的父类加载器是扩展类加载器,扩展类加载器的父类加载器是启动类加载器。
3)双亲委派机制的好处有两点:第一是避免恶意代码替换JDK中的核心类库,比如java.lang.String,确保核心类库的完整性和安全性。第二是避免一个类重复地被加载。
每个Java实现的类加载器中保存了一个成员变量叫“父”(Parent)类加载器,可以理解为它的上级,并不是继承关系。本质是在加载器内部创建一个ClassLoader来存储其父类加载器。
Arthas中类加载器相关的功能:类加载器的父子关系可以通过classloader -t
查看
整个双亲委派机制都是在Classload中进行的,因此我们主要看这部分源码:
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
尝试加载一个类的时候,我们会调用loadClass方法,该方法的第一个参数为加载的类名,第二个参数为是否对类进行解析。进入loadClass方法
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) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
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;
}
}
这段代码整体的逻辑为:
(1)通过双亲委派机制可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证唯一性。
(2)为了安全,保证类库API不会被修改
package java.lang;
public class String {
public static void main(String[] args) {
System.out.println("demo info");
}
}
由于是双亲委派的机制,java.lang.String的在启动类加载器得到加载,因为在核心jre库中有其相同名字的类文件,但该类中并没有main方法。这样就能防止恶意篡改核心API库。
此时执行main函数,会出现异常,在类 java.lang.String 中找不到 main 方法
总的来说,双亲委派机制可以保证类的一致性、安全性和隔离性,避免重复加载,同时也提供了灵活的扩展性,使得类加载器可以根据特定需求进行定制。
而虽然双亲委派机制为JAVA类的加载提供了很好的安全性和便捷性。但是有的时候我们不得不打破双亲委派机制,例如:一个Tomcat容器中可以运行多个WEB应用,而如果这两个应用中出现了同名的A类,那么Tomcat就要保证这两个A类都被加载并且是各自不同的类。如果不打破双亲委派机制,那么WEB1中的A类记载后,WEB2中自己的A类就不会加载成功了,按照双亲委派机制来讲,此时会直接返回WEB1中的A类。此时我们就需要打破双亲委派机制。
打破双亲委派机制的三种方式:
通过上文我们对源码单独阅读,相信大家已经理解了双亲委派机制的基本流程。而我们如果想要打破双亲委派机制,重写一下loadClass方法就好,具体地讲,是重写以下代码块:
代码示例:
public class MyClassLoader extends ClassLoader {
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 首先检查类是否已经被加载
Class<?> c = findLoadedClass(name);
if (c == null) {
// 检查类是否在系统类加载器中已经加载
c = findClass(name);
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 在这里实现自定义的类加载逻辑
// 可以从其他位置加载类的字节码,并使用 defineClass() 方法定义类
}
}
但需要注意的是,在这段代码的逻辑中,虽然我们没有给自定义类加载任何父类加载器,但是他也会有一个默认的父类加载器 应用程序类加载器,只不过我们重写loadClass的时候并没有用到父类加载器而已。
问题:两个自定义类加载器加载相同限定名的类,不会冲突吗
sc -d com.xxx.A
如果我们只是想自定义一个加载器,自主加载一些类。此时就不应该打破双亲委派机制,而是选择在FindClass中进行重写
JDBC在尝试连接数据库的时候会使用到一个叫做DriveManager的包来管理各种数据库驱动和加载相关驱动:
String url = "jdbc:mysql://localhost:3306/your_database_name";
String username = "your_username";
String password = "your_password";
Connection connection = DriverManager.getConnection(url, username, password);
DriveManager位于rt.jar中,由启动类加载器进行加载。
而这个包又要去加载各种数据库驱动类。而这种第三方的包又要在应用程序加载类中进行加载。那么就出现了一个问题:
也就是说启动类加载器加载完DriveManager之后,对于其需要加载的各种数据库驱动,启动类加载器是无法进行加载的,他只能交给应用程序类加载器进行加载。这就打破了双亲委派机制的从下向上委托原则。我们来看看DriverManager是如何解决解决这个问题的
DriverManager怎么知道jar包要加载的驱动在哪儿? ——用到JDK中的SPI机制。
SPI中是如何获取到应用程序类加载器的? ——在SPI机制中,通常使用线程上下文类加载器(Thread Context Class Loader)来加载具体的实现类。线程上下文类加载器是在多线程环境中引入的概念,用于指定每个线程的类加载器。线程上下文类加载器通常通过Thread.currentThread().setContextClassLoader()方法进行设置。
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader c1 = Thread.currentThread().setContextClassLoader();
return ServiceLoader.load(service, c1);
}
在SPI机制中,通过线程上下文类加载器,可以解决在双亲委托模型下从底层向上委托的问题。具体来说,当SPI实现框架的代码位于一个类库中,而由应用程序自定义的SPI实现类位于应用程序的类路径下时,由于双亲委托模型的限制,无法直接由应用程序加载SPI实现类。此时可以通过在应用程序中使用线程上下文类加载器来加载SPI实现类,即将线程上下文类加载器设置为应用程序的类加载器。这样,SPI实现框架就可以通过线程上下文类加载器加载应用程序中的SPI实现类,从而打破了双亲委托模型的限制。
需要注意的是,SPI机制依赖于线程上下文类加载器的正确设置,因此在使用SPI机制时,需要确保正确设置线程上下文类加载器,以保证SPI实现框架能够正确加载应用程序中的SPI实现类。
简单来讲:SPI有上下文类加载器,他可以提前保存好一个应用类程序加载器。然后当我们使用启动类加载器加载DriveManager,而DriveManager需要加载数据库驱动的时候,DriveManager就会调用上下文类加载器,使得当前加载器从启动类加载变为应用类加载器
但其实对于上下文加载器打破双亲委派机制这种方式呢,普遍还是存在争议的。
历史上OSGI模块化框架打破了双亲委派机制,它存在同级之间的类记载器的委托加载。
在OSGi框架中,每个模块被称为一个bundle(捆绑包),bundle可以包含自己的类和资源。OSGi使用了自己的类加载器实现,称为BundleClassLoader。
BundleClassLoader是OSGi框架中的核心类加载器,它在加载类时打破了双亲委派机制。它首先尝试自己加载类,如果找不到所需的类,则会委托给父类加载器。这种机制与标准的双亲委派机制不同,因为BundleClassLoader首先尝试自己加载,并不一定按照父优先的原则。
热部署指的是在服务不停止的情况下,动态地更新字节码文件到内存中
注意事项:
(1)什么是类加载器
JVM只会运行二进制文件,类加载器的作用就是将字节码文件加载到JVM中,从而让Java程序能够启动起来。
(2)类加载器的作用是什么
类加载器(ClassLoader)负责在类加载器过程中的字节码获取并加载到内存这一部分。通过加载字节码数据放入内存转换成byte[],接下来调用虚拟机底层方法将byte[]转换成方法区和堆中的数据
(3)类加载器有哪些/有几种常见的类加载器
(4)什么是双亲委派机制
每个Java实现的类加载器中保留了一个成员变量叫“父”(Parent)类加载器。
(5)JVM为什么采用双亲委派机制
(6)怎么打破双亲委派机制
参考 黑马程序员相关视频及笔记,大部分内容来源于黑马程序员的视频黑马程序员JVM虚拟机入门到实战全套视频教程,java大厂面试必会的jvm一套搞定(丰富的实战案例及最热面试题),加上自己部分思考
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。