先抛出问题
- ClassLoader是什么
- 什么是双亲委派机制
- Java是如何由源代码->执行结果的
- JVM按什么顺序加载.class文件的
- ClassLoader 如何保证每个类只会加载一次
- 同名同包类最终加载的结果是什么, 是都会被加载还是只会加载其中一个.如果都会被加载那为什么呢? 如果只会被加载其中一个那又是为什么?
- 同名不同包类最终加载的结果是什么, 是都会被加载还是只会加载其中一个.如果都会被加载那为什么呢? 如果只会被加载其中一个那又是为什么?
ClassLoader是什么
ClassLoader是虚拟机负责装载类的对象。类ClassLoader
是一个抽象类。给定类的二进制名称,类加载器应该尝试定位或生成构成类定义的数据。典型的策略是将名称转换为文件名,然后从文件系统中读取该名称的“类文件”即.class
什么是双亲委派机制
为了实现双亲委派机制. 在Java中,类加载器负责加载类的字节码并创建对应的Class对象。而双亲委派机制是指当一个类加载器收到类加载请求时,它会先将该请求委派给它的父类加载器去尝试加载。只有当父类加载器无法加载该类时,子类加载器才会尝试加载。
这种机制的设计目的是为了保证类的加载是有序的,避免重复加载同一个类。Java中的类加载器形成了一个层次结构,根加载器(Bootstrap ClassLoader)位于最顶层,它负责加载Java核心类库,它的父级为null, 其他子类加载器如扩展类加载器(Extension ClassLoader)和应用程序类加载器(Application ClassLoader)都有各自的加载范围和职责。通过双亲委派机制,可以确保类在被加载时,先从上层的加载器开始查找,逐级向下,直到找到所需的类或者无法找到为止。
这种机制的好处是可以避免类的重复加载,同事提高了类加载的效率和系统类的安全性。同时,它也为Java提供了一种扩展机制,允许开发人员自定义类加载器,实现自己的加载策略
Java是如何由源代码->执行结果的
java源代码->执行结果中间分为2大部分.
1 编译=>结果生成java虚拟机能够接受的.class
文件
2. 类加载执行class
文件=>得到运行结果
其中编译阶段分为: 词法分析->语法分析->语义分析->编译class文件
而虚拟机执行流程分为 加载.class
文件(状态->连接->初始化)阶段得到系统指令码->执行阶段发送指令码进行相关操作
如上的委托父级去加载. 是在已经获取到class文件后才会去执行的
JVM按什么顺序去加载.class文件
- 本地类路径:JVM会首先在本地类路径中查找并加载类。本地类路径一般包括当前工程的编译输出目录和依赖的第三方库。
- 扩展类路径:如果在本地类路径中没有找到类,则JVM会在扩展类路径中查找并加载类。扩展类路径一般包括JVM安装目录下的jre/lib/ext目录和java.ext.dirs系统属性指定的目录。
- 系统类路径:如果在扩展类路径中仍然没有找到类,则JVM会在系统类路径中查找并加载类。系统类路径一般包括JVM启动时指定的类路径,可以通过java -classpath或java -cp参数来指定。
**注意:**在上述加载顺序中,当JVM找到第一个与类名匹配的类时,就会停止搜索,不再继续查找
ClassLoader怎样保证同一时间每个类只会加载一次
ClassLoader
设计的大概实现和目的上述已经描述清了, 接下来看看如何去保证每个类同时只会加载一次
源码分析
/**
ClassLoader.loadClass();
* 根据名称去加载类, 这点可以自定义
* 整体的类加载实现逻辑在这个方法里就可以看到
*/
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 检查类是否已经被加载过. 有就直接使用,没有就跳过
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
//逐级向上递, 所有的ClassLoader实现类都会实现/使用该方法.除非自定义破坏
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//只有BootstrapLoader 父类才会为null. 运行到这里也就是到了顶层了
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
//如果父类加载都为空, 那么尝试自己去实现
if (c == null) {
long t1 = System.nanoTime();
c = findClass(name);
//// 多余代码已删除
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
重点关注下这里的synchronized (getClassLoadingLock(name))
synchronized
关键字应该都清楚, 放在这里锁的是代码块, 锁的是getClassLoadingLock(name)
返回的这个对象. 保证返回相同的这个对象统一时间只会被执行一次 再观察下这个方法
private final ConcurrentHashMap<String, Object> parallelLockMap;
/**
* 根据className去获取对象. 这里的parallelLockMap对象为ConcurrentHashMap. 这属于是一个线程安全的Map. 感兴趣可以额外去了解下. 这里不再赘述
*/
protected Object getClassLoadingLock(String className) {
Object lock = this;
if (parallelLockMap != null) {
// 创建一个新Object对象
Object newLock = new Object();
// 当map.putIfAbsent() put成功时, 返回null,当前对象, 反之,已经存在则返回当前对象
lock = parallelLockMap.putIfAbsent(className, newLock);
// 注意如果put成功才会为null. 也就是返回的lock 是当前classLoader对象
if (lock == null) {
// put失败, 才会返回一个新的对象
lock = newLock;
}
}
return lock;
}
整理流程
拿这里举几个例子.依次整理下思路
自定义MyString(com.mwsfot包下) 和 MyString(com.mwsfot包下) 从包的概念理解上, 它就应该被加载两次吧. 那我自定义一个String(java.lang). 然后在main方法中输出下自身的加载器和类加载器, 结果应该是什么? 按照我们预期的结果
第一行结果是AppClassLoader
第二行则是ExtClassLoader
package java.lang;
/**
* 自定义String类
* @author MC
* @description String
* @date 2024/2/5 11:03
*/
public class String {
private int a;
public String(Integer a){
a = a;
}
public String(){
}
}
输出类
public class Test{
public static void main(String[] args) {
String s = new String("123");
byte[] bytes = s.getBytes();
System.out.println(bytes);
System.out.println("自己的类加载器: "+ s.getClass().getClassLoader());
System.out.println("父类的类加载器: " + s.getClass().getClassLoader().getParent());
}
以上,都编译通过
想好了么, 揭晓答案了. 答案是会报错. 可以看到这里main
方法. 我们用的是真正的java官方提供的java.lang.String
. 只有它才拥有getBytes
方法. 为什么会报错呢. 这里没有使用到我们自己编写的String类, 使用的官方的java.lang.String
了. 而官方的String
是main
方法的入口参数. 同一个类中只会引入一个类型.
而报错原因也很简单, String
是rt.jar包里的, 是通过BootstrapClassLoader
加载的. 使用的是C++底层. 所以不存在类加载器, 也就是null
既然同包名无法执行,那我们换个包名区分下
public class String {
private int a;
public String(int a){
a = a;
}
}
ok 可以看到, 编译通过了, 我们换个包名就被识别到了, 现在再想想执行结果是什么
揭晓答案, 答案是找不到主类.
原因很简单.这里的main方法不是我们熟知的java入口了, 这里的String[] args
是我们自己编写的String
注意不是因为类加载系统的问题, 上面说了这里只是编译阶段, 还没有到类加载的阶段!
ok我们再改写下, 指定下java.lang.String
如下:
public static void main(java.lang.String[] args) {
String s = new String(1);
System.out.println("自己的类加载器: "+ s.getClass().getClassLoader());
System.out.println("父类的类加载器: " + s.getClass().getClassLoader().getParent());
}
现在再最后想想执行结果是什么. 第一行结果是AppClassLoader
吗. 第二行是ExtClassLoader
吗
揭晓答案, 正如我们猜想的一样. 自定义的类使用AppClassLoader
加载. 同时与String
不冲突.
结论
我们现在将最后一步的自定义的String
类代入ClassLoader
的当中.
我们的name叫做com.mwsfot.parent.String
. 继续往下走进入getClassLoadingLock
方法
可以看到存在的是叫做java.lang.String
. 实际上在map中存储的不是同一个String类
返回为null也可以得出.
此时的两个String类, 不是同一个对象. 我们的String就相当于普通的类. 会被AppClassLoader
加载也就不奇怪了. 这也就回答上面提出的问题.因为返回的value
不是同一个对象, 我们的synchronized (getClassLoadingLock(name))
也就当然锁不住了.
这也就是为何每个类同时只会被加载一次的原因. 而同名同包类本身编译阶段都无法通过. java 就是通过这样去保证每个类同时只会被加载一次的.
同名同包类和同名不同包类的结果
虽然上面解释了, 保证每个类同时只会加载一次的问题. 但是又引申出了新的问题. 那加载呢? 这里只是控制了顺序, 具体的加载那怎么办?
在loadClass
中存在一个findLoadedClass
方法, 就是去缓存中获取已经加载过的class信息.
Class<?> c = findLoadedClass(name);
如果没有加载过走双亲委派去找,上述都失败为null的情况. 才去自身的findClass
方法查找. 默认抛出异常信息. 由实现类自行去实现. 一般实现defineClass
方法去网络或者本地去查找class信息.最终加载进来.
以上就是是加载的流程.
考虑到我们标题提的问题, 这里就需要去扩展思考下了,前提都是没有自定义ClassLoader
- 假如真的有两个同名同包的
java.lang.String
通过了编译阶段. 第一个是正版, 第二个盗版.
1). 第一个正版String
存入了Map当中, 锁的是value值, 即new Object()
.
2). 第二个盗版String
存入Map. 发现存在, 于是返回对象ClassLoader.
3). 锁的是ClassLoader这个对象. 当然没锁住, 但是锁住了类. 此时的class缓存必然存在结果, 要么已经存在要么不存在.
4). 此时去缓存中根据名称去获取判断, 存在就直接返回, 不存在就走双亲委派机制去找到当前class. 如果不存在委派去父类加载, 父类层层委派.最终在BootstrapClassLoader
返回正版的String
. 我们的盗版替代正版的希望又轮空了.
5). 再假设我们又通过了委派机制(自定义),返回的就是我们自定义的类, 后续又有一个方法叫做defineClass
用于返回class定义, 这里又逃逸不过去, 会校验名称,java.lang.String
java开通头的名称, 必须由BootstrapClassLoader
去加载. 至此形成了闭环. 只会返回正版的String
- 还是上面的情况, 只不过我们目标现在没那么远大了, 我们就想替换某个三方包的类, 比如
com.a.A
和com.a .B
A是正版, B是我们想替换的盗版, 再代入进去, A 和 B任意一个, 先被加载存入Map.不存在同时, 因为有synchronized
关键字锁value值
, 同时ComcurrentHashMap
存返值. 所以两个加载必然被保留先后顺序. 最终结果只会加载第一个同名文件. - 同名不同包就很简单理解了, 不同的全限定名就是不同的Map对象. 存在先后顺序, 找到的class文件当然就是不同的class
评论区