前言
上一篇文章,《从头开始学习JVM(四):类加载器(中)》,我们知道了类加载器的基本原理,但是知道了这些原理之后,我们对类加载器的底层的逻辑,算不上有多清楚明白。
我们仅仅是意识到,类加载器的种类有多少,类加载器的加载机制双亲委派模型以及如何打破双亲委派模型等等等相关的一些原理。
但是在这些原理的背后,是什么在支撑着呢?
想一想吧,所谓的JVM,本身就是一直概念上的产物,并不是物理上的东西,那么作为JVM一部分的类加载器,以及我们的类加载流程,也只是我们在概念上的划分。而支持着这些概念上划分的,就是我们的代码。这些代码里面,有C++代码,有java代码,这些代码,组合成了一个个闭合的逻辑,在现实层面,实现了我们所要实现的原理和概念理论。
今天这篇文章,我们尝试从代码层面,来解读我们的类加载器以及一些相关的类加载机制。
正文
1.Launcher类
由于BootStapClassLoader加载器是由C++语言写的,是嵌入到了JVM内核中,也就是说,从java代码的层面我们是看不到的,因此我们不去细究这个启动类加载器。
JVM生成的第一个类是Launcher类,先看代码,如下:
public static void main(String[] args) {
ClassLoader classLoader = Launcher.class.getClassLoader();
System.out.println("加载器:" + classLoader);
}
代码运行结果为:
E:\jdk\bin\java.exe
加载器:null
Process finished with exit code 0
请注意,最后的根加载器打印出的是null,这不代表着就是真的null,只是因为启动类加载器是由C++写的,所以在java中显示就是为null。
通过代码运行的结果为null,我们可以得知,这个类是由BootstrapClassLoader加载器加载的。
也就是说,JVM在启动的时候,就生成了BootstrapClassLoader加载器,而BootrapClassLoader加载器会JVM启动的第一时间就会去创建Launcher类的实例。
那么,为什么要创建Laucher类的实例呢?我们先来看一下Launcher类的内部代码,如下:
public class Launcher {
private static URLStreamHandlerFactory factory = new Launcher.Factory();
private static Launcher launcher = new Launcher();
private static String bootClassPath = System.getProperty("sun.boot.class.path");
private ClassLoader loader;
private static URLStreamHandler fileHandler;
public static Launcher getLauncher() {
return launcher;
}
public Launcher() {
Launcher.ExtClassLoader var1;
try {
//创建ExtClassLoader加载器
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
//通过创建的ExtClassLoader加载器来获取AppClassLoader设置为默认加载器加载器。
//并且将AppClassLoader设置为默认加载器
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
Thread.currentThread().setContextClassLoader(this.loader);
String var2 = System.getProperty("java.security.manager");
if (var2 != null) {
SecurityManager var3 = null;
if (!"".equals(var2) && !"default".equals(var2)) {
try {
var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
} catch (IllegalAccessException var5) {
;
} catch (InstantiationException var6) {
;
} catch (ClassNotFoundException var7) {
;
} catch (ClassCastException var8) {
;
}
} else {
var3 = new SecurityManager();
}
if (var3 == null) {
throw new InternalError("Could not create SecurityManager: " + var2);
}
System.setSecurityManager(var3);
}
}
}
通过这段代码,我们发现,Launcher类中有一个构造方法Launcher(),这个方法在Launcher创建的时候,就会创建ExtClassLoader加载器,通过创建的ExtClassLoader加载器来创建AppClassLoader加载器,并且将AppClassLoader设置为默认的系统加载器(也就是当前线程的上下文类加载器)。
也因此,当我们想要获取当前应用程序的AppClassLoader加载器的时候,可以通过以下代码获取:
public static void main(String[] args) {
ClassLoader classLoader = Launcher.getLauncher().getClassLoader();
System.out.println("加载器:" + classLoader);
}
运行结果如下:
E:\jdk\bin\java.exe
加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
Process finished with exit code 0
然后,既然创建了ExtClassLoader加载器和AppClassLoader加载器,那么我们可以去看看这两个加载器的源码。请注意的是,这两个加载器都是Launcher类的静态内部类。
2.ExtclassLoader和AppClassLoader
ExtclassLoader:
static class ExtClassLoader extends URLClassLoader类, {
public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
final File[] var0 = getExtDirs();
try {
return (Launcher.ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction() {
public Launcher.ExtClassLoader run() throws IOException {
int var1 = var0.length;
for(int var2 = 0; var2 < var1; ++var2) {
MetaIndex.registerDirectory(var0[var2]);
}
return new Launcher.ExtClassLoader(var0);
}
});
} catch (PrivilegedActionException var2) {
throw (IOException)var2.getException();
}
}
}
ExtclassLoader加载器继承了URLClassLoader类,URLClassLoader这个类是继承了顶级类ClassLoader的,是ClassLoader类的一个扩展。
ExtclassLoader没有重写ClassLoader类的loadClass()方法,而是直接调用了顶级类ClassLoader的loadClass()方法。
AppClassLoader:
static class AppClassLoader extends URLClassLoader {
final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);
public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
final String var1 = System.getProperty("java.class.path");
final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction() {
public Launcher.AppClassLoader run() {
URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
return new Launcher.AppClassLoader(var1x, var0);
}
});
}
public Class> loadClass(String var1, boolean var2) throws ClassNotFoundException {
int var3 = var1.lastIndexOf(46);
if (var3 != -1) {
SecurityManager var4 = System.getSecurityManager();
if (var4 != null) {
var4.checkPackageAccess(var1.substring(0, var3));
}
}
if (this.ucp.knownToNotExist(var1)) {
Class var5 = this.findLoadedClass(var1);
if (var5 != null) {
if (var2) {
this.resolveClass(var5);
}
return var5;
} else {
throw new ClassNotFoundException(var1);
}
} else {
return super.loadClass(var1, var2);
}
}
}
AppclassLoader加载器也继承了URLClassLoader类。
AppclassLoader重写了ClassLoader类的loadClass()方法。
3.继承了ClassLoader的URLClassLoader类
java.net.URLClassLoader类是继承了ClassLoader的一个类,是ClassLoader类的一个扩展。
ClassLoader只能加载classpath下面的类,而URLClassLoader则可以加载任意路径下的类。
一般动态加载类的时候,都是直接用Class.forName()这个方法,但这个方法只能创建程序中已经引用的类,并且只能用包名的方法进行索引,比如Java.lang.String,不能对一个.class文件或者一个不在程序引用里的.jar包中的类进行创建。
URLClassLoader提供了这个功能,它让我们可以通过以下几种方式进行加载:
- 文件: (从文件系统目录加载)
- jar包: (从Jar包进行加载)
- Http: (从远程的Http服务进行加载)
4.抽象的类加载器ClassLoader(顶级类)
java.lang.ClassLoader是Java层面对类加载器的抽象,这个类规范了类加载的基本流程,是类加载中的顶级类,其中比较重要的属性及方法如下:
- parent():父类加载器的引用,一个类加载器通常都会保存一个父类加载器的引用,用于实现双亲委派机制。
- loadClass()方法,该方法为类加载器的核心方法,其中实现了双亲委派的逻辑。
代码如下:
public abstract class ClassLoader {
//通过这个类,我们可以找到这个加载器的父类加载器
private final ClassLoader parent;
//name是要加载的类的名称,resolve是判断这个类是否要解析
protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,检查类是否已经加载
Class> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//如果父类加载器不为null(也就是不是启动类加载器),那就调用父类加载器的loadClass()方法来加载这个类
c = parent.loadClass(name, false);
} else {
//如果父类加载器为null,那么就获取到启动类加载器BootstrapClassLoader来加载这个类
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果没有找到类,则抛出ClassNotFoundException异常
// from the non-null parent class loader
}
if (c == null) {
long t1 = System.nanoTime();
//如果仍然没有找到,那么就调用这个加载器本身的findClass()去找到这个类
c = findClass(name);
// 这是定义类加载器,记录数据
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
//如果传进来的参数resolve为true,那么我们就去解析这个类
resolveClass(c);
}
//返回这个类对应的Class对象
return c;
}
}
}
要说明一下,在这个loadClass()方法中,我们调用的方法大部分都是native方法,说白了都是使用了C++写的,在java里面是看不到具体实现的。
从以上代码我们可以看出来,是实现了类加载机制之双亲委派模型的。
5.加载器之间的层级关系
类加载器有着一定的层级关系,比如说userClassLoader的父类加载器是AppClassLoader,而AppClassLoader的父类加载器是ExtClassLoader,而ExtClassLoader的父类加载器是BootStrapClassLoader,而BootStrapClassLoader就是最高层级的类加载器了。
我先写出如下一段代码:
public static void main(String[] args) throws IOException {
//获取应用程序类加载器AppClassLoader
ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
System.out.println("应用程序类加载器:" + appClassLoader);
Enumeration enums = appClassLoader.getResources("");
while (enums.hasMoreElements()) {
System.out.println("应用程序类加载器加载路径:"+enums.nextElement());
break;
}
//获取应用程序类加载器的父类加载器(也就是扩展类加载器ExtClassLoader)
ClassLoader extClassLoader = appClassLoader.getParent();
System.out.println("扩展类加载器:" + extClassLoader);
System.out.println("扩展类加载器加载路径:" + System.getProperty("java.ext.dirs"));
//获取扩展类加载器的父类加载器(也就是启动类加载器BootStrapClassLoader)
ClassLoader bootClassLoader = extClassLoader.getParent();
System.out.println("根类加载器:" + bootClassLoader);
}
这段代码,运行的结果如下:
E:\jdk\bin\java.exe
应用程序类加载器 :sun.misc.Launcher$AppClassLoader@18b4aac2
扩展类加载器 :sun.misc.Launcher$ExtClassLoader@156643d4
根类加载器 :null
Process finished with exit code 0
所以我们可以看到,程序最后运行的结果,是和我们代码里面所需要的结果,是一致的。也就是说,确实如我之前所说,总结如下:
- userClassLoader的父类加载器是AppClassLoader。
- AppClassLoader的父类加载器是ExtClassLoader。
- ExtClassLoader的父类加载器是BootStrapClassLoader。
- BootStrapClassLoader是最高层级的类加载器。
- AppClassLoader是程序默认的类加载器。
6.类加载器的加载路径
在上面那段代码的基础上,我们稍微修改一下代码:
public static void main(String[] args){
//获取到启动类加载器BootStrapClassLoader
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for(URL url : urls) {
System.out.println("BootstrapClassLoader的加载路径: "+url);
break;
}
//获取到扩展类加载器ExtClassLoader
URLClassLoader extClassLoader = (URLClassLoader)ClassLoader.getSystemClassLoader().getParent();
urls = extClassLoader.getURLs();
for(URL url : urls){
System.out.println("ExtClassLoader的加载路径: "+url);
break;
}
//取得应用(系统)类加载器AppClassLoader
ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
Enumeration enums = appClassLoader.getResources("");
while (enums.hasMoreElements()) {
System.out.println("AppClassLoader的加载路径:"+enums.nextElement());
break;
}
}
得出的代码的运行结果如下:
E:\jdk\bin\java.exe
BootstrapClassLoader负责加载存放在 \lib目录
2. 的加载路径: file:/E:/jdk/jre/lib/resources.jar
ExtClassLoader的加载路径: file:/E:/jdk/jre/lib/ext/access-bridge-64.jar
AppClassLoader的加载路径: file:/L:/order-service/order-service-web/target/classes/
Process finished with exit code 0
因此,从最后的结果来看,我们可以获取到这些类加载器的加载路径,总结如下:
- BootstrapClassLoader负责加载存放在
\lib目录 - ExtClassLoader主要加载JAVA中的一些拓展类,java.ext.dirs目录中加载类库,或者从JDK安装目录:jre/lib/ext目录下加载类库,是启动类加载器的子类。
- AppClassLoader负责加载环境变量classpath或者系统属性java.class.path指定路径下的类库。
7.手写一个UserClassLoader加载器
我们知道,实现属于自己的类加载器UserClassLoader有两种方式:
- 继承java.lang.ClassLoader类,重写findClass()方法
- 如果没有太复杂的需求,可以直接继承URLClassLoader类,重写loadClass方法,具体可参考AppClassLoader和ExtClassLoader(我在上面已经截图说明了,请注意,这种方案会打破双亲委派模型)。
第二种方案,直接模拟AppClassLoader或者ExtClassLoader的源码就可以了,我们今天,就来通过第一种方案,来手写一个我们的UserClassLoader加载器。
7.1 创建一个加载器MyUserClassLoader
package com.blog.permission.classLoad;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
public class MyUserClassLoader extends ClassLoader{
private String rootPath;
public MyUserClassLoader() {
super();
}
public MyUserClassLoader(String rootPath) {
this.rootPath = rootPath;
}
/**
* 用于寻找类文件
* @param className 类文件的全限定名
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class> findClass(String className) {
Class> clz = findLoadedClass(className);
if (clz != null)
return clz;
byte[] classData = loadClassData(className);
clz = defineClass(className, classData, 0, classData.length);
return clz;
}
/**
* 这是将文件转换为二进制字节码
* @param className
* @return
*/
private byte[] loadClassData(String className) {
String pathName = rootPath + className.replace(".", "/") + ".class";
System.out.println(pathName);
byte[] bytes = null;
try (FileInputStream fis = new FileInputStream(pathName);
ByteArrayOutputStream bos = new ByteArrayOutputStream();) {
byte[] flush = new byte[1024 * 1024];
int len = -1;
while (-1 != (len = fis.read(flush))) {
bos.write(flush);
}
bytes = bos.toByteArray();
} catch (Exception e) {
System.out.println("异常");
}
return bytes;
}
}
7.2 创建一个要加载的java类:Shuaige.java
package com.blog.permission.classLoad;
public class Shuaige {
public static void main(String[] args) {
System.out.println("I'm so cool,you underStand?");
}
}
7.3 将shuaige.java文件编译成Shuaige.class文件
7.4 创建一个运行的类
package com.blog.permission.classLoad;
public class TestClassLoad {
public static void main(String[] args) throws ClassNotFoundException {
MyUserClassLoader fileSystemClassLoader = new MyUserClassLoader("/com/blog/permission/classLoad");
Class> c = fileSystemClassLoader.loadClass("com.blog.permission.classLoad.Shuaige");
System.out.println(c);
}
}
最后形成的层级目录如下所示:
7.5 运行结果如下
E:\jdk\bin\java.exe
class com.blog.permission.classLoad.Shuaige
Process finished with exit code 0
总结
通过对加载器的代码的分析和解读,很明显,我们对类加载的机制有了更加深刻的了解。
当然,某种程度上来讲,本篇文章的源码解析,也只是解析了一部分而已,还有很多地方是没有涉及到的。但是透过这些源码,我们对类加载器,有了质感上的提升,类加载器以及类加载过程,在我们心里,不再是虚拟的不落到实处的概念,而是真真切切存在着的由代码实现了的东西了。
到此,类加载器的整个解读就已经结束了。
参考博客
https://blog.csdn.net/how_interesting/article/details/80091472
https://blog.csdn.net/chuodan5158/article/details/100765519
https://www.cnblogs.com/chinaifae/p/10401523.html
https://blog.csdn.net/weixin_39161031/article/details/83000750
https://www.cnblogs.com/rogge7/p/7766522.html
参考书籍
周志明《深入理解java虚拟机》