上一节(2)JVM 类加载之类加载器初始化记录了JVM自带的3中类加载器,也分析了类加载器初始化的流程。那么问题来了:
本文涉及到的源码都在JDK的jre\lib\rt.jar包中。
双亲委派机制用在一个类被加载之前,因为需要判断由哪个类加载器去加载这个类。双亲委派机制在Java运行一个类的流程中的位置如图:
从图中可以看出,引导类加载器是拓展类加载器的父加载器,拓展类加载器是应用程序类加载器的父加载器,应用程序类加载器是自定义类加载器的父加载器。在加载一个类之前,若没有自定义类加载器,则默认是从应用程序类加载器开始加载,逐级委托父级类加载器,最终委托到引导类加载器。若引导类加载器不能加载需要加载的类,则委派拓展类加载器进行加载;若拓展类加载器仍不能进行加载,则委派应用程序类加载器来完成加载。若自定义了类加载器,并使用自定义类加载器对某个类进行加载,则从自定义类加载器开始逐级委托,然后逐级委派。
为什么这几种类加载器是这样的关系呢?因为源码是这样设计的。
在下面分析之前,先放上一个类图:
这是在IDEA中点击ExtClassLoader然后按组合键Ctrl + alt + U看见的类图,其实这个类图没有画完,我在visio中画了一下:
可以发现,拓展类加载器虽然是应用程序类加载器的父级加载器,但应用程序类加载器并不是继承于拓展类加载器,只不过在应用程序类加载器的类中有个parent属性,这个属性是从ClassLoader继承过来的,里面存的值就是拓展类加载器对象的引用,所以看起来他们像是父子的关系。其它的加载器情况与上面的类似,自定义类加载器的parent是应用程序类加载器,拓展类加载器的parent是null,因为其父类加载器引导类加载器是用C++实现的,在Java里获取不到。
我在(2)JVM 类加载之类加载器初始化中记录过,拓展类加载器和应用程序类加载器是在sun.misc.Launcher.getLauncher()这里创建的,那么我们具体再跟进一下,先进入sun.misc.Launcher的构造器:
public Launcher() {
Launcher.ExtClassLoader var1;
try {
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
...
}
在(2)JVM 类加载之类加载器初始化中分析得到:
继续跟进getExtClassLoader():
public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
final File[] var0 = getExtDirs();
try {
return (Launcher.ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction<Launcher.ExtClassLoader>() {
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();
}
}
看到返回值是:
return new Launcher.ExtClassLoader(var0);
那么继续跟进ExtClassLoader(var0):
public ExtClassLoader(File[] var1) throws IOException {
super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
}
发现进入到了拓展类加载器的一个有参构造器,里面有一行调用其父类的构造器的代码:
super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
发现传进了一个**(ClassLoader)null**,这其实是其父类加载器,继续跟进super:
this.parent = parent;
这个this.parent是ExtClassLoader的一个属性,它最终被赋值为null,印证了上面的说法。
对应用程序类加载器的分析差不多,从下面的Launcher构造器源码开始:
跟进getAppClassLoader(var1),注意传值var1是上面创建的拓展类加载器对象:
跟进AppClassLoader(var1x, var0),注意传值var0是拓展类加载器对象,进入到AppCLassLoader的有参构造器:
到了赋值的地方,this.parent被初始化为拓展类加载器。
经过上面的源码分析,前言中问题1的答案一目了然。
下面的代码是一个自定义的类加载器:
package com.jim.jvm.classload;
import java.io.FileInputStream;
import java.io.IOException;
public class MyClassLoader extends ClassLoader {
private String path; //默认加载路径
// 初始化自定义类加载器
MyClassLoader(String path) {
// TODO Auto-generated constructor stub
super();
this.path = path;
}
// 重写ClassLoader的finClass函数(为了加载自定义路径下的字节码文件)
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
// TODO Auto-generated method stub
byte[] b = new byte[0];
try {
b = loadData(name);
} catch (IOException e) {
e.printStackTrace();
}
return defineClass(name, b, 0, b.length);
}
// 读取自定义路径的字节码文件
private byte[] loadData(String name) throws IOException {
// 将com.jim.jvm.classload.Test的'.'替换成'/'
name = name.replaceAll("\\.", "/");
// 从自定义类路径D:/test/com/jim/jvm/classload/Test.class读取字节码文件
FileInputStream fis = new FileInputStream(this.path + "/" + name + ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
}
接下来测试自定义加载器:
package com.jim.jvm.classload;
public class MyClassLoaderTest {
public static void main(String[] args) throws Exception {
// 创建自定义类加载器对象
MyClassLoader loader1 = new MyClassLoader("D:/test");
String name = "com.jim.jvm.classload.Test";
loadClassByMyClassLoader(loader1, name);
}
private static void loadClassByMyClassLoader(ClassLoader loader, String name) throws Exception{
Class<?> c = loader.loadClass(name);
// 输出加载自己这个类的类加载器
System.out.println(c.getClassLoader());
}
}
在测试中,我在D盘创建了一个test文件夹,然后创建com、jim、jvm和classload文件夹,将需要加载的字节码文件Test.class放进去。
在程序中设置自定义加载器的加载路径是D:/test,要加载的文件是com.jim.jvm.classload.Test,com.jim.jvm.classload这个路径一定要和正在运行的这个程序所在目录的路径一致。
最后的输出是:
com.jim.jvm.classload.MyClassLoader@677327b6
这样就成功实现了一个自定义类加载器。
上面自定义类加载器的有参构造器代码如下,
// 初始化自定义类加载器
MyClassLoader(String path) {
// TODO Auto-generated constructor stub
super();
this.path = path;
}
其中有这行代码:
super();
我们知道,在面向对象中,一个类在初始化时,会先初始化构造其父类,而我们自定义类加载器的父类是ClassLoader,跟进代码,跳转到父类的构造器:
再跟进
getSystemClassLoader()
进入到:
进入initSystemClassLoader():
我在(2)JVM 类加载之类加载器初始化中贴了一张sum.misc.Launcher类的成员属性部分的截图:
那么执行上一张截图中这一行代码
sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
就能获取到由引导类加载器加载创建的单例sun.misc.Launcher对象。
接下来执行上张图的
scl = l.getClassLoader();
跟进函数getClassLoader():
返回的是this.loader!!!在上一小节已经分析得到:this.loader是创建的应用程序类加载器,所以这里返回的是应用程序类加载器。
src = 应用程序类加载器
再返回继续执行上层函数:
再返回上层调用:
this.parent = parent;
这里的this.parent是自定义类加载器MyClassLoader继承自ClassLoader的属性,最终被赋值为应用程序类加载器对象,也就是说,自定义类加载器的默认父类加载器是应用程序类加载器。
前面两小节详细分析了四种类加载器之间的关系。那么既然有四种类加载器,在对一个类进行加载时,怎么判断由哪个类加载器来加载呢?
JVM的设计者们设计了双亲委派机制。
下面是我自己写的一个加法类:
package com.jim.jvm.classload;
public class Add {
private int add(int a, int b){
return a + b;
}
public static void main(String[] args) {
int a = 10, b = 20;
Add ad = new Add();
int result = ad.add(a, b);
}
}
在相应地地方加上断点,并在如下图需要传name参数的地方添加debug表达式,以此来过滤其它类(核心类等)的加载过程,直达Add类的加载:
name.equals("com.jim.jvm.classload.Add")
现在开始debug:
即将进入loadClass函数加载com.jim.jvm.classload.Add:
Class<?> c = findLoadedClass(name);
这行代码的功能是寻找应用程序类加载器是否已经加载了Add类,底层实现是本地方法(C++),加载了就赋值给c,没有加载则返回null。
因为Add类还没被加载过,因此
c = null
进入下一个断点执行:
c = parent.loadClass(name, false);
上面这行代码是调用了应用程序类加载器的父类加载器的loadClass方法,也就是将Add委托给拓展类加载器加载,于是进入下一个断点执行:
Class<?> c = findLoadedClass(name);
c = null
接下来判断:
parent != null ?
我们知道,拓展类加载器的父类加载器是null,因此会进入执行:
c = findBootstrapClassOrNull(name);
该方法的功能是调用本地方法(C++实现)来查找引导类加载器是否加载过Add。
很明显没有加载过,因此:
c = null
进入下一个断点执行:
c = findClass(name);
findClass(name)在上节的自定义类加载器中也用到过,功能是寻找当前类加载器所能够加载的目录下是否有Add。
拓展类加载器的加载目录是jre.lib.ext,很明显Add不在此目录,因此拓展类加载器的loadClass函数最终返回的c是null。
回到上一层应用程序类加载器的loadClass函数,继续往下执行,发现c是null,则进入断点执行:
c = findClass(name);
我们知道,Add就在应用程序类加载器的加载目录中,因此成功被加载,最终返回:
c = Add对象
到这里,就能回答前言中的问题2了。
优点:
缺点:
回答前言中的问题四:双亲委派机制当然能够打破。
在第一节的第三小节中我们从源码上分析了双亲委派机制的实现逻辑,也知道每个类加载器的顶级父类是ClassLoader,顶级父类中有两个核心方法findClass和loadClass。我们在第一节的第二小节中的自定义类加载器里面重写过findClass,而双亲委派机制的核心实现在loadClass里,那想要打破这个机制,就得在自定义类加载器中重写loadClass,下面是打破双亲委派机制的自定义类加载器代码:
要重写loadClass,可以在ClassLoadr中直接把源码复制过来:
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;
}
}
然后删除双亲委派的部分:
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
}
最后将
c = findClass(name);
修改成:
if (!name.startsWith("com.jim.jvm.classload")){
c = this.getParent().loadClass(name);
}else{
c = findClass(name);
}
为什么呢?因为在加载自己的类之前还需要加载很多其它类库,比如核心类库,如果不修改,那么在自定义的路径下肯定找不到核心类库,因此需要将这些类委托给父加载器,也就是引用程序类加载器,然后通过双亲委派的流程来进行加载。
最终得到:
package com.jim.jvm.classload;
import java.io.FileInputStream;
import java.io.IOException;
public class MyClassLoader extends ClassLoader {
private String path; //默认加载路径
MyClassLoader(String path) {
// TODO Auto-generated constructor stub
super();
this.path = path;
}
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
// TODO Auto-generated method stub
byte[] b = new byte[0];
try {
b = loadData(name);
} catch (IOException e) {
e.printStackTrace();
}
return defineClass(name, b, 0, b.length);
}
@Override
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 t1 = System.nanoTime();
if (!name.startsWith("com.jim.jvm.classload")){
c = this.getParent().loadClass(name);
}else{
c = findClass(name);
}
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
private byte[] loadData(String name) throws IOException {
name = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(this.path + "/" + name + ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
}
运行测试:
package com.jim.jvm.classload;
public class MyClassLoaderTest {
public static void main(String[] args) throws Exception {
MyClassLoader loader1 = new MyClassLoader("D:/test");
String name = "com.jim.jvm.classload.Test";
loadClassByMyClassLoader(loader1, name);
}
private static void loadClassByMyClassLoader(ClassLoader loader, String name) throws Exception{
Class<?> c = loader.loadClass(name);
System.out.println(c.getClassLoader());
}
}
最终结果:
可以看到,我在应用程序类加载器的加载目中中也放置了Test类,而我想要加载的是D:/test/com/jim/jvm/classload/Test.class,最终输出的Test类的加载器也是自定义的类加载器,可见上面的自定义类加载器成功打破了双亲委派机制。
第一次发现肝源码如此有趣,继续加油!