文章目录
版权声明
- 本博客的内容基于我个人学习黑马程序员课程的学习笔记整理而成。我特此声明,所有版权属于黑马程序员或相关权利人所有。本博客的目的仅为个人学习和交流之用,并非商业用途。
- 我在整理学习笔记的过程中尽力确保准确性,但无法保证内容的完整性和时效性。本博客的内容可能会随着时间的推移而过时或需要更新。
- 若您是黑马程序员或相关权利人,如有任何侵犯版权的地方,请您及时联系我,我将立即予以删除或进行必要的修改。
- 对于其他读者,请在阅读本博客内容时保持遵守相关法律法规和道德准则,谨慎参考,并自行承担因此产生的风险和责任。本博客中的部分观点和意见仅代表我个人,不代表黑马程序员的立场。
类加载器
- 类加载器(ClassLoader)是Java虚拟机提供给应用程序去实现获取类和接口字节码数据的技术。
- 类加载器只参与加载过程中的字节码获取并加载到内存这一部分
- 类加载器的应用场景
类加载器的分类
- 类加载器分为两类,一类是Java代码中实现的,一类是Java虚拟机底层源码实现的。
- 类加载器的设计JDK8和8之后的版本差别较大,JDK8及之前的版本中默认的类加载器有如下几种
- 类加载器的详细信息可以通过classloader命令查看:
- classloader - 查看 classloader 的继承树,urls,类加载信息,使用 classloader 去getResource
- 详细的指令教程请参考Arthas官网
java
[arthas@17048]$ classloader
name numberOfInstances loadedCountTotal
BootstrapClassLoader 1 2248
com.taobao.arthas.agent.ArthasClassloader 1 1351
sun.misc.Launcher$ExtClassLoader 1 47
sun.reflect.DelegatingClassLoader 15 15
sun.misc.Launcher$AppClassLoader 1 8
Affect(row-cnt:5) cost in 7 ms.
[arthas@17048]$ classloader -t
+-BootstrapClassLoader
+-sun.misc.Launcher$ExtClassLoader@7fe21cf8
+-com.taobao.arthas.agent.ArthasClassloader@53e59711
+-sun.misc.Launcher$AppClassLoader@18b4aac2
Affect(row-cnt:4) cost in 5 ms.
- BootstrapClassLoader:c++编写的启动类加载器,位于虚拟机的源码中
numberOfInstances :1
类加载器的个数为一个loadedCountTotal: 2248
共加载了2248个核心类
- ArthasClassloader: ArthasClassloader工具的加载器
- ExtClassLoader:拓展类加载器
- DelegatingClassLoader: jdk底层实现用来提升反射效率的加载器
- AppClassLoader:应用程序类加载器
启动类加载器
- 启动类加载器(Bootstrap ClassLoader)是由Hotspot虚拟机提供的、使用C++编写的类加载器
- 默认加载Java安装目录/jre/lib下的类文件,比如rt.jar,tools.jar,resources.jar等。
- 通过启动类加载器去加载用户jar包
- 放入jre/lib下进行扩展:不推荐,尽可能不要去更改JDK安装目录中的内容,会出现即时放进去由于文件名不匹配的问题也不会正常地被加载
- 使用参数进行扩展:推荐,使用
-Xbootclasspath/a:jar包目录/jar包名
进行扩展
拓展类加载器&应用程序类加载器
-
扩展类加载器和应用程序类加载器都是JDK中提供的、使用Java编写的类加载器
-
它们的源码都位于
sun.misc.Launcher
中,是一个静态内部类,继承自URLClassLoader。通过目录或者指定jar包将字节码文件加载到内存中。
-
扩展类加载器(Extension Class Loader)是JDK中提供的、使用Java编写的类加载器
-
默认加载Java安装目录/jre/lib/ext下的类文件
- 通过扩展类加载器去加载用户jar包
- 放入/jre/lib/ext下进行扩展:不推荐,尽可能不要去更改JDK安装目录中的内容
- 使用参数进行扩展
- 推荐,使用-Djava.ext.dirs=jar包目录 进行扩展,这种方式会覆盖掉原始目录,可以用;(windows):(macos/linux)追加上原始目录
- 类加载器的加载路径可以先使用
classloader -l
查看类的hash值,然后通过classloader --c hash值
查看
双亲委派机制
- 由于Java虚拟机中有多个类加载器,那么一个类到底由哪个加载器进行加载?
- 解决方案:双亲委派机制 双亲委派机制的核心是解决一个类到底由谁加载的问题
- 每个Java实现的类加载器中保存了一个成员变量叫"父"(Parent)类加载器,可以理解为它的上级,并不是继承关系。
- 启动类加载器使用C++编写,没有上级类加载器
- 应用程序类加载器的parent父类加载器是扩展类加载器,而扩展类加载器的parent是空。扩展类加载器(Extension ClassLoader)在类加载过程中会委托给启动类加载器(Bootstrap ClassLoader)。这是因为Java的类加载器遵循所谓的"双亲委派模型"。
|-------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------|
| 图1 真实的父类关系 | 图2 双亲委派机制中的逻辑关系 |
- 尽管在类加载器的层次结构中,扩展类加载器的父类加载器是启动类加载器,但实际上,在类的加载过程中,应用类加载器的父类加载器是扩展类加载器,扩展类加载器没有父类加载器,但会委派给启动类加载器加载。这就是通常说扩展类加载器的父类加载器是启动类加载器。
- 类加载器的继承关系可以通过classloader --t 查看
-
双亲委派机制:当一个类加载器收到类加载请求时,它并不会尝试立即加载这个类,而是把这个请求委派给父类加载器。这样,每个类加载请求都会传递到启动类加载器,只有当父类加载器无法处理这个请求时(例如,类不在它的搜索路径中),子类加载器才会尝试自己去加载。
-
双亲委派机制口诀:自底向上查找是否加载过,再由顶向下进行加载。
解决三个问题
- 重复的类:如果一个类重复出现在三个类加载器的加载位置,应该由谁来加载?
- 启动类加载器加载,根据双亲委派机制,它的优先级是最高的
- String类覆盖问题:在自己的项目中去创建一个java.lang.String类,会被加载吗?
- 不能,会交由启动类加载器加载在rt.jar包中的String类
- 类加载器的关系:这几个类加载器彼此之间存在关系吗?
- 应用类加载器的父类加载器是扩展类加载器,扩展类加载器没有父类加载器,但是会委派给启动类加载器加载
打破双亲委派机制
- 打破双亲委派机制的三种方式
- 自定义类加载器
- 自定义类加载器并且重写loadClass方法,就可以将双亲委派机制的代码去除
- 线程上下文类加载器
- Tomcat通过这种方式实现应用之间类隔离
- Osgi框架的类加载器
- 历史上Osgi框架实现了一套新的类加载器机制,允许同级之间委托进行类的加载
自定义类加载器
- 一个Tomcat程序中是可以运行多个Web应用的,如果这两个应用中出现了相同限定名的类,比如Servlet类,Tomcat要保证这两个类都能加载并且它们应该是不同的类。
- 如果不打破双亲委派机制,当应用类加载器加载Web应用1中的MyServlet之后,Web应用2中相同限定名的MyServlet类就无法被加载。
- Tomcat使用了自定义类加载器来实现应用之间类的隔离。每一个应用会有一个独立的类加载器加载对应的类。
- 先来分析ClassLoader的原理,ClassLoader中包含了4个核心方法
- 双亲委派机制的核心代码就位于loadClass方法中
java
public Class<?> loadClass(String name) throws ClassNotFoundException {
return this.loadClass(name, false); //resolve
}
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
//加锁 保证只有一个线程加载类
synchronized(this.getClassLoadingLock(name)) {
//查询当前类加载器是否加载过需要的类,true 返回类对象 否则,返回null
Class<?> c = this.findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
//向逻辑父类委派加载任务
try {
//直到委派到顶级父类------Extension,进入else分支
if (this.parent != null) {
c = this.parent.loadClass(name, false);
} else {
//调用启动类加载器,进行加载,无法加载返回null
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException var10) {
}
//所有类加载器加载失败,使用本类加载器进行加载
if (c == null) {
long t1 = System.nanoTime();
c = this.findClass(name);
PerfCounter.getParentDelegationTime().addTime(t1 - t0);
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
this.resolveClass(c);
}
return c;
}
}
final Class<?> loadClass(Module module, String name) {
synchronized(this.getClassLoadingLock(name)) {
Class<?> c = this.findLoadedClass(name);
if (c == null) {
c = this.findClass(module.getName(), name);
}
return c != null && c.getModule() == module ? c : null;
}
}
//读取二进制数据
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
protected Class<?> findClass(String moduleName, String name) {
if (moduleName == null) {
try {
return this.findClass(name);
} catch (ClassNotFoundException var4) {
}
}
return null;
}
//类名校验 将字节码信息加载到虚拟机内存中
protected final Class<?> defineClass(byte[] b, int off, int len) throws ClassFormatError {
return this.defineClass((String)null, b, off, len, (ProtectionDomain)null);
}
protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError {
return this.defineClass(name, b, off, len, (ProtectionDomain)null);
}
protected final void resolveClass(Class<?> c) {
if (c == null) {
throw new NullPointerException();
}
}
- 阅读双亲委派机制的核心代码,分析如何通过自定义的类加载器打破双亲委派机制
- 打破双亲委派机制的核心就是将下边这一段代码重新实现
java
//parent等于null说明父类加载器是启动类加载器,直接调用findBootstrapClassOrNull
//否则调用父类加载器的加载方法
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
//父类加载器爱莫能助,我来加载!
if (c == null)
c = findClass(name);
案例演示
java
public class CustomClassLoader extends ClassLoader {
public CustomClassLoader(ClassLoader parent) {
super(parent);
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
// 简单的例子, 只处理自定义的类
if (name.startsWith("com.mydomain")) {
return findClass(name);
}
// 其他的类还是采用双亲委派机制加载
return super.loadClass(name);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] bytes = loadClassData(name);
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e) {
throw new ClassNotFoundException("Cannot load class " + name, e);
}
}
private byte[] loadClassData(String name) throws IOException {
// 根据类名加载类的二进制数据. 假设所有类都在一个固定的目录下
Path path = Paths.get("classes", name.replace('.', '/') + ".class");
return Files.readAllBytes(path);
}
}
- 自定义类加载器默认父类是AppClassLoader
- 以Jdk8为例,ClassLoader类中提供了构造方法设置parent的内容
- 这个构造方法由另外一个构造方法调用,其中父类加载器由getSystemClassLoader方法设置,该方法返回的是AppClassLoader。
- 两个自定义类加载器加载相同限定名的类,不会冲突
- 在同一个Java虚拟机中,只有相同类加载器+相同的类限定名才会被认为是同一个类
- 正确的去实现一个自定义类加载器的方式是重写findClass方法,这样不会破坏双亲委派机制
线程上下文类加载器
- 利用上下文类加载器加载类,比如JDBC和JNDI等
- JDBC中使用了DriverManager来管理项目中引入的不同数据库的驱动,比如mysql驱动、oracle驱动。
-
DriverManager类位于rt.jar包中,由启动类加载器加载
-
依赖中的mysql驱动对应的类,由应用程序类加载器来加载。
- DriverManager属于rt.jar是启动类加载器加载的。而用户jar包中的驱动需要由应用类加载器加载,这就违反了双亲委派机制
- DriverManage使用SPI机制,最终加载jar包中对应的驱动类
- SPI中获取到应用程序类加载器
- SPI中使用了线程上下文中保存的类加载器进行类的加载,这个类加载器一般是应用程序类加载器
- SPI中使用了线程上下文中保存的类加载器进行类的加载,这个类加载器一般是应用程序类加载器
案例梳理
⚫1. 启动类加载器加载DriverManager。
⚫ 2. 在初始化DriverManager时,通过SPI机制加载jar包中的myql驱动。
⚫ 3. SPI中利用了线程上下文类加载器(应用程序类加载器)去加载类并创建对象。
- 由启动类加载器加载的类,委派应用程序类加载器去加载类的方式,打破了双亲委派机制
OSGi模块化
- OSGi模块化框架。它存在同级之间的类加载器的委托加载。OSGi还使用类加载器实现了热部署的功能。
- 热部署指的是在服务不停止的情况下,动态地更新字节码文件到内存中