Java笔记_1
类加载器
- 类加载器(
ClassLoader
)是JVM
给应用程序实现类和接口字节码数据的技术 - 本地接口
JNI
允许Java
调用其他语言编写的方法, 在hotspot
类加载器中, 主要用于调用JVM
中使用CPP
编写的方法
应用
SPI
机制- 类的热部署
Tomcat
类的隔离- 类的双亲委派机制(怎么打破双亲委派机制)
- 自定义类加载器
- 使用
Arthas
不停机解决线上故障
分类
Java
代码中实现的 orJDK
默认提供 or 自定义的, 所有实现的类加载器都需要继承抽象类ClassLoader
JVM
底层源码实现的, 跟虚拟机实现语言一致, 比如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
, 这是因为启动类加载器是无法获得的, 所以是null
Arthas
使用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!