类加载器是 JVM 执行类加载机制的前提
作用:
ClassLoader 是 Java 的核心组件,所有的 class 都是由 ClassLoader 进行加载的,ClassLoader 负责通过各种方式将 class 信息的二进制数据流读入 JVM 内部,转换为一个与目标类对应的 java.lang.Class 对象实例。然后交给 Java 虚拟机尽心链接、初始化等操作。因此,ClassLoader 在整个装载阶段,只能影响到类的加载,而无法通过 ClassLoader 去改变类的链接和初始化行为。至于它是否可以运行,则由 Execution Engine 决定
类加载器最早出现在 Java 1.0 版本中,那个时候只是单纯地为了满足 Java Applet 应用而被研发出来,但如今类加载器却在 OSGI、字节码加解密领域大放异彩。这主要归功于 Java 虚拟机的设计者们当初在设计类加载器的时候,并没有考虑将它绑定在 JVM 内部,这样做的好处就是能够更加灵活和动态地执行类加载操作
类的加载分类:显式加载 vs 隐式加载
class 文件的显式加载与隐式加载的方式是指 JVM 加载 Ccass 文件到内存的方式
在日常开发中以上两种方式一般会混合使用
类加载器的必要性
一般情况下,Java 开发人员并不需要在程序中显式地使用类加载器,但是了解类加载器的加载机制却显得至关重要。从以下几个方面说:
概念
不同类加载器的命名空间关系
每个类加载器都有各自的命名空间,命名空间由该加载器及所有父加载器所加载的类组成。
在同一个命名空间中,不会出现全类名相同的两个类。
在不同的命名空间中,有可能出现全类名相同的两个类。
命名空间是类加载器中一个很重要的概念,对于只要学过java的人都知道java万物皆对象,在java中即使是一个“.class”文件,通过类加载器加载到虚拟机内存,那么在内存中会生成一个对应的Class对象
那么问题又来了,你应该听说过,一个类在内存中只能有一个Class对象,那么真的是这样吗?没有任何前提吗?接下类我们就来详细的分析一下,为什么那么多人说同一个类在内存中有且只有一个Class对象?真的是这样吗?
首先先来介绍一下类加载器,只有了解了类加载器的概念你才能理解我接下来说的。
在java中一共有三种类加载器,如果也可以说是四种,因为还有一种是我们的自定义类加载器,需要我们自己实现,
接下来我们来理解一下类加载器的双亲委派机制
当一个类加载器尝试加载某一个类之前会先委派给它的父类加载器,以此类推,如果父类加载器加载不了才会自己加载,如果自己也加载不了,这时候就会抛出异常
平时我们自己写的类都是由系统类加载器负责加载,所以平时我们写的类信息都保存在系统类加载器的命名空间中
命名空间:
命名空间是由该类加载器以及其父类加载器所构成的,其中父类加载器加载的类对其子类可见,但是反过来子类加载的类对父类不可见,同一个命名空间中一定不会出现同一个类(全限定名一模一样的类)多个Class对象,换句话说就是在同一命名空间中只能存在一个Class对象,所以当你听别人说在内存中同一类的Class对象只有一个时其实指的是同一命名空间中,当然也不排除他压根就不知道这个概念。
到这里你应该知道,同一命名空间一个类的Class对象只有一个,那么不同的命名空间呢?看来你能想到这个问题以及很厉害了
接下来我们来看一个异常
可能你一眼就看出来了,这不就是一个类型转换异常吗?是的!没错!但你看看我圈中的两条信息,发现没有?明明是同一类,而java却告诉我不能转换?这是什么鬼?
当然只看这个异常你是看不懂的,接下类我把代码贴出来,并详细的分析一下
这是一个自定义的类加载器,path为成员属性,来指定这个类加载器负责加载的classPath
extName为扩展的名是一个常量指定为“.class”,然后通过defineClass来返回一个Class对象
这是Studnt类
这是测试类
我们先不讨论异常的事,我们先把这段代码给说清楚
实例化了两个自定义类加载器 loader1和loader2,分别将他们的classPath指定到我电脑的G盘下,
然后通过loader1和loader2分别加载Student类,正常来说这是没有任何问题的。而且还会输出true,先来讲讲为什么没有问题,只有理解了为什么不会出现问题,才能理解为什么出问题
按照类加载器的层级关系 自定义类加载器->系统类加载器->扩展类加载器->根类加载器 是按照这种层级关系来的,这是的层级关系并不是通过继承体现的,而是在自类加载器的内部有个成员属性保存了父类加载器
然后我们知道类加载器是有双亲委派机制的,那么loader1加载student类之前一定会去让它的父类,也就是系统类加载器去加载,系统类加载器然后又让它的父类加载,,以此类推,还是由最后系统类加载器加载,因为它的父类都加载不了,
那么loader2去加载的时候也会按照上面那种流程,但是会先判断这个类是否已经加载过了,如果加载过了就直接返回这个类的Class对象,很显然Student已经被系统类加载器加载过了,所以clazz1和clazz2都代表同一个对象,他们肯定是相同的,然后调用方法本来传入的就是Student对象 ,向下转型也是没问题的,那么我刚刚那个异常是怎么导致的呢?
细心的读者应该会发现,在那个异常信息的上面还打印了一个false。
什么?false?他们不是同一个Class对象吗?接下来看我动了哪些手脚
通过System.property("java.class.path")获取到系统类加载器的classPath当然使用Idea的话编译后的代码是存放在out这个目录的,用idea的都知道,我这里只是因为idea上面有其他的项目所以才使用Eclipse写一下,Eclipse会存放在一个bin目录下,如果实在不知道可以通过上面那行代码确定一下
有没有发现什么问题?我把Student的class字节码文件删除了,
然后在我的G盘保存了一份,然后我通过我自己的类加载器来加载这个Student肯定是能加载的,但是这样的话系统类加载是无法加载的,因为当前字节码文件没有在系统类加载器的classPath中,所以只能由我们的自定义类加载器加载,然后我们通过loader1和loader2各自加载一个。但是此时,loader1和loader2是没有任何关系的,他们加载前只会去找它的父类,父类加载不了只能自己加载,而loader1和loader2没有任何关系,他们加载的类只能它们的子类可见,故他们各自加载了一个Student类,这就加载了两个Class对象,而且他们存放在不同的命名空间中,不同的命名空间中的对象是互不可见的,到这里你应该明白为什么是false了,但是类型转换异常又是怎么出来的呢?其实这个也很好理解,连Class对象都不同,那又怎么转换呢?而且他们还互不可见
摘自https://blog.csdn.net/yuge1123/article/details/99945983
作用
在大型应用中,我们往往借助这一特性,来运行同一个类的不同版本
类加载机制的基本特征
通常类加载机制有三个基本特征:
JVM 支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)
从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是 Java 虚拟机规范却没有这么定义,而是将所有派生于抽象类 ClassLoader 的类加载器都划分为自定义类加载器。无论类加载器的类型如何划分,在程序中我们最常见的类加载器结构主要是如下情况:
Bootstrap ClassLoader
Bootstrap ClassLoader为根类加载器,负责加载java的核心类库。根加载器不是ClassLoader的子类,是有C++实现的。
public class BootstrapTest {
public static void main(String[] args) {
//获取根类加载器所加载的全部URL数组
URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
Arrays.stream(urLs).forEach(System.out::println);
}
}
//输出结果
//file:/C:/SorftwareInstall/java/jdk/jre/lib/resources.jar
//file:/C:/SorftwareInstall/java/jdk/jre/lib/rt.jar
//file:/C:/SorftwareInstall/java/jdk/jre/lib/sunrsasign.jar
//file:/C:/SorftwareInstall/java/jdk/jre/lib/jsse.jar
//file:/C:/SorftwareInstall/java/jdk/jre/lib/jce.jar
//file:/C:/SorftwareInstall/java/jdk/jre/lib/charsets.jar
//file:/C:/SorftwareInstall/java/jdk/jre/lib/jfr.jar
//file:/C:/SorftwareInstall/java/jdk/jre/classes
根类加载器负责加载%JAVA_HOME%/jre/lib下的jar包(以及由虚拟机参数 -Xbootclasspath 指定的类)。
我们将rt.jar解压,可以看到我们经常使用的类库就在这个jar包中。
Extension ClassLoader
Extension ClassLoader为扩展类加载器,负责加载%JAVA_HOME%/jre/ext或者java.ext.dirs系统熟悉指定的目录的jar包。大家可以将自己写的工具包放到这个目录下,可以方便自己使用。
System ClassLoader
System ClassLoader为系统(应用)类加载器,负责加载加载来自java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH环境变量所指定的JAR包和类路径。程序可以通过ClassLoader.getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器默认都以系统类加载器作为父加载器。
类加载机制
JVM主要的类加载机制。
注意:类加载器之间的父子关系并不是类继承上的父子关系,而是实例之间的父子关系。
public class ClassloaderPropTest {
public static void main(String[] args) throws IOException {
//获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println("系统类加载器:" + systemClassLoader);
/*
获取系统类加载器的加载路径——通常由CLASSPATH环境变量指定,如果操作系统没有指定
CLASSPATH环境变量,则默认以当前路径作为系统类加载器的加载路径
*/
Enumeration<URL> eml = systemClassLoader.getResources("");
while (eml.hasMoreElements()) {
System.out.println(eml.nextElement());
}
//获取系统类加载器的父类加载器,得到扩展类加载器
ClassLoader extensionLoader = systemClassLoader.getParent();
System.out.println("系统类的父加载器是扩展类加载器:" + extensionLoader);
System.out.println("扩展类加载器的加载路径:" + System.getProperty("java.ext.dirs"));
System.out.println("扩展类加载器的parant:" + extensionLoader.getParent());
}
}
//输出结果
//系统类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
//file:/C:/ProjectTest/FengKuang/out/production/FengKuang/
//系统类的父加载器是扩展类加载器:sun.misc.Launcher$ExtClassLoader@1540e19d
//扩展类加载器的加载路径:C:SorftwareInstalljavajdkjrelibext;C:WINDOWSSunJavalibext
//扩展类加载器的parant:null
从输出中验证了:系统类加载器的父加载器是扩展类加载器。但输出中扩展类加载器的父加载器是null,这是因为父加载器不是java实现的,是C++实现的,所以获取不到。但扩展类加载器的父加载器是根加载器。
类加载流程图
图中红色部分,可以是我们自定义实现的类加载器来进行加载。
自定义类加载分析
除了根类加载器,所有类加载器都是ClassLoader的子类。所以我们可以通过继承ClassLoader来实现自己的类加载器。
ClassLoader类有两个关键的方法:
所以,如果要实现自定义类,可以重写这两个方法来实现。但推荐重写findClass方法,而不是重写loadClass方法,因为loadClass方法内部会调用findClass方法。
我们来看一下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 {
//第二步,判断父加载器是否为null
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
//第三步,如果前面都没有找到,就会调用findClass方法
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
loadClass加载方法流程:
所以,为了不影响类的加载过程,我们重写findClass方法即可简单方便的实现自定义类加载。
public class Hello {
public void test(String str){
System.out.println(str);
}
}
public class MyClassloader extends ClassLoader {
/**
* 读取文件内容
*
* @param fileName 文件名
* @return
*/
private byte[] getBytes(String fileName) throws IOException {
File file = new File(fileName);
long len = file.length();
byte[] raw = new byte[(int) len];
try (FileInputStream fin = new FileInputStream(file)) {
//一次性读取Class文件的全部二进制数据
int read = fin.read(raw);
if (read != len) {
throw new IOException("无法读取全部文件");
}
return raw;
}
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = null;
//将包路径的(.)替换为斜线(/)
String fileStub = name.replace(".", "/");
String classFileName = fileStub + ".class";
File classFile = new File(classFileName);
//如果Class文件存在,系统负责将该文件转换为Class对象
if (classFile.exists()) {
try {
//将Class文件的二进制数据读入数组
byte[] raw = getBytes(classFileName);
//调用ClassLoader的defineClass方法将二进制数据转换为Class对象
clazz = defineClass(name, raw, 0, raw.length);
} catch (IOException e) {
e.printStackTrace();
}
}
//如果clazz为null,表明加载失败,抛出异常
if (null == clazz) {
throw new ClassNotFoundException(name);
}
return clazz;
}
public static void main(String[] args) throws Exception {
String classPath = "loader.Hello";
MyClassloader myClassloader = new MyClassloader();
Class<?> aClass = myClassloader.loadClass(classPath);
Method main = aClass.getMethod("test", String.class);
System.out.println(main);
main.invoke(aClass.newInstance(), "Hello World");
}
}
//输出结果
//Hello World
ClassLoader还有一个重要的方法defineClass(String name, byte[] b, int off, int len)。此方法的作用是将class的二进制数组转换为Calss对象。
此例子很简单,我写了一个Hello测试类,并且编译过后放在了当前路径下(大家可以在findClass中加入判断,如果没有此文件,可以尝试查找.java文件,并进行编译得到.class文件;或者判断.java文件的最后更新时间大于.class文件最后更新时间,再进行重新编译等逻辑)。
摘自:https://zhuanlan.zhihu.com/p/108180758
对上文再进行刨析
class ClassLoader {
ClassLoader parent; //父类加载器
public ClassLoader(ClassLoader parent) {
this.parent = parent;
}
}
class ParentClassLoader extends ClassLoader {
public ParentClassLoader(ClassLoader parent) {
super(parent);
}
}
class ChildClassLoader extends ClassLoader {
public ChildClassLoader(ClassLoader parent) {
//parent = new ParentClassLoader();
super(parent);
}
}
引导类加载器
启动类加载器(引导类加载器)
扩展类加载器
系统类加载器
应用程序类加载器(系统类加载器,AppClassLoader)
系统类加载器
应用程序类加载器(系统类加载器,AppClassLoader)
用户自定义类加载器
借鉴于https://zhuanlan.zhihu.com/p/268637283
原文:https://www.cnblogs.com/llsj/p/14200942.html