在JVM中,一个类被加载到虚拟机这个过程包括有3个步骤,即加载、链接和初始化。而加载这个过程,就是由类加载器ClassLoader
进行加载的,类加载器天生就负责这个职责。java
Java自己给咱们提供了几种类型的类加载器,启动类加载器Bootstrap ClassLoader
、扩展类加载器Extension ClassLoader
、应用类加载器App ClassLoader
,除了上面3种,咱们也能够定义咱们本身的类加载器,此时咱们只须要继承ClassLoader
类,重写其findClass()
方法便可。数组
固然,有时候,你觉得的并非你觉得的,当咱们用自定义类加载器去加载类路径ClassPath
下某个class
文件时,而后调用该Class
对象的getClassLoader()
方法时,咱们会发现,加载该类的类加载器,并非咱们自定义的加载器,至于为何?下文将会讲到。 下图是JVM类加载器机制bash
一、启动类加载器Bootstrap ClassLoader
网络
启动类加载器,负责%JRE_HOME/lib/
目录下的相关类文件,好比rt.jar
、tools.jar
等等,好比说咱们的String
类存放在rt.jar
中,器加载器就是Bootstrap ClassLoader
eclipse
二、扩展类加载器Extension ClassLoader
ide
扩展类加载器,负责%JRE_HOME/lib/ext
目录下的相关类文件,好比rt.jar
、tools.jar
等等工具
三、应用类加载器App ClassLoader
开发工具
应用类加载器,负责加载应用程序类路径下的class
文件。ui
说到类路径的问题,咱们解释一下,java的类路径指的是咱们配置的系统环境变量的值:CLASSPATH
,但又不局限于这个值。小编先打印下咱们本地的classpath
的值: this
.;D:\development\jdk\lib\dt.jar;D:\development\jdk\lib\tools.jar;
你会发现,这根本就不是咱们用的开发工具中的目录呀,那么JVM
是怎么加载到咱们在eclipse
上编写的类呢?答案是eclipse
已经帮咱们弄好了一切。
下边咱们举个例子,看下String
类的类加载器是什么:
System.out.println(String.class.getClassLoader());
上面这句代码输出null
。为何呢?由于若是一个类是被Bootstrap ClassLoader
或者Extension ClassLoader
加载时,getClassLoader()
规定输出null
。又由于String
类存在rt.jar
中,将会被Bootstrap ClassLoader
加载,因此输出null
。
谈及类加载器,咱们不得不说类的双亲委派机制,一句话总结双亲委派机制,小编总结成一句话:
若是自定义加载器P有父加载器P1,那么在加载前就将加载任务委派给其父亲P1,若是P1也存在父加载器P2,那么将加载任务委派给P2,若是最顶层的Bootstrap ClassLoader还加载不到,那么就再逆着顺序加载,直到类被加载到~~
ClassLoader
类特性介绍一、每个Class
实例都包含一个ClassLoader
引用
Class
对象的getClassLoader()
方法获取其类加载器。固然了,就如上面说的,若是一个类是被Bootstrap ClassLoader
或者Extension ClassLoader
加载时,getClassLoader()
规定输出null
。这是一个须要咱们注意的点。 二、对于数组类型[]
对象,它们不是由类加载器ClassLoader
去进行加载的,而是Java虚拟机根据须要自动建立的。咱们经过数组类型的Class
对象的getClassLoader()
方法返回的值跟数组里边元素所使用的的类加载器同样,但若是数组元素为原始类型int
啥的,则getClassLoader()
方法将返回null
// sun.misc.Launcher$AppClassLoader@73d16e93
ClassLoaderTest[] array = new ClassLoaderTest[5];
System.out.println(array.getClass().getClassLoader());
// null
int[] intArray = new int[3];
System.out.println(intArray.getClass().getClassLoader());
复制代码
三、咱们能够经过继承ClassLoader
类,来实现本身的类加载器
/**
* 自定义类加载器
* @Author jiawei huang
* @Since 2019年8月7日
* @Version 1.0
*/
public class MyClassLoader extends ClassLoader {
// 若是咱们从其余地方进行加载,咱们能够指定路径
private String classpath;
public MyClassLoader(String classPath) {
classpath = classPath;
}
public MyClassLoader() {
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = null;
// 获取该class文件字节码数组
byte[] classData = getClassData();
if (classData != null) {
// 将class的字节码数组转换成Class类的实例
clazz = defineClass(name, classData, 0, classData.length);
}
return clazz;
}
private byte[] getClassData() {
byte[] bytes = null;
File file = new File(classpath);
if (file.exists()) {
// 从文件中读取class字节数据
FileInputStream in = null;
ByteArrayOutputStream out = null;
try {
in = new FileInputStream(file);
out = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int size = 0;
while ((size = in.read(buffer)) != -1) {
out.write(buffer, 0, size);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
bytes = out.toByteArray();
}
return bytes;
}
}
复制代码
假设咱们在ex
包路径下有个Main.java
,看看咱们下面的输出:
A.java
MyClassLoader loader = new MyClassLoader();
Class<?> clazzClass = loader.loadClass("ex.Main");
// 1
System.out.println(clazzClass.getClassLoader());
复制代码
上面1
处将输出sun.misc.Launcher$AppClassLoader@73d16e93
,咱们可能会问,明明我是用本身的类加载器去加载ex.Main
的呀,为何却输出AppClassLoader
这个类加载器呢?其实这就是双亲委派机制,MyClassLoader
把加载任务给到其父加载器App ClassLoader
,恰好ex.Main
又处于类路径下,因此App ClassLoader
加载以后就直接返回了。
四、一个类整个生命周期只会被加载一次,有且仅有一次
五、支持并行加载的加载器称为并行加载器,但前提是,咱们必须在自定义加载器的初始化时,调用ClassLoader.registerAsParallelCapable();
方法注册本身,怎么作呢?咱们看看URLClassLoader
的源码。
static {
// 注册本身
ClassLoader.registerAsParallelCapable();
}
复制代码
详情咱们看下ClassLoader
中loadClass()
方法源码:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 这里有个锁
synchronized (getClassLoadingLock(name)) {
一、先检查这个名称的类是否已经被加载过,若是是,就再也不加载了,这也印证了咱们第4点说的一个类只会被加载一次
Class<?> c = findLoadedClass(name);
// 二、若是该类没有被加载,那么就进行加载
if (c == null) {
long t0 = System.nanoTime();
try {
// 三、若是存在父加载器,就进行委派,这就是双亲委派机制的原理
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 四、去`Bootstrap classloader`加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
// 五、若是3,4都没有加载到,那就执行咱们自定义的classloader,这也是为何咱们要重写findClass方法的缘由所在
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;
}
}
复制代码
class
文件的加载,获取byte[]
,而后经过ClassLoader
的defineClass()
将byte[]
转换成Class
对象,最后经过Class.newInstance()
便可转成Java对象啦~~~ClassLoader loader = new NetworkClassLoader(host, port);
Object main = loader.loadClass("Main", true).newInstance();
复制代码
SpringBoot
中,构造器为何要加上ClassLoader
参数?由于初始化过程当中,load()
方法须要从不一样的地方去加载类文件。
protected void load(ApplicationContext context, Object[] sources) {
if (logger.isDebugEnabled()) {
logger.debug(
"Loading source " + StringUtils.arrayToCommaDelimitedString(sources));
}
BeanDefinitionLoader loader = createBeanDefinitionLoader(
getBeanDefinitionRegistry(context), sources);
if (this.beanNameGenerator != null) {
loader.setBeanNameGenerator(this.beanNameGenerator);
}
if (this.resourceLoader != null) {
loader.setResourceLoader(this.resourceLoader);
}
if (this.environment != null) {
loader.setEnvironment(this.environment);
}
loader.load();
}
复制代码