相关文章
Java虚拟机系列html
热修复和插件化是目前比较热门的技术,要想更好的掌握它们须要了解ClassLoader,所以也就有了本系列的产生,这一篇咱们先来学习Java中的ClassLoader。
java
在Java虚拟机(一)结构原理与运行时数据区域这篇文章中,我提到过类加载子系统,它的主要做用就是经过多种类加载器(ClassLoader)来查找和加载Class文件到 Java 虚拟机中。
Java中的类加载器主要有两种类型,系统类加载和自定义类加载器。其中系统类加载器包括3种,分别是Bootstrap ClassLoader、 Extensions ClassLoader和 App ClassLoader。算法
用C/C++代码实现的加载器,用于加载Java虚拟机运行时所须要的系统类,如java.lang.*、java.uti.*
等这些系统类,它们默认在$JAVA_HOME/jre/lib目录中,也能够经过启动Java虚拟机时指定-Xbootclasspath选项,来改变Bootstrap ClassLoader的加载目录。
Java虚拟机的启动就是经过 Bootstrap ClassLoader建立一个初始类来完成的。因为Bootstrap ClassLoader是使用C/C++语言实现的, 因此该加载器不能被Java代码访问到。须要注意的是Bootstrap ClassLoader并不继承java.lang.ClassLoader。
咱们能够经过以下代码来得出Bootstrap ClassLoader所加载的目录:数组
public class ClassLoaderTest {
public static void main(String[]args) {
System.out.println(System.getProperty("sun.boot.class.path"));
}
}复制代码
打印结果为:缓存
C:\Program Files\Java\jdk1.8.0_102\jre\lib\resources.jar;
C:\Program Files\Java\jdk1.8.0_102\jre\lib\rt.jar;
C:\Program Files\Java\jdk1.8.0_102\jre\lib\sunrsasign.jar;
C:\Program Files\Java\jdk1.8.0_102\jre\lib\jsse.jar;
C:\Program Files\Java\jdk1.8.0_102\jre\lib\jce.jar;
C:\Program Files\Java\jdk1.8.0_102\jre\lib\charsets.jar;
C:\Program Files\Java\jdk1.8.0_102\jre\lib\jfr.jar;
C:\Program Files\Java\jdk1.8.0_102\jre\classes复制代码
能够发现几乎都是$JAVA_HOME/jre/lib目录中的jar包,包括rt.jar、resources.jar和charsets.jar等等。安全
用于加载 Java 的拓展类 ,拓展类的jar包通常会放在$JAVA_HOME/jre/lib/ext目录下,用来提供除了系统类以外的额外功能。也能够经过-Djava.ext.dirs选项添加和修改Extensions ClassLoader加载的路径。
经过如下代码能够获得Extensions ClassLoader加载目录:bash
System.out.println(System.getProperty("java.ext.dirs"));复制代码
打印结果为:网络
C:\Program Files\Java\jdk1.8.0_102\jre\lib\ext;
C:\Windows\Sun\Java\lib\ext复制代码
负责加载当前应用程序Classpath目录下的全部jar和Class文件。也能够加载经过-Djava.class.path选项所指定的目录下的jar和Class文件。 jvm
除了系统提供的类加载器,还能够自定义类加载器,自定义类加载器经过继承java.lang.ClassLoader类的方式来实现本身的类加载器,除了 Bootstrap ClassLoader,Extensions ClassLoader和App ClassLoader也继承了java.lang.ClassLoader类。关于自定义类加载器后面会进行介绍。ide
运行一个Java程序须要用到几种类型的类加载器呢?以下所示。
public class ClassLoaderTest {
public static void main(String[] args) {
ClassLoader loader = ClassLoaderTest.class.getClassLoader();
while (loader != null) {
System.out.println(loader);//1
loader = loader.getParent();
}
}
}复制代码
首先咱们获得当前类ClassLoaderTest的类加载器,并在注释1处打印出来,接着打印出当前类的类加载器的父加载器,直到没有父加载器终止循环。打印结果以下所示。
sun.misc.Launcher$AppClassLoader@75b84c92
sun.misc.Launcher$ExtClassLoader@1b6d3586复制代码
第1行说明加载ClassLoaderTest的类加载器是AppClassLoader,第2行说明AppClassLoader的父加载器为ExtClassLoader。至于为什么没有打印出ExtClassLoader的父加载器Bootstrap ClassLoader,这是由于Bootstrap ClassLoader是由C/C++编写的,并非一个Java类,所以咱们没法在Java代码中获取它的引用。
咱们知道系统所提供的类加载器有3种类型,可是系统提供的ClassLoader相关类却不仅3个。另外,AppClassLoader的父类加载器为ExtClassLoader,并不表明AppClassLoader继承自ExtClassLoader,ClassLoader的继承关系以下所示。
能够看到上图中共有5个ClassLoader相关类,下面简单对它们进行介绍:
类加载器查找Class所采用的是双亲委托模式,所谓双亲委托模式就是首先判断该Class是否已经加载,若是没有则不是自身去查找而是委托给父加载器进行查找,这样依次的进行递归,直到委托到最顶层的Bootstrap ClassLoader,若是Bootstrap ClassLoader找到了该Class,就会直接返回,若是没找到,则继续依次向下查找,若是还没找到则最后会交由自身去查找。
这样讲可能会有些抽象,来看下面的图。
咱们知道类加载子系统用来查找和加载Class文件到 Java 虚拟机中,假设咱们要加载一个位于D盘的Class文件,这时系统所提供的类加载器不能知足条件,这时就须要咱们自定义类加载器继承自java.lang.ClassLoader,并复写它的findClass方法。加载D盘的Class文件步骤以下:
总的来讲就是Class文件加载到类加载子系统后,先沿着图中红色虚线的方向自下而上进行委托,再沿着黑色虚线的方向自上而下进行查找,整个过程就是先上后下。
类加载的步骤在JDK8的源码中也获得了体现,来查看抽象类的ClassLoader方法,以下所示。
protected Class<?> More ...loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
Class<?> c = findLoadedClass(name);//1
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);//2
} else {
c = findBootstrapClassOrNull(name);//3
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);//4
// 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处用来检查类是否已经加载,若是已经加载则后面的代码不会执行,最后会返回该类。没有加载则会接着向下执行。
注释2处,若是父类加载器不为null,则调用父类加载器的loadClass方法。若是父类加载器为null则调用注释3处的findBootstrapClassOrNull方法,这个方法内部调用了Native方法findLoadedClass0,findLoadedClass0方法中最终会用Bootstrap Classloader来查找类。若是Bootstrap Classloader仍没有找到该类,也就说明向上委托没有找到该类,则调用注释4处的findClass方法继续向下进行查找。
采起双亲委托模式主要有两点好处:
系统提供的类加载器只可以加载指定目录下的jar包和Class文件,若是想要加载网络上的或者是D盘某一文件中的jar包和Class文件则须要自定义ClassLoader。
实现自定义ClassLoader须要两个步骤:
下面咱们就自定义一个ClassLoader用来加载位于D:\lib的Class文件。
首先编写测试类并生成Class文件,以下所示。
package com.example;
public class Jobs {
public void say() {
System.out.println("One more thing");
}
}复制代码
将这个Jobs.java放入到D:\lib中,使用cmd命令进入D:\lib目录中,执行Javac Jobs.java
对该java文件进行编译,这时会在D:\lib中生成Jobs.class。
接下来编写自定义ClassLoader,以下所示。
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class DiskClassLoader extends ClassLoader {
private String path;
public DiskClassLoader(String path) {
this.path = path;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = null;
byte[] classData = loadClassData(name);//1
if (classData == null) {
throw new ClassNotFoundException();
} else {
clazz= defineClass(name, classData, 0, classData.length);//2
}
return clazz;
}
private byte[] loadClassData(String name) {
String fileName = getFileName(name);
File file = new File(path,fileName);
InputStream in=null;
ByteArrayOutputStream out=null;
try {
in = new FileInputStream(file);
out = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int length=0;
while ((length = in.read(buffer)) != -1) {
out.write(buffer, 0, length);
}
return out.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
if(in!=null) {
in.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try{
if(out!=null) {
out.close();
}
}catch (IOException e){
e.printStackTrace();
}
}
return null;
}
private String getFileName(String name) {
int index = name.lastIndexOf('.');
if(index == -1){//若是没有找到'.'则直接在末尾添加.class
return name+".class";
}else{
return name.substring(index+1)+".class";
}
}
}复制代码
这段代码有几点须要注意的,注释1处的loadClassData方法会得到class文件的字节码数组,并在注释2处调用defineClass方法将class文件的字节码数组转为Class类的实例。loadClassData方法中须要对流进行操做,关闭流的操做要放在finally语句块中,而且要对in和out分别采用try语句,若是in和out共同在一个try语句中,那么若是in.close()
发生异常,则没法执行 out.close()
。
最后咱们来验证DiskClassLoader是否可用,代码以下所示。
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class ClassLoaderTest {
public static void main(String[] args) {
DiskClassLoader diskClassLoader = new DiskClassLoader("D:\\lib");//1
try {
Class c = diskClassLoader.loadClass("com.example.Jobs");//2
if (c != null) {
try {
Object obj = c.newInstance();
System.out.println(obj.getClass().getClassLoader());
Method method = c.getDeclaredMethod("say", null);
method.invoke(obj, null);//3
} catch (InstantiationException | IllegalAccessException
| NoSuchMethodException
| SecurityException |
IllegalArgumentException |
InvocationTargetException e) {
e.printStackTrace();
}
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}复制代码
注释1出建立DiskClassLoader并传入要加载类的路径,注释2处加载Class文件,须要注意的是,不要在项目工程中存在名为com.example.Jobs的Java文件,不然就不会使用DiskClassLoader来加载,而是AppClassLoader来负责加载,这样咱们定义DiskClassLoader就变得毫无心义。接下来在注释3经过反射来调用Jobs的say方法,打印结果以下:
com.example.DiskClassLoader@4554617c
One more thing复制代码
使用了DiskClassLoader来加载Class文件,say方法也正确执行,显然咱们的目的达到了。
这一篇文章咱们学习了Java中的ClassLoader,包括ClassLoader的类型、双亲委托模式、ClassLoader继承关系以及自定义ClassLoader,为的是就是更好的理解下一篇所要讲解的Android中的ClassLoader。
参考资料
一看你就懂,超详细java中的ClassLoader详解
深刻分析Java ClassLoader原理