Java类加载器

类加载器的作用

类加载器的作用就是把类的字节码加载到JVM。同时JVM规定,程序员可以用Java代码来自定义类加载器,把类的字节码信息加载到JVM中。

类加载器的类型

从java虚拟机角度来看,类加载器分成C++语言实现的启动类加载器属于虚拟机的而一部分,和java语言实现所有其他类的加载器,独立于虚拟机外部并且全部集成自抽象类java.lang.ClassLoader。

从开发者角度来看,类加载器分为以下3种:

启动类加载器(Bootstrap Class Loader)

负责加载系统的核心类(<JAVA_HOME>\lib目录下,或者被-Xbootclasspath参数所指定的),有了它之后才能保证java基本的运行环境。例如java.lang.Object,java.lang.String都是由它加载的。

扩展类加载器(Extension Class Loader)

负责加载系统<JAVA_HOME>\lib\ext目录中的,或者是被java.ext.dirs系统变量所指定的路径中的目录,也就是加载对JDK的扩展类,面向JNI的那些代码。

应用程序类加载器(Application Class Loader)

负责加载用户类。也就是我们平时java程序员自己写的代码程序类。

什么是双亲委派

App ClassLoader的父类加载器是Ext ClassLoader,Ext ClassLoader的父类加载器是Boot ClassLoader。如下图:

 

在加载一个类的时候,如果加载器还有父类加载器,那么会尝试让父类加载器去加载该类。只有当父类没有加载过该类,并且父类也无法加载该类的时候,自己才会加载。

Demo1

具体是什么意思,我们可以看如下代码:

public class ClassLoaderTest{

    public static void main(String[] args) {
        ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
        while (classLoader != null) {
            System.out.println(classLoader);
            // 获取父类加载器
            classLoader = classLoader.getParent();
        }
        System.out.println(classLoader);
    }

}

class ClassLoaderTest{

}

程序很简单,在main方法中打印ClassLoadTest的类加载器和父类加载器。这里有一个概念“一个类被加载时的默认类加载器,是和它外类类加载器是同一个”,所以此时执行程序后打印结果如下:

sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@60e53b93
null

打印结果分析

第一行结果说明:ClassLoaderTest使用的类加载器是AppClassLoader。

第二行结果说明:AppClassLoader父类加载器是ExtClassLoader

第三行结果说明:ExtClassLoader父类加器是Bootstrap ClassLoader,因为Bootstrap ClassLoader不是一个普通的Java类,所以ExtClassLoader的parent=null,所以第三行的打印结果为null就是这个原因。

Demo2

此时,如果我们把ClassLoaderTest达成Jar包,然后丢到JAVA_HOME/jre/lib/ext目录下去,再执行这段程序,此时的结果会是什么呢?

 

此时打印的结果如下

sun.misc.Launcher$ExtClassLoader@60e53b93
null

打印结果分析

当打印ClassLoaderTest的类加载器时,根据双亲委派首会传递到父类加载器Extension Class Loader;Extension ClassLoader会传递到它的父类Bootstrap Class Loader。因为Bootstrap ClassLoader是顶级的加载器,此时就去自己的类空间JAVA_HOME/jre/lib找,发现没有ClassLoaderTest后就告诉Ext我无能为力;Ext ClassLoader收到这个消息后就在自己类空间JAVA_HOME/jre/lib/ext找,发现有ClassLoaderTest这个类。所以就是用Ext ClassLoader加载。

为什么要有双亲委派

类加载器虽然只用于实现类的加载作用,但是实际意义远远不限于类加载阶段。所有的类最终都会被加载到内存中,对于任意一个类如何确定它再内存中的唯一性是根据类的全限定名+同一类加载器。更通俗的来讲就是即使这两个类来源于同一Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那么这两个类就必定不相等。

java.lang.String是我们很常用的一个类,根据双亲委派的这种模型,所以无论哪一个类需要使用String,最终都会被Bootstrap ClassLoader所加载。相反如果没有使用双亲委派模型的话,由各个类加载器自行加载,那么系统中将会有多个不用的String类,java类型体系中最基础的行为也就无法保证,应用程序将会一片混乱。

打破双亲委派

现在ClassLoaderTest是ext目录下的一个扩展类,假如现在ClassLoaderTest类中有一个方法出现了一个A a = new A();那么根据双亲委派这个类A肯定是被Ext发起然后传递到Boot。但是如果这个类A再classpath下面,那么加载的时候就会爆出ClassNotFound。

你肯定会问,怎么还会有这样的情况?现实是存在这种情况的,JDBC中的类被Boot加载,最后还是需要到classpath下面去找驱动类。这就说明有一些系统级别的类需要反过来调用用户类。这时候就需要打破双亲委派模型了。

如何打破双亲委派

如果系统类需要加载用户类,那么此时要怎么做的?我们可以在线程上下文中,把App类加载器绑定,然后线程运行到需要A类这一行的时候,使用getContextClassloader把App类加载器取出来,然后使用App.loadClass()方法去加载。这就是线程上下文Classloader的作用,用来打破双亲委派模型。

案例

public class ClassLoaderTest {

    // 加载扩展Ext目录下的类
    public void loadExtClass() throws Exception {
        ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
        System.out.println("方法loadExtClass使用的类加载器为" + classLoader);

        // 加载ExtClass
        Class<?> clazz = classLoader.loadClass("ExtClass");

        Constructor<?> constructor = clazz.getConstructor();
        System.out.println("ExtClass类正在加载" + constructor.newInstance());
    }

}

class ExtClass {

    public ExtClass() {
        System.out.println("ExtClass执行构造方法...");
    }

}

此时我们对之前放入ext目录下的ClassLoaderTest类进行了扩展,loadExtClass方法是加载了一个名字叫ExtClass的类,这个类再当前目录下面,我们查看它的类加载和方法。打包后丢入ext目录,然后写一个如下的类运行:

public class ClassLoaderDemo {

    public static void main(String[] args) throws Exception {

        ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
        Class<?> classLoaderTest = classLoader.loadClass("ClassLoaderTest");

        // 利用反射得到类
        Constructor<?> constructor = classLoaderTest.getConstructor();
        ClassLoaderTest obj = (ClassLoaderTest)constructor.newInstance();

        obj.loadExtClass();
    }

}

这个类也非常简单,获取ClassLoaderTest的类加载器,并加载“ClassLoaderTest”,之后使用反射生成ClassLoaderTest的实例,最后执行loadExtClass方法得到如下结果:

方法loadExtClass使用的类加载器为sun.misc.Launcher$ExtClassLoader@1d44bcfa
ExtClass执行构造方法...
ExtClass类正在加载ExtClass@66d3c617

 

和我们上面理解的双亲委派模型一样,ClassLoaderTest所使用的类加载器为ExtClassLoader,并且使用ExtClassLoader成功加载了ExtClass。

接下来我们要来写一个复杂一点的案例了,如下:p

ublic class ClassLoaderTest {
    // 加载classpath下的类
    public void loadClassUnderClasspath(String name) throws Exception {

        System.out.println("当前类的类加载器为" + ClassLoaderTest.class.getClassLoader());

        ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();

        System.out.println("和当前线程上线文绑定的类加载器为" + contextClassLoader);

        Class<?> aClass = contextClassLoader.loadClass(name);


        Constructor<?> contextClassLoaderConstructor = aClass.getConstructor(ClassLoader.class);
        // 执行构造器
        contextClassLoaderConstructor.newInstance(contextClassLoader);
    }

}

ClassLoaderTest中增加了一个loadClassUnderClasspath,意思就是用户传入当前目录下类的名字去加载。

系统类Ext需要去加载用户类,那么此时双亲委派就被打破,我们修改上文的程序,现在执行loadClassUnderClasspath

public class ClassLoaderDemo {

    public static void main(String[] args) throws Exception {
	// 多余代码同上
        obj.loadClassUnderClasspath("MyDriver");
    }
}
class MyDriver {
    public MyDriver(ClassLoader classLoader) {
        System.out.println("MyDriver正在被构造,使用的类加载器为" + classLoader);
    }
}

打印结果如下

当前类的类加载器为sun.misc.Launcher$ExtClassLoader@1d44bcfa
和当前线程上线文绑定的类加载器为sun.misc.Launcher$AppClassLoader@18b4aac2
MyDriver正在被构造,使用的类加载器为sun.misc.Launcher$AppClassLoader@18b4aac2

 

执行loadClassUnderClasspath时候的类加载器为ExtClassLoader说明当前类的外围加载器为Ext ClassLoader。但是我们通过

Thread.currentThread().getContextClassLoader();

方法获取的和当前线程上下文绑定的类加载为App ClassLoader,此时就可以愉快的加载用户类空间classpath下的类了。

这个也是双亲委派模型自身缺陷导致的,为了解决这个困境,Java的设计团队只好引入了这个不太优雅的设计:线程上下文类加载器。如果应用程序在全局范围类没有设置过得话,那么这个类加载器默认就是App ClassLoader

 

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值