前文已经描述Java源文件经过前端编译器后变成字节码文件,字节码文件通过类加载器的类加载机制在Java虚拟机中生成Class对象
前文深入浅出JVM(六)之前端编译过程与语法糖原理重点描述过编译的过程
前文深入浅出JVM(三)之HotSpot虚拟机类加载机制重点描述过类加载机制的过程
本篇文章将重点聊聊类加载器,围绕类加载器深入浅出的解析类加载器的分类、种类、双亲委派模型以及从源码方面推导出我们的结论
什么是类加载器?
类加载器通过类的全限定类名进行类加载机制从而生成Class对象
Class对象中包含该类相关类信息,通过Class对象能够使用反射在运行时阶段动态做一些事情
显示加载与隐式加载
类加载器有两种方式进行加载,一种是在代码层面显示的调用,另一种是当程序遇到创建对象等命令时自行判断该类是否进行过加载,未加载就先进行类加载
显示加载:显示调用ClassLoader加载class对象
隐式加载:不显示调用ClassLoader加载class对象(因为虚拟机会在第一次使用到某个类时自动加载这个类)
//显示类加载 第7章虚拟机类加载机制.User为全限定类名(包名+类名)
Class.forName("第7章虚拟机类加载机制.User");
//隐式类加载
new User();
唯一性与命名空间
判断两个类是否完全相同可能并不是我们自认为理解的那样,类在JVM中的唯一性需要根据类本身和加载它的类加载器
基本特征
类加载器中有一些基本特性,比如子类加载器可以访问父类加载器所加载的类、父类加载过的类子类不再加载、双亲委派模型等
类加载器可以分成两种,一种是引导类由非Java语言实现的,另一种是由Java语言实现的自定义类加载器
ClassLoader
类派生的类加载器类(包括扩展类,系统类,程序员自定义加载器等)
系统(应用程序)类加载器和扩展类加载器是Launcher的内部类,它们间接实现了ClassLoader
注意
平常说的系统(应用程序)类加载器的父类加载器是扩展类加载器,而扩展类加载器的父类加载器是启动类加载器,都是"逻辑"上的父类加载器
实际上扩展类加载器和系统(应用程序)类加载器间接继承的ClassLoader
中有一个字段parent
用来表示自己的逻辑父类加载器
<JAVA_HOME>\lib\部分jar包
java.lang.ClassLoader
,没有父类加载器<JAVA_HOME>\lib\ext*.jar
java.lang.ClassLoader
,父类加载器为启动类加载器java.lang.ClassLoader
,父类加载器为扩展类加载器ClassLoader.getSystemClassLoader()
获得通过代码来演示:
public class TestClassLoader {
public static void main(String[] args) {
URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
/*
启动类加载器能加载的api路径:
file:/D:/Environment/jdk1.8.0_191/jre/lib/resources.jar
file:/D:/Environment/jdk1.8.0_191/jre/lib/rt.jar
file:/D:/Environment/jdk1.8.0_191/jre/lib/sunrsasign.jar
file:/D:/Environment/jdk1.8.0_191/jre/lib/jsse.jar
file:/D:/Environment/jdk1.8.0_191/jre/lib/jce.jar
file:/D:/Environment/jdk1.8.0_191/jre/lib/charsets.jar
file:/D:/Environment/jdk1.8.0_191/jre/lib/jfr.jar
file:/D:/Environment/jdk1.8.0_191/jre/classes
*/
System.out.println("启动类加载器能加载的api路径:");
for (URL urL : urLs) {
System.out.println(urL);
}
/*
扩展类加载器能加载的api路径:
D:\Environment\jdk1.8.0_191\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext
*/
System.out.println("扩展类加载器能加载的api路径:");
String property = System.getProperty("java.ext.dirs");
System.out.println(property);
//加载我们自定义类的类加载器是AppClassLoader,它是Launcher的内部类
ClassLoader appClassLoader = TestClassLoader.class.getClassLoader();
//sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(appClassLoader);
//AppClassLoader的上一层加载器是ExtClassLoader,它也是Launcher的内部类
ClassLoader extClassloader = appClassLoader.getParent();
//sun.misc.Launcher$ExtClassLoader@511d50c0
System.out.println(extClassloader);
//实际上是启动类加载器,因为它是c/c++写的,所以显示null
ClassLoader bootClassloader = extClassloader.getParent();
//null
System.out.println(bootClassloader);
//1号测试:基本类型数组 的类加载器
int[] ints = new int[10];
//null
System.out.println(ints.getClass().getClassLoader());
//2号测试:系统提供的引用类型数组 的类加载器
String[] strings = new String[10];
//null
System.out.println(strings.getClass().getClassLoader());
//3号测试:自定义引用类型数组 的类加载器
TestClassLoader[] testClassLoaderArray = new TestClassLoader[10];
//sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(testClassLoaderArray.getClass().getClassLoader());
//4号测试:线程上下文的类加载器
//sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(Thread.currentThread().getContextClassLoader());
}
}
从上面可以得出结论
ClassLoader
中的官方注释虚拟机自动生成的一个类,管理数组,会对这个类进行类加载
对数组类类加载器是数组元素的类加载器
如果数组元素是基本类型则不会有类加载器
loadClass()
ClassLoader
的 loadClass
方法(双亲委派模型的源码)
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
//参数resolve:是否要解析类
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
//加锁同步 保证只加载一次
synchronized (getClassLoadingLock(name)) {
// 首先检查这个class是否已经加载过了
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// c==null表示没有加载,如果有父类的加载器则让父类加载器加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//如果父类的加载器为空 则说明递归到bootStrapClassloader了
//则委托给BootStrap加载器加载
//bootStrapClassloader比较特殊无法通过get获取
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//父类无法加载抛出异常
}
//如果父类加载器仍然没有加载过,则尝试自己去加载class
if (c == null) {
long t1 = System.nanoTime();
c = findClass(name);
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
//是否要解析
if (resolve) {
resolveClass(c);
}
return c;
}
}
先递归交给父类加载器去加载,父类加载器未加载再由自己加载
findClass()
ClassLoader
的findClass()
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
由子类URLClassLoader
重写findClass去寻找类的规则
最后都会来到defineClass()
方法
defineClass()
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
根据从off开始长度为len定字节数组b转换为Class实例
在自定义类加载器时,覆盖findClass()
编写加载规则,取得要加载的类的字节码后转换为流调用defineClass()
生成Class对象
resolveClass()
protected final void resolveClass(Class<?> c) {
resolveClass0(c);
}
使用该方法可以在生成Class对象后,解析类(符号引用 -> 直接引用)
findLoadedClass()
protected final Class<?> findLoadedClass(String name) {
if (!checkName(name))
return null;
return findLoadedClass0(name);
}
如果加载过某个类则返回Class对象否则返回null
双亲委派模型源码实现对应ClassLoader
的loadClass()
可是我明明写了main方法
这是因为类装载器的双亲委派模型
很明显这里的报错是因为它找到的是启动类加载器中的java.lang.String而不是在应用程序类加载器中的java.lang.String(我们写的)
而且核心类库的包名也是被禁止使用的
类装载器的加载机制:启动类加载器->扩展类加载器->应用程序类加载器
loadClass
不使用双亲委派模型是否就能够用自定义类加载器加载核心类库了呢? JDK为核心类库提供一层保护机制,不管用什么类加载器最终都会调用defineClass()
,该方法会执行preDefineClass()
,它提供对JDK核心类库的保护
ClassLoader
类loadClass
方法,也可以覆写findClass
方法findClass
方法,因为loadClass是双亲委派模型实现的方法,其中父类类加载器加载不到时会调用findClass
尝试自己加载loadClass
方法来实现类加载自定义类加载器代码
public class MyClassLoader extends ClassLoader {
/**
* 字节码文件路径
*/
private final String codeClassPath;
public MyClassLoader(String codeClassPath) {
this.codeClassPath = codeClassPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//字节码文件完全路径
String path = codeClassPath + name + ".class";
System.out.println(path);
Class<?> aClass = null;
try (
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(path));
ByteArrayOutputStream baos = new ByteArrayOutputStream()
) {
int len = -1;
byte[] bytes = new byte[1024];
while ((len = bis.read(bytes)) != -1) {
baos.write(bytes,0,len);
}
byte[] classCode = baos.toByteArray();
//用字节码流 创建 Class对象
aClass = defineClass(null, classCode, 0, classCode.length);
} catch (IOException e) {
e.printStackTrace();
}
return aClass;
}
}
客户端调用自定义类加载器加载类
public class Client {
public static void main(String[] args) {
MyClassLoader myClassLoader = new MyClassLoader("C:\");
try {
Class<?> classLoader = myClassLoader.loadClass("HotTest");
System.out.println("类加载器为:" + classLoader.getClassLoader().getClass().getName());
System.out.println("父类加载器为" + classLoader.getClassLoader().getParent().getClass().getName());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
记得对要加载的类先进行编译
解释如果类加载器不同那么它们肯定不是同一个类
MyClassLoader myClassLoader1 = new MyClassLoader("D:\代码\JavaVirtualMachineHotSpot\src\main\java\");
MyClassLoader myClassLoader2 = new MyClassLoader("D:\代码\JavaVirtualMachineHotSpot\src\main\java\");
try {
Class<?> aClass1 = myClassLoader1.findClass("HotTest");
Class<?> aClass2 = myClassLoader2.findClass("HotTest");
System.out.println(aClass1 == aClass2);//false
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
热替换: 服务不中断,修改会立即表现在运行的系统上
对Java来说,如果一个类被类加载器加载过了,就无法被再加载了
但是如果每次加载这个类的类加载不同,那么就可以实现热替换
还是使用上面写好的自定义类加载器
//测试热替换
try {
while (true){
MyClassLoader myClassLoader = new MyClassLoader("D:\代码\JavaVirtualMachineHotSpot\src\main\java\");
Class<?> aClass = myClassLoader.findClass("HotTest");
Method hot = aClass.getMethod("hot");
Object instance = aClass.newInstance();
Object invoke = hot.invoke(instance);
TimeUnit.SECONDS.sleep(3);
}
} catch (Exception e){
e.printStackTrace();
}
通过反射调用HotTest类的hot方法
中途修改hot方法并重新编译
本篇文章围绕类加载器深入浅出的解析类加载器的分类与种类、双亲委派模型、通过源码解析证实我们的观点、最后还自定义的类加载器和说明热替换
类加载器将字节码文件进行类加载机制生成Class对象从而加载到Java虚拟机中
类加载只会进行一次,能够显示调用执行或者在遇到创建对象的字节码命令时隐式判断是否进行过类加载
类加载器分为非Java语言实现的引导类加载器和Java语言实现的自定义类加载器,其中JDK中实现了自定义类加载器中的扩展类加载器和系统类加载器
引导类加载器用来加载Java的核心类库,它的子类扩展类加载器用来加载扩展类,扩展类的子类系统类加载器常用于加载程序中自定义的类(这里的父子类是逻辑的,并不是代码层面的继承)
双亲委派模型让父类加载器优先进行加载,无法加载再交给子类加载器进行加载;通过双亲委派模型和沙箱安全机制来保护核心类库不被其他恶意代码替代
基本类型不需要类加载、数组类型的类加载器是数组元素的类加载器、线程上下文类加载器是系统类加载器
由于类和类加载器才能确定JVM中的唯一性,每次加载类的类加载不同时就能够多次进行类加载从而实现在运行时修改的热替换
本篇文章将被收入JVM专栏,觉得不错感兴趣的同学可以收藏专栏哟~
觉得菜菜写的不错,可以点赞、关注支持哟~
有什么问题可以在评论区交流喔~
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。