一、前言
这是JVM
系列博客的第五篇,也是最后一篇,写完这篇博客,我就要暂时停止对JVM
的学习,开始学习其他方面的内容了。这篇博客就来说一说JVM
中的类加载器,以及类加载的双亲委派模型。
二、正文
2.1 什么是类加载器
首先我们要知道一件事,那就是什么是类加载器?大家都知道,我们编写的代码需要先被编译为class
字节码才能被执行,JVM
解释器只能识别字节码,而不能执行Java
源代码。而程序都是在内存中执行的,所以,为了能够执行字节码,就需要将它读取到内存中。将字节码读取到内存中这个工作,就是由类加载器来完成的。类加载器根据提供的全限定类名(包名+类名),找到对应路径下的类的class
文件,将其读取到JVM
管理的方法区中,这样才能执行其中的代码指令,访问类中的数据。关于类加载机制,可以参考这篇博客:JVM基础——分析类的加载过程。
对于类加载器,需要注意一个问题。每一个类都是由它本身和类加载器一同来确定唯一性。这是什么意思?这就是说,如果一个类的字节码,分别使用两个不同的类加载器进行加载,则对于JVM来说,会将这两次加载识别为不同的两个类。举个例子,我们有一个类Test
,创建了一个自定义的类加载器去加载它,并通过自定义的类加载器得到的Class
对象创建Test
对象t
(反射),此时运行t instanceof Test
,得到的将是false
。因为这个对象是通过自定义类加载器加载的Test
创建,而intanceof
语句中的Test
是JVM
类加载器加载的Test
,对于JVM
来说,这是两个不同的类。除此之外,对于拥有相同全限定名的类,同一个类加载器只会加载一次,不会重复加载。
2.2 类加载器的分类
在Java
中,类加载器一般分为四类,分别是:
- 启动类加载器(Bootstrap ClassLoader);
- 扩展类加载器(Extension ClassLoader);
- 应用程序类加载器(Application ClassLoader);
- 自定义类加载器(User ClassLoader);
下面就来分别介绍一下这四种不同的类加载器。
(1)启动类加载器(Bootstrap ClassLoader)
启动类加载器不是由Java
语言实现,而是由C++
实现的(HotSpot虚拟机中),它负责加载%JAVA_HOME%\lib
目录下的类,比如String
,Integer
,HashMap
…….这些类都是放在这个目录下,所以都是由启动类加载器加载。除此之外,JVM
还提供了一个配置参数-Xbootclasspath
,这个参数指定的目录下的类文件(如jar
包,class
文件),也会被启动类加载器加载。由于这个类加载器是由C++
实现,所以它并不属于Java
的一部分,而是虚拟机的一部分,不能在Java
代码中直接引用。通过下面的代码可以尝试获得String
类的类加载器,也就是启动类加载器,但是输出结果为null
,因为它不属于Java
的一部分:
1 | public static void main(String[] args) { |
(2)扩展类加载器(Extension ClassLoader)
扩展类加载器是由Java
实现的,实现类是sun.misc.Launcher$ExtClassLoader
(名字可以看出这是一个内部类),这个类加载器负责加载%JAVA_HOME%\lib\ext
目录下的类。由于这个类加载器是由Java
实现,所以可以直接在Java
程序中引用。我们可以通过一个%JAVA_HOME%\lib\ext
目录下的类的Class
对象来获得这个类加载器,也可以通过下面这种方式获得(以下方法基于双亲委派模型,后面解释):
1 | public static void main(String[] args) { |
(3)应用程序类加载器(Application ClassLoader)
这个类加载期也是由Java
语言实现的,实现类是sun.misc.Launcher$AppClassLoader
,从$
符号可以看出,这也是一个内部类。它负责加载类路径(CLASSPATH
)下的类库,而我们编写的代码也是属于这个路径下的(CLASSPATH
包含当前所在路径)。所以,当我们自己编写的Java
代码没有指定类加载器,则默认使用这个加载器进行加载。由于这个类加载器是Java
实现,所以也可以在我们的代码中引用,引用方法如下:
1 | public static void main(String[] args) { |
(4)自定义类加载器(User ClassLoader)
根据需要,我们可以编写自己的类加载器,而自己编写的类加载器就称为自定义加载器。编写自定义类加载器的方式很简单:继承ClassLoader
类,然后重写其中的方法即可。对于Java
实现的类加载器,都是调用其中的loadClass()
方法对类进行加载的,所以我们可以重写这个方法,但是这种做法不推荐,因为这样容易破坏类的加载机制(之后会讲到的双亲委派模型)。最好的做法是重写findClass()
方法,这个方法会在loadClass()
中被调用。在findClass()
方法中,读取需要加载的类的class
文件,转换成一个字节数组,再调用父类的defineClass
方法将字节数组转换为Class
对象返回即可,如下所示:
1 | // 自定义类加载器,实现ClassLoader |
2.3 双亲委派模型
上面这张图所对应的关系,就被称为类加载器的双亲委派模型。对于每一个类加载器,都有一个父加载器(除了启动类加载器)。当类加载器需要加载一个类时,它首先会将这个类委托给它的父加载器进行加载,若这个父加载器也有父加载器,则继续向上委托,一直到启动类加载器。启动类加载器尝试加载这个类,若这个类在自己管理的目录之下,且还没有被加载,则成功加载,否则加载失败;若加载失败,则交给下层的扩展类加载器进行加载,而扩展类加载器也进行同样的操作。总的来说,双亲委派模型就是先将类交给父类加载器尝试加载,若加载失败,再由子类来加载,每一层都是如此。而在实际实现中,这种父子关系并不是由继承实现,而是由组合实现,在ClassLoader
类中,有一个属性,名叫parent
,就是指向它的父加载器的引用(之前的扩展类加载器代码就是这样得到的)。
下面我们是ClassLoader
类的loadClass
方法源码,看看双亲委派模型的具体实现代码:
1 | // name为类的全限定类名 |
通过上面的代码我们可以清楚地看到双亲委派机制是如何实现的。那这种机制有什么好处呢?使用双亲委派机制可以确保类是被第一个满足条件的类加载器所加载。而对于一个类加载器来说,每一个类只会加载一次,所以不会造成同一个类加载多次,或者一个类被不同的类加载器所加载的情况。之前说过,类的唯一性是由它的本身和加载它的类加载器确定的,使用双亲委派模型很好地避免了类被多个加载器加载,导致在内存中出现相同却不等价的类。比如说我们也定义一个java.lang.String
类,如果没有双亲委派模型,内存中将出现两个String
,但是在我们看来只有一个,此时将会导致程序产生莫名其妙的错误。而双亲委派模型保证了这种同名的类永远没有办法被加载运行,所以我们自己定义的重名类将永远无法使用,虽然编译可以通过(这里可以自己去试试)。
当然,双亲委派模型并不是JVM
规范中强制要求的,而是一种推荐的策略。所以我们完全可以在编写自己的类加载器时不遵守这个模型,比如重写loadClass
方法覆盖这个机制。但是如果不是必要,还是不推荐这么做。
三、总结
关于类加载器和双亲委派模型就先说这么多吧。上面对这两部分内容做了一个还算具体的描述,相信看完之后会对Java
的一些相关特性有更加深入的理解,也能解决我们平常的一些疑惑。
四、参考
- 《深入理解Java虚拟机》