Java笔记_1
类加载器
- 类加载器(
ClassLoader)是JVM给应用程序实现类和接口字节码数据的技术 - 本地接口
JNI允许Java调用其他语言编写的方法, 在hotspot类加载器中, 主要用于调用JVM中使用CPP编写的方法
应用
SPI机制- 类的热部署
Tomcat类的隔离- 类的双亲委派机制(怎么打破双亲委派机制)
- 自定义类加载器
- 使用
Arthas不停机解决线上故障
分类
Java代码中实现的 orJDK默认提供 or 自定义的, 所有实现的类加载器都需要继承抽象类ClassLoaderJVM底层源码实现的, 跟虚拟机实现语言一致, 比如Hotspot使用cpp实现- 作用是加载运行时的基础类, 保证运行中基础类可以被正确的加载, 比如
java.lang.String, 确保可靠性
- 作用是加载运行时的基础类, 保证运行中基础类可以被正确的加载, 比如
JDK8之前
JVM底层实现的类加载器, 启动类加载器Bootstrap, 加载Java中最核心的类, 在jre/lib目录下的所有类文件, 比如rt.jar, tools.jar, resources.jar
- 如果打印看到类加载器为
null, 则就是启动类加载器. 因为为了安全性考虑, 不允许在代码中获得启动类加载器 - 用户自己如果写了一些底层的
jar包可以由启动类加载器加载, 有两种方式- 放在
jre/lib目录下(但是不推荐这么做) - 使用
-Xbootclasspath/a:<jar包目录>/<jar包名>进行扩展
- 放在
Java代码中实现了扩展类加载器Extension, 允许扩展Java中比较通用的类- 应用程序类加载器
Application, 加载应用使用的类, 比如由第三方jar包引入的类- 源码都位于
sun.misc.Launcher中, 是一个静态内部类, 继承自URLClassLoader, 可以通过目录或指定jar包将字节码文件加载到内存中
- 扩展类加载器
Extension ClassLoader默认加载jre/lib/ext下的类文件- 所以同样可以将自定义的
jar包放在jre/lib/ext目录下(但是不推荐) - 使用
-Djava.ext.dirs=<jar包目录>进行扩展, 会覆盖原始目录 windows上可以使用分号分隔,macos或者linux上使用冒号分隔, 再追加上原始目录
- 所以同样可以将自定义的
- 应用程序类加载器
Application加载classpath下的类文件, 项目中的类或者是第三方依赖中的类
- 源码都位于
Arthas中使用classloader命令可以查看classloader -l查看所有类加载器的哈希值classloader -c <hash>其中<hash>表示某一个类加载器的哈希值, 就可以把所有加载的jar包都打印出来- 可以看到扩展类加载器加载的都是扩展类加载器的
jar包, 但是应用程序类加载器可以加载maven第三方jar包, 还加载了扩展类加载器, 以及启动类加载器加载的jar包的内容, 这部分是因为双亲委派机制
双亲委派机制
解决了一个类到底由谁来加载的问题
作用
- 保证类加载的安全性, 避免恶意代码替换
JDK的核心类库, 确保核心类库的完整性和安全性 - 避免重复加载
含义
- 一个类加载器接收到加载类的任务时, 会自底向上查找是否加载过, 再自顶向下加载
- 自顶向下依次是启动类加载器, 扩展类加载器, 应用程序类加载器
- 先自底向上查找一个类是否被加载过
- 如果已经加载过, 则这个类的加载任务就结束了
- 如果没有加载过, 则会自顶向下逐个加载
- 双亲委派机制就是向上查找和向下加载的过程
- 比如一个定义的类A, 先由应用程序类加载器查找, 没有加载过则再由扩展类加载器查找, 没有加载过则再由启动类加载器进行查找, 如果启动类加载器加载过了, 则加载过程结束
- 比如一个类B, 没有被三个类加载器加载, 则由启动类加载器加载, 查看加载路径, 发现不在启动类加载器的加载路径中, 则会由扩展类加载器尝试加载, 判断路径, 如果依然不在路径中, 最后由应用程序类加载器加载
- 所以向上查找的过程避免了重复加载, 向下加载的过程起到了加载优先级的作用
- 先自底向上查找一个类是否被加载过
- 如果一个类重复出现在三个类加载器的加载位置处, 则由谁来加载?
- 根据双亲委派机制, 应该是由启动类加载器加载的, 因为他的优先级是最高的
String类能否被覆盖? 如果自定义了一个java.lang.String类, 那么这个类是否会被加载- 启动类加载器在向上查找阶段已经会发现
rt.jar包中的String类已经加载过了, 所以不会加载自定义的String类, 会返回已经加载过的String类
- 启动类加载器在向上查找阶段已经会发现
- 如何使用代码主动加载一个类?(两种方法)
- 使用
Class.forName(), 使用当前类的类加载器去加载指定的类 - 通过
getClassLoader()获取到类加载器, 通过类加载器的loadClass()指定某个类加载器的加载, 这种方式更加明确一些, 比如:1
2
3
4
5
6// 获取main方法所在类的类加载器, 应用程序类加载器
ClassLoader cl = D.class.getClassLoader();
System.out.println(cl);
// 使用应用程序类加载器加载指定的类
Class<?> clazz = cl.loadClass("类名");
System.out.println(clazz.getClassLoader());
- 使用
- 如果一个类无法被三个类加载器加载呢?
- 会报类无法找到的错误
- 每一个类加载器都有一个父类加载器, 在代码中都是
parent, 类型是ClassLoader, 比如应用程序类加载器的parent=扩展类加载器, 但是扩展类加载器的parent=null, 这是因为启动类加载器是无法获得的, 所以是nullArthas使用classloader -t查看父子关系
总结-类的双亲委派机制是什么
- 类加载器加载某个类的时候, 会自底向上向父类查找是否加载过, 如果加载过就直接返回, 如果一直到最顶层的类加载器都没有加载, 再由自顶向下进行加载
- 应用程序类加载器的父类加载器是扩展类加载器, 扩展类加载器的父类加载器是启动类加载器
- 双亲委派机制的好处有两点:
- 避免恶意代码替换
JDK中核心类, 比如java.lang.String, 确保安全性, 完整性 - 避免重复加载类
- 避免恶意代码替换
打破双亲委派机制
- 自定义类加载器
- 线程上下文类加载器
Osgi框架的类加载器
场景
- 有两个
Web应用, 都具有一个名为MyServlet的类, 只有名字相同, 内容不同Tomcat想去加载这两个类, 这两个类都位于classpath上, 应用1的类可以正常加载, 但是应用2的类,Tomcat无法加载- 因为这个时候
MyServlet类已经存在了, 这样只能加载一个类 - 实际上
Tomcat对每个应用都有一个类加载器, 这样就不会走双亲委派机制 - 应用1和应用2都加载自己的类, 因此实现了使用自定义类加载器打破双亲委派机制, 实现应用类的隔离
自定义类加载器
ClassLoader中包含了四个核心方法, 双亲委派机制的核心代码在loadClass()中
1 | public Class<?> loadClass(String name) |
- 双亲委派机制
1 | // parent == null 说明父类加载器是启动类加载器, 直接调用findBootstrapClassOrNull |
- 自定义的类加载器, 如果不手动指定父类加载器, 则默认指向应用程序类加载器
- 两个自定义类加载器, 加载相同的限定名的类, 是否会冲突?
- 不会冲突, 只有相同的类加载器+相同的类限定名才会被认为是同一个类, 在
Arthas中使用sc -d 类名可以查看具体情况
- 不会冲突, 只有相同的类加载器+相同的类限定名才会被认为是同一个类, 在
- 为了实现多种不同的渠道加载字节码的方式, 不应该打破双亲委派机制, 应该重写
findClass方法
线程上下文类加载器
以
JDBC为例, 使用了DriverManager来管理项目中引入的不同数据库的驱动, 比如MySQL驱动, 或者Oracle驱动
DriverManager类位于rt.jar中, 所以由启动类加载器加载- 但是不同数据库的驱动是在用户的
jar包中, 由应用程序类加载器加载, 因此违反了双亲委派机制 - 所以在初始化
DriverManager时, 通过SPI机制加载jar包中的MySQL驱动 SPI利用了线程上下文类加载器(应用程序类加载器)加载类并创建对象
DriverManager如何知道驱动类已经引入
SPI(Service Provider Interface)机制, 是JDK内置的服务提供发现机制, 类似于Spring的依赖注入
SPI原理
- 在
ClassPath路径下的META-INF/services目录中, 以接口的全限定名来命名文件, 对应的文件里面写了接口的实现 - 使用
ServiceLoader类加载实现类,ServiceLoader<Driver> lD = ServiceLoader.load(Driver.class);获取Driver对象
SPI如何获取应用程序类加载器
使用了线程上下文中保存的类加载器进行类的加载, 一般是应用程序类加载器
1 | public static <S> ServiceLoader<S> load(Class<S> service){ |
在《深入理解Java虚拟机》一书中认为
JDBC打破了双亲委派机制, 因为由启动类加载器加载的类, 委派应用程序类加载器去加载类也有一种说法, 认为
JDBC没有打破双亲委派机制, 因为单独的启动类加载器在DriverManager加载完了之后, 通过初始化阶段触发了驱动类的加载, 依然遵循的是双亲委派机制. 因为驱动类加载器自底向上查找后, 再自顶向下进行判断, 发现启动类加载器和扩展类加载器都无法加载, 所以由应用程序类加载器进行加载, 这个过程也没有打破双亲委派机制. 这两个类加载器都没有重写loadClass(), 打破双亲委派机制的唯一方法就是重写loadClass()
OSGI模块化
JDK9之后有另一种模块化方法, 因此不再使用了.OSGI也使用了类加载器实现了热部署的功能, 即在服务不停止的情况下, 动态更新字节码文件到内存中. 现在可以使用Arthas实现热部署, 本质上还是类加载器
Arthas不停机解决线上问题
- 出问题服务器上部署一个
Arthas jad --source-only <类全限定名> > <dir/文件名.java>jad命令反编译, 用其他编译器, 比如vim来修改源代码mc -c <类加载器的hash> <dir/文件名.java> -d <输出目录>mc命令用来编译修改过的代码- 这里的类加载器的
hash可以使用sc -d <对应的类名>来查看
- 这里的类加载器的
retransform class文件所在的<目录/xxx.class>用retransform命令加载新的字节码
注意事项:
- 如果程序重启, 则字节码文件会恢复, 因为只是通过
retransform这个命令将字节码文件放在了内存中. 因此要将字节码文件放在jar包中进行更新 - 使用
retransform不能添加方法或者字段, 也不能更新正在执行中的方法
JDK9之后
引入了
module概念
- 启动类加载器由
Java编写了, 不再由CPP编写, 位于jdk.internal.loader.ClassLoaders类中Java中的BootClassLoader继承自BuiltinClassLoader实现从模块中找到要加载的字节码资源文件- 但是启动类加载器依然无法通过
Java代码获取, 这是为了保证统一, 所以返回的内容依然是null
- 扩展类加载器被替换成了平台类加载器(
Platform ClassLoader)- 平台类加载器遵循模块化方式加载字节码文件, 所以继承关系从
URLClassLoader变成了BuiltinClassLoader, 实现了从模块中加载字节码文件 - 平台类加载器的存在更多是为了与老版本兼容, 自身没啥特殊的逻辑. 如果使用了模块化的思想, 平台类加载器基本上就不需要了
- 平台类加载器遵循模块化方式加载字节码文件, 所以继承关系从
总结
- 类加载器的作用
- 类加载器(
CLassLoader)负责在类加载的过程中的字节码获取并加载到内存, 通过加载字节码数据放入内存转换成byte[], 接下来调用虚拟机底层方法将byte[]转换成方法区和堆中的数据
- 类加载器(
- 有几种类加载器
- 启动类加载器
Bootstrap加载核心类 - 扩展类加载器
Extension加载扩展类 - 应用程序类加载器
Application加载应用classpath中的类 - 自定义类加载器, 重写
findClass方法 JDK9之后从扩展类加载器变成了平台类加载器
- 启动类加载器
- 什么是双亲委派机制
- 每个
Java实现的类加载器中保存了一个成员变量叫做父 类加载器, 自底向上查找是否加载过, 再自顶向下加载, 避免核心类被应用程序重写覆盖的问题, 提升安全性
- 每个
- 如何打破双亲委派机制
- 重写
loadClass方法 JNDI, JDBC, JCE, JBI等框架使用了SPI+ 线程上下文类加载器OSGI实现了一整套类加载逻辑, 允许同级类加载器之间互相调用
- 重写
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Sangs Blog!




