1. 使用类加载器加载类的过程
通过ClassLoader源码分析 ClassLoader.loadClass()方法得知加载类有以下三个过程。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
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) {
// If still not found, then invoke findClass in order
// to find the class.
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;
}
}
1.1 Class<?> c = findLoadedClass(name);
如果Java虚拟机已将此加载程序记录为具有该二进制名称的类的初始加载程序,则返回具有给定二进制名称的类。
1.2 parent.loadClass(name, false);
这里就体现出双亲委托是如何实现的,不管是什么类加载器加载,会先抛给parent类加载器尝试加载,
1.3 c = findClass(name);
如果从下致上都不无法加载该类,则将从上致下开始尝试加载,如果到最底层都无法加载,则抛出异常,类找不到。
2. 自定义类加载器
所有的自定义类加载器都需要 extends ClassLoader,一般只需要重写 findClass()方法即可,在方法中,根据指定文件名在指定的路径下获取文件流信息,然后交于jvm底层 defineClass()方法处理即可。
以下是自定义的类加载器
package jvmtest;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.text.MessageFormat;
import java.util.Optional;
class CustomClassLoader extends ClassLoader {
/**
* 类加载器名称
*/
private String classLoaderName;
/**
* 类加载器根目录
*/
private String path;
private static final String suffixName = ".class";
/**
* 默认以systemClassLoader为父类加载器
*
* @param classLoaderName
*/
public CustomClassLoader(String classLoaderName) {
super();
this.classLoaderName = classLoaderName;
}
/**
* 使用传入的classlaoder作为其双亲
*
* @param parent
* @param classLoaderName
*/
public CustomClassLoader(ClassLoader parent, String classLoaderName) {
super(parent);
this.classLoaderName = classLoaderName;
}
/**
* Finds the class with the specified <a href="#name">binary name</a>.
* This method should be overridden by class loader implementations that
* follow the delegation model for loading classes, and will be invoked by
* the {@link #loadClass <tt>loadClass</tt>} method after checking the
* parent class loader for the requested class. The default implementation
* throws a <tt>ClassNotFoundException</tt>.
*
* @param name The <a href="#name">binary name</a> of the class
* @return The resulting <tt>Class</tt> object
* @throws ClassNotFoundException If the class could not be found
* @since 1.2
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
System.out.println("findClass..");
byte[] bytes = this.loadClassData(name);
return this.defineClass(name, bytes, 0, bytes.length);
}
/**
* 加载指定className对应的文件,返回对应的数据,这里注意使用 read()读取class文件内容时,只能一个一个字节的读取,不能一次多个。
*
* @param className
* @return
*/
private byte[] loadClassData(String className) {
className = className.replace(".", "/");
byte[] readDatas = null;
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
FileInputStream fileInputStream = new FileInputStream(new File(Optional.ofNullable(this.path).orElse("") + className + suffixName));) {
int result;
while ((result = fileInputStream.read()) != -1) {
byteArrayOutputStream.write(result);
}
/*byte[] datas = new byte[1];
while (fileInputStream.read(datas) != -1) {
byteArrayOutputStream.write(datas);
}*/
readDatas = byteArrayOutputStream.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}
return readDatas;
}
@Override
public String toString() {
return MessageFormat.format("[{0}]", this.classLoaderName);
}
public void setPath(String path) {
this.path = path;
}
}
测试类
package jvmtest;
/**
* @author ztkj-hzb
* @Date 2019/12/13 14:48
* @Description
*/
public class Test {
public static void main(String[] args) throws ClassNotFoundException {
CustomClassLoader classLoader = new CustomClassLoader("customClassLoader");
Class<?> loadClass = classLoader.loadClass("jvmtest.Test1");
System.out.println(loadClass.getClassLoader());
}
}
执行测试类,会看到一下结果
sun.misc.Launcher$AppClassLoader@18b4aac2
从结果上看,加载该类的类加载器并不是我们写的自定义的类加载器,而是系统类加载器。因为代码里加载的类 "jvmtest.Test1" 在项目中,会被AppClassLoader所加载,那么怎么使用自己写的类加载器加载该类呢。
3. 如何让自定义类加载器加载执行路径下的指定类
将编译后的Test1.class文件移动到另一个指定目录,例如桌面中(需要在测试类中修改对应的类加载器路径),然后删除原来文件夹中编译的Test1.class文件,再次执行程序
以下是新的测试类
public static void main(String[] args) throws ClassNotFoundException {
CustomClassLoader classLoader = new CustomClassLoader("customClassLoader");
//需要指定类加载器是从什么路径下寻找指定类文件的
classLoader.setPath("D:/");
Class<?> loadClass = classLoader.loadClass("jvmtest.Test1");
System.out.println(loadClass.getClassLoader());
}
执行测试类,得出以下的结果
findClass..
[customClassLoader]
从结果上看可以得出,Test1类是被我们自定义的类加载器所加载的。分析一下,为什么?
首先,这里我们在构建 CustomClassLoader 类加载器的时候,没有指定parentClassLoader是什么类加载器,这里会调用super(name),那么ClassLoader会默认使用AppClassLoader来作为默认的类加载器。
所以,这里会先被AppClassLoader加载,由于双亲委托机制的存在,会逐渐的由扩展类,启动类等类加载器加载,而我们可知,该类最终会被AppClassLoader类尝试加载,但是由于上面,我们将Test1.class文件在执行的编译文件中删除掉了,导致运行中AppClassLoader找不到该类,则会让我们自定义的类加载器来尝试进行,这时候,查看源码可以得知,会触发了loadClass()的第三步,findClass(),由于自定义的类加载器重写了findClass()方法,就进入了我们的findClass()方法,即会打印出我们的结果第一行数据, findClass....
随后,在指定path目录下找到了指定文件,获取内容后,交由ClassLoader封装成Class对象,返回。
4. 数组不同于其他数据类型,加载数组类型的类加载器是谁呢?
数组不同于其他数据类型,因为数组没有实际类型,而是在运行时jvm构建的,但是加载数组类型的类加载器是谁呢?
举例而说,假如存在一个变量 Object[] objectArr , 加载 objectArr的类加载器其实是由加载Object类的类加载器一致,即启动类加载器。
这里还需要区分数据类型是基础数据类型还是引用类型,如果是基础数据类型,即原始类型(4类8中种,出了String),jvm规定他们的类加载器为null,并不代表就是启动类加载器。
而引用类型按照正常规则来即可。
举例如下:
package jvmtest;
/**
* @author ztkj-hzb
* @Date 2019/12/9 13:56
* @Description 针对数组类型而言,数组类型的类加载器是有其对象类型对应的类加载器来决定的,例如 Xxx[] 的类加载器是有Xxx类的类加载器来决定的。
* 针对Integer[] 和 int[]也有区别,虽然结果都是null,但是Integer是有启动类加载器加载,所以返回null,而int是原生类型,原生类型没有类加载器,所以
* 返回null
*/
public class Test14 {
public static void main(String[] args) {
System.out.println(Object[].class);
System.out.println(Object[].class.getSuperclass());
int[] intArr = new int[5];
System.out.println(intArr.getClass().getClassLoader());
Integer[] integersArr = new Integer[5];
System.out.println(integersArr.getClass().getClassLoader());
MyTest14[] myTest14s = new MyTest14[5];
System.out.println(myTest14s.getClass().getClassLoader());
}
}
class MyTest14{
}
5. 关于类加载器的父子加载区别
这里的父子关系,并不是指的是待加载的类的继承关系,而是指的是类加载器的逻辑父子关系。
例如加载 A类的类加载器是自定义类加载器,加载 B类的类加载器是App类加载器,则可以在A类中调用B类,而不能在B类中调用A类,因为B类由AppClassLoader加载,属于自定义类加载器的逻辑父类,因为命名空间的存在,是无法找到由子类加载器加载的类文件。
5.1 代码原始版本
package jvmtest;
/**
* @author ztkj-hzb
* @Date 2019/12/12 17:09
* @Description
*/
public class MySample {
public MySample(){
System.out.println("MySample类加载器:" + this.getClass().getClassLoader());
//实例化MyCat
new MyCat();
}
}
package jvmtest;
/**
* @author ztkj-hzb
* @Date 2019/12/12 17:09
* @Description
*/
public class MyCat {
public MyCat(){
System.out.println("MyCat类加载器:" + this.getClass().getClassLoader());
}
}
package jvmtest;
/**
* @author ztkj-hzb
* @Date 2019/12/12 11:47
* @Description
*/
public class Test16 {
public static void main(String[] args) throws Exception {
MyTest15ClassLoader classLoader = new MyTest15ClassLoader("loader1");
classLoader.setPath("D:/");
Class<?> loadClass = classLoader.loadClass("jvmtest.MySample");
System.out.println("classLoader: " + loadClass.getClassLoader());
//实例化MySample类
Object instance = loadClass.newInstance();
}
}
结果如下:
classLoader: sun.misc.Launcher$AppClassLoader@18b4aac2
MySample类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
MyCat类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
由结果分析可知,因为创建 MyTest15ClassLoader 类的时候没有指定父加载器,所以这里使用默认的父加载器(系统类加载器)。而 MySample类本来就在当前项目编译的class中存在,所以,这里使用的是系统类加载器加载,没有用到自定义的类加载器。
5.2 变动1:在当前项目的编译的文件夹中删除MySample.class文件
在当前项目的编译的文件夹中删除MySample.class文件,再次执行代码,得到如下结果:
findClass..
classLoader: [loader1]
MySample类加载器:[loader1]
MyCat类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
由结果分析可知,由于在当前Runtime环境中,删除了MySample.class文件,所以首先通过系统类加载器尝试加载该类,找不到,则会使用到自定义的类加载器尝试加载,只要在指定目录中放入MySample.class文件,由自定义加载器找到且加载编译成Class对象,因此可以得到输出结果的前两条数据。然后因为调用了 newInstance()实例化方法,触发了 MySample类的初始化,执行构造方法,于是输出了第三条数据,在MySample类的构造方法中,触发了MyCat类的初始化,导致自定义类加载器开始加载MyCat类,同理,首先会尝试使用系统类加载器来加载MyCat.class,由于当前Runtime环境中,存在MyCat.class文件,所以,会被系统类加载器所加载该类,因此输出了最后一条数据。
这里体现出了,可以由子类加载器加载父类加载器的类。
5.3 变动2:在当前项目的编译的文件夹中删除MyCat.class文件
在当前项目的编译的文件夹中删除MyCat.class文件,再次执行代码,得到如下结果:
classLoader: sun.misc.Launcher$AppClassLoader@18b4aac2
MySample类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
Exception in thread "main" java.lang.NoClassDefFoundError: jvmtest/MyCat
at jvmtest.MySample.<init>(MySample.java:14)
at java.lang.Class.newInstance(Class.java:442)
at jvmtest.Test16.main(Test16.java:20)
Caused by: java.lang.ClassNotFoundException: jvmtest.MyCat
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
... 7 more
由结果分析可知,首先,在Runtime环境中,存在MySample.class文件,所以必然会被系统类加载器所加载,即出现第一行数据,执行 newInstant方法后,触发MySample类的初始化,输出第二行数据,然后在MySample类的构造方法中触发了MyCat类的初始化,即使用当前的类加载器加载MyCat类,但是在之前,已经将MyCat.class文件从Runtime环境中删除了,所以在系统类加载器对应的命名空间中,找不到MyCat类,就会导致类找不到异常。因此报错。
这里体现出了,不能由父类加载器加载子类加载器命名空间的类。这就体现出了命名空间的特性。
5.4 变动3:在当前项目的编译的文件夹中同时删除MySample.class和MyCat.class文件
在当前项目的编译的文件夹中同时删除MySimple.class和MyCat.class文件,再次执行代码,得到如下结果:
findClass..
classLoader: [loader1]
MySample类加载器:[loader1]
findClass..
MyCat类加载器:[loader1]
由结果分析可知,首先,在Runtime环境中,不存在MySample.class文件,所以会被被自定义类加载器所加载,即出现第一行,第二行数据,执行 newInstant方法后,触发MySample类的初始化,出现第三行,同理,使用自定义类加载器加载MyCat.class,根据双亲委托机制,在系统类加载器命名空间中无法找到MyCat类,因此还是由自定义类加载器加载,出现第四行和第五行数据。