动手体验JVM中Class对象的唯一性
概述
本文不深究理论,不深究原理,从我们开发使用者的角度,动手实践,去体验一下JVM中,Class对象的唯一性与类加载器的关系。
引入
我们通常说:每个类,无论创建多少个实例,在JVM中都对应同一个Class对象。
其实这么说还是挺别扭的,首先是先有的Class对象,然后才有的类的实例。而且这么说其实也并不严谨,假如说我们有一个类的两个实例对象,而这两个实例对象在内存里对应的的class信息是由两个不同的类加载器加载的,也就是说这个时候这两个实例对应的就是两个不同的Class对象。Class对象的唯一性的确定因素之一就是加载它的类加载器。
下面我们从4个章节去体验一下Class对象的唯一性与类加载器之间的关系。
1、默认类加载器下体验
首先创建一个Java bean
package com.myspring.service.impl;
public class MyTestBean {
public String testStr = "testStr";
public String getTestStr() {
return testStr;
}
public void setTestStr(String testStr) {
this.testStr = testStr;
}
}
新建一个测试类,在单元测试中添加测试
@Test
public void test(){
MyTestBean myTestBean1 = new MyTestBean();
MyTestBean myTestBean2 = new MyTestBean();
//两个实例肯定是不等的
System.out.print("myTestBean1与myTestBean2是否相等:");
System.out.println(myTestBean1 == myTestBean2);
Assert.assertNotEquals(myTestBean1,myTestBean2);
Class c1 = myTestBean1.getClass();
Class c2 = myTestBean2.getClass();
System.out.print("加载c1的类加载器:");
System.out.println(c1.getClassLoader());
System.out.print("加载c2的类加载器:");
System.out.println(c2.getClassLoader());
System.out.print("c1与c2是否相等:");
System.out.println(c1 == c2);
Assert.assertEquals(c1,c2);
}
运行结果如下:
myTestBean1与myTestBean2是否相等:false
加载c1的类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
加载c2的类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
c1与c2是否相等:true
没什么可说的,这是我们最常用、最常见的形式了,隐式的使用类加载器加载class文件生成Class对象,c1和c2的类加载器是同一个,c1=c2即myTestBean1与myTestBean2对应的是同一个Class对象,此时MyTestBean的Class对象在JVM中是唯一的。
2、指定URLClassLoader类加载器体验
这里我们就使用URLClassLoader吧,然后这里我们把MyTestBean.class拷贝到一个新的目录下/Users/zhangcheng/test/com/myspring/service/impl/
新建一个测试类,在单元测试中添加测试
@Test
public void test() throws MalformedURLException, ClassNotFoundException, IllegalAccessException, InstantiationException {
ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader();
System.out.print("当前线程的类加载器:");
System.out.println(currentClassLoader);
URL[] urls = new URL[1];
urls[0] = new URL("file:///Users/zhangcheng/test/");
URLClassLoader urlClassLoader1 = new URLClassLoader(urls);
System.out.print("urlClassLoader1的父 类加载器:");
System.out.println(urlClassLoader1.getParent());
URLClassLoader urlClassLoader2 = new URLClassLoader(urls);
System.out.print("urlClassLoader2的父 类加载器:");
System.out.println(urlClassLoader2.getParent());
Object myTestBean1 = urlClassLoader1.loadClass("com.myspring.service.impl.MyTestBean").newInstance();
Object myTestBean2 = urlClassLoader2.loadClass("com.myspring.service.impl.MyTestBean").newInstance();
//两个实例肯定是不等的
System.out.print("myTestBean1与myTestBean2是否相等:");
System.out.println(myTestBean1 == myTestBean2);
Assert.assertNotEquals(myTestBean1,myTestBean2);
Class c1 = myTestBean1.getClass();
Class c2 = myTestBean2.getClass();
System.out.print("加载c1的类加载器:");
System.out.println(c1.getClassLoader());
System.out.print("加载c2的类加载器:");
System.out.println(c2.getClassLoader());
System.out.print("c1与c2是否相等:");
System.out.println(c1 == c2);
Assert.assertEquals(c1,c2);
}
运行结果如下:
当前线程的类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
urlClassLoader1的父 类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
urlClassLoader2的父 类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
myTestBean1与myTestBean2是否相等:false
加载c1的类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
加载c2的类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
c1与c2是否相等:true
我C?什么情况?为啥都是AppClassLoader??我们不是指定的file:///Users/zhangcheng/test/
URL了嘛?不应该是AppClassLoader啊!!
其实是这样的:类加载器有一个委托加载机制,某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父 类加载器(注意:不是父类
),依次递归,如果父 类加载器可以完成类加载任务,就成功返回;只有当父 类加载器无法完成此加载任务时,才自己去加载。而new URLClassLoader(urls)
没有指定父 类加载器但默认指定了AppClassLoader为父 类加载器。由上面的测试结果看,AppClassLoader是能够加载到com.myspring.service.impl.MyTestBean
的,但是我们没有指定file:///Users/zhangcheng/test/
这个URL给它啊!那就说明AppClassLoader的URLs里也有com.myspring.service.impl.MyTestBean
!!!怎么去验证?把它打印出来就行了
在上面的测试中加一行,打印出urlClassLoader1的父 类加载的所有URL
Arrays.asList(((URLClassLoader)urlClassLoader1.getParent()).getURLs()).forEach(System.out::println);
结果
...省略
file:...(省略)/target/test-classes/
file:...(省略)/target/classes/
...省略
我们看到了什么?这NM不是我们这个测试项目build的后的class文件的目录嘛!!所以在此次的测试中,myTestBean1与myTestBean2的两个实例对应的Class对象是同一个,是由AppClassLoader加载file:...(省略)/target/classes/
目录下的MyTestBean.class文件,而不是我们指定的那两个URLClassLoader去加载file:///Users/zhangcheng/test/
目下的MyTestBean.class文件。
此时MyTestBean的Class对象在JVM中是唯一的。
那么怎么指定它使用URLClassLoader去加载file:///Users/zhangcheng/test/
目下的MyTestBean.class文件呢?
3、再次指定URLClassLoader类加载器体验
测试类基本不变,我们删除file:...(省略)/target/classes/
目录下的MyTestBean.class,再次进行测试
@Test
public void test() throws MalformedURLException, ClassNotFoundException, IllegalAccessException, InstantiationException {
ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader();
System.out.print("当前线程的类加载器:");
System.out.println(currentClassLoader);
URL[] urls = new URL[1];
urls[0] = new URL("file:///Users/zhangcheng/test/");
URLClassLoader urlClassLoader1 = new URLClassLoader(urls);
System.out.print("urlClassLoader1的父 类加载器:");
System.out.println(urlClassLoader1.getParent());
URLClassLoader urlClassLoader2 = new URLClassLoader(urls);
System.out.print("urlClassLoader2的父 类加载器:");
System.out.println(urlClassLoader2.getParent());
Object myTestBean1 = urlClassLoader1.loadClass("com.myspring.service.impl.MyTestBean").newInstance();
Object myTestBean2 = urlClassLoader2.loadClass("com.myspring.service.impl.MyTestBean").newInstance();
//两个实例肯定是不等的
System.out.print("myTestBean1与myTestBean2是否相等:");
System.out.println(myTestBean1 == myTestBean2);
Assert.assertNotEquals(myTestBean1,myTestBean2);
Class c1 = myTestBean1.getClass();
Class c2 = myTestBean2.getClass();
System.out.print("加载c1的类加载器:");
System.out.println(c1.getClassLoader());
System.out.print("加载c2的类加载器:");
System.out.println(c2.getClassLoader());
System.out.print("c1与c2是否相等:");
System.out.println(c1 == c2);
Assert.assertNotEquals(c1,c2);
}
运行结果如下:
当前线程的类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
urlClassLoader1的父 类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
urlClassLoader2的父 类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
myTestBean1与myTestBean2是否相等:false
加载c1的类加载器:java.net.URLClassLoader@dcf3e99
加载c2的类加载器:java.net.URLClassLoader@7dc5e7b4
c1与c2是否相等:false
这下就对了吧!这里myTestBean1与myTestBean2的两个实例对应的Class对象就不是同一个了!两个Class对象是由两个不同的类加载器URLClassLoader去加载file:///Users/zhangcheng/test/
目下的MyTestBean.class文件得到的。
同一个MyTestBean.class文件,由两个不同的类加载器加载,得到的就是两个不同的Class对象,此时MyTestBean的Class对象在JVM中是不唯一的。
4、指定自定义类加载器体验
首先我们自定义一个类加载器MyClassLoader,它不完全符合委派机制,它可以指定一些类直接自己先加载,不需要委托给父 类加载器加载;
这里我们不用像第3节那样删除file:...(省略)/target/classes/
目录下的MyTestBean.class文件,直接运行就可以了。
public class MyClassLoader extends ClassLoader {
//用于读取.Class文件的路径
private String swapPath;
//用于标记这些name的类是先由自身加载的
private Set<String> useMyClassLoaderLoad;
public MyClassLoader(String swapPath, Set<String> useMyClassLoaderLoad) {
this.swapPath = swapPath;
this.useMyClassLoaderLoad = useMyClassLoaderLoad;
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
Class<?> c = findLoadedClass(name);
if (c == null && useMyClassLoaderLoad.contains(name)) {
//特殊的类让我自己加载
c = findClass(name);
if (c != null) {
return c;
}
}
return super.loadClass(name);
}
@Override
protected Class<?> findClass(String name) {
//根据文件系统路径加载class文件,并返回byte数组
byte[] classBytes = getClassByte(name);
//调用ClassLoader提供的方法,将二进制数组转换成Class类的实例
return defineClass(name, classBytes, 0, classBytes.length);
}
private byte[] getClassByte(String name) {
String className = name.substring(name.lastIndexOf('.') + 1, name.length()) + ".class";
try {
FileInputStream fileInputStream = new FileInputStream(swapPath + className);
byte[] buffer = new byte[1024];
int length = 0;
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
while ((length = fileInputStream.read(buffer)) > 0) {
byteArrayOutputStream.write(buffer, 0, length);
}
return byteArrayOutputStream.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return new byte[]{};
}
}
新建测试类添加测试
@Test
public void test() throws ClassNotFoundException, IllegalAccessException, InstantiationException {
ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader();
System.out.print("当前线程的类加载器:");
System.out.println(currentClassLoader);
String classPath = "/Users/zhangcheng/test/com/myspring/service/impl/";
Set<String> stringSet = new HashSet<>();
stringSet.add("com.myspring.service.impl.MyTestBean");
ClassLoader classLoader1 = new MyClassLoader(classPath, stringSet);
ClassLoader classLoader2 = new MyClassLoader(classPath, stringSet);
Object myTestBean1 = classLoader1.loadClass("com.myspring.service.impl.MyTestBean").newInstance();
Object myTestBean2 = classLoader2.loadClass("com.myspring.service.impl.MyTestBean").newInstance();
//两个实例肯定是不等的
System.out.print("myTestBean1与myTestBean2是否相等:");
System.out.println(myTestBean1 == myTestBean2);
Assert.assertNotEquals(myTestBean1, myTestBean2);
Class c1 = myTestBean1.getClass();
Class c2 = myTestBean2.getClass();
System.out.print("加载c1的类加载器:");
System.out.println(c1.getClassLoader());
System.out.print("加载c2的类加载器:");
System.out.println(c2.getClassLoader());
System.out.print("c1与c2是否相等:");
System.out.println(c1 == c2);
Assert.assertNotEquals(c1, c2);
}
运行结果:
当前线程的类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
myTestBean1与myTestBean2是否相等:false
加载c1的类加载器:com.myspring.test.MyClassLoader@6d9c638
加载c2的类加载器:com.myspring.test.MyClassLoader@1ee0005
c1与c2是否相等:false
看到没,通过这种方式,我们使用了两个不同的类加载器去加载了同一个class文件,得到了两个不同的Class对象,此时MyTestBean的Class对象在JVM中是不唯一的。
第4节里有一个细节
Object myTestBean1 = classLoader1.loadClass("com.myspring.service.impl.MyTestBean").newInstance();
Object myTestBean2 = classLoader2.loadClass("com.myspring.service.impl.MyTestBean").newInstance();
我并没有将他们强转为MyTestBean,为什么呢?假如我们在上面测试类修改一下为MyTestBean myTestBean1 = (MyTestBean)classLoader1.loadClass("com.myspring.service.impl.MyTestBean").newInstance()
,运行后发现报异常了。
java.lang.ClassCastException: com.myspring.service.impl.MyTestBean cannot be cast to com.myspring.service.impl.MyTestBean
这是因为强转(MyTestBean)
中的MyTestBean的Class对象是由当前测试类的类加载器加载的(隐式加载),而classLoader1.loadClass("com.myspring.service.impl.MyTestBean")
得到的这个Class对象是由classLoader1加载得来的,Class对象都不一样,这是两个不同的类,所以强转会报异常。
总结
在第1节和第2节我们看到,两个实例myTestBean1和myTestBean2对应的class对象是相同的,这样有一个好处就是可以避免类的重复加载。
而在第3节通过指定父 类加载器没有的URL,第4节自定义类加载器打破委托机制,两个实例myTestBean1和myTestBean2对应的class对象是不相同的,这样有一个好处就是资源隔离,比如说我就是有两个类都叫MyTestBean,虽然包名、名字等等什么一样,但是他们内部的实现机制就是不一样,这时候就可以使用类似于第3节第4节的方式来做资源隔离。
其实对于任意一个Class对象,都需要由它的类加载器和这个类本身一同确定其在就Java虚拟机中的唯一性,也就是说,即使两个Class对象来源于同一个class文件,只要加载它们的类加载器不同,那这两个Class对象就必定不相等。
- 零基础学编程041:欧拉公式的几何意义
- 零基础学编程040:在Windows上安装Python库的正确姿势
- c++/c 获取cpp文件行号跟文件名
- 零基础学编程042:画函数图像
- C-SATS工程副总裁教你如何用TensorFlow分类图像 part2
- C++11 Lambda表达汇总总结
- TensorFlow开发环境搭建(Ubuntu16.04+GPU+TensorFlow源码编译)
- C++虚析构函数解析
- C-SATS工程副总裁教你如何用TensorFlow分类图像 part1
- 帝国cms文章页调用当前文章URL如何操作?
- dedecms文章页调用地址(当前文章URL)如何操作?
- 饭团开通一周,3人学会了比特币操作
- Sample K算法
- C#读取“我的文档”等特殊系统路径及环境变量
- java教程
- Java快速入门
- Java 开发环境配置
- Java基本语法
- Java 对象和类
- Java 基本数据类型
- Java 变量类型
- Java 修饰符
- Java 运算符
- Java 循环结构
- Java 分支结构
- Java Number类
- Java Character类
- Java String类
- Java StringBuffer和StringBuilder类
- Java 数组
- Java 日期时间
- Java 正则表达式
- Java 方法
- Java 流(Stream)、文件(File)和IO
- Java 异常处理
- Java 继承
- Java 重写(Override)与重载(Overload)
- Java 多态
- Java 抽象类
- Java 封装
- Java 接口
- Java 包(package)
- Java 数据结构
- Java 集合框架
- Java 泛型
- Java 序列化
- Java 网络编程
- Java 发送邮件
- Java 多线程编程
- Java Applet基础
- Java 文档注释
- cURL-7.72.0(scheme)
- cURL-7.72.0初体验(参数写法)
- 21道前端面试题,值得收藏~
- OpenCV4.4 CUDA编译与加速全解析
- JavaScript中的Event Loop机制详解(前端必看)
- HDFS集群缩容案例: Decommission DataNode
- 应用深度学习进行乳腺癌检测
- 为什么你的数据仓库项目推进不下去?
- 19个有趣的Linux 命令,最后一个?... 打死我都不敢尝试!
- SpringBoot 整合 Quartz 实现 JAVA 定时任务的动态配置
- 使用 IntelliJ IDEA 查看类图,内容极度舒适
- 精选10款谷歌浏览器插件武装你的浏览器
- 王者荣耀为什么不使用微服务架构?
- Dubbo 时间轮
- Spring Boot 无侵入式 实现API接口统一JSON格式返回