计算机系统应用教程网站

网站首页 > 技术文章 正文

Java基础之:类加载原理

btikc 2024-09-08 11:58:52 技术文章 18 ℃ 0 评论

今天小编打算讲一下Java的类加载原理,可能有朋友会觉得大部分开发都不会直接和加载机制打交道,没有必要去理解这么底层的东西。但我不这么认为,譬如对于java.lang.ClassNotFoundExcetpion这个异常很多人都不陌生,但是你知道如何去排查这个异常么,这就涉及到类加载机制。而且了解类加载对理解java虚拟机的连接模型和java语言的动态性都有很大帮助。

1、什么是类加载器:

先放一张JVM的执行java代码的流程图:

类加载器负责加载文件系统、网络或其他来源的类文件。类加载器其实是一个用来加载类文件的类。Java源代码通过javac编译器编译成类文件。然后JVM来执行类文件中的字节码来执行程序。

2、JVM预定义类型类加载器:

我们首先看一下JVM预定义的三种类型类加载器,当一个 JVM 启动的时候,Java 缺省开始使用如下三种类型类装入器:

启动(Bootstrap)类加载器:引导类装入器是用本地代码实现的类装入器,它负责将 <Java_Runtime_Home>/lib 下面的类库加载到内存中。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。

标准扩展(Extension)类加载器:扩展类加载器是由 Sun 的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将 < Java_Runtime_Home >/lib/ext 或者由系统变量 java.ext.dir 指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。

系统(System)类加载器:系统类加载器是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。它负责将系统类路径(CLASSPATH)中指定的类库加载到内存中。开发者可以直接使用系统类加载器。

除了以上列举的三种类加载器,还有一种比较特殊的类型就是线程上下文类加载器,这个暂时不介绍。

3、类加载双亲委派机制:

JVM在加载类时默认采用的是双亲委派机制。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。关于虚拟机默认的双亲委派机制,我们可以从标准扩展类加载器为例作简单分析。

可以看出,类加载器均是继承自java.lang.ClassLoader抽象类。我们下面我们就看简要介绍一下java.lang.ClassLoader中几个最重要的方法:

//加载指定名称(包括包名)的二进制类型,供用户调用的接口
public Class<?> loadClass(String name) throws ClassNotFoundException{//…}
//加载指定名称(包括包名)的二进制类型,同时指定是否解析(但是,这里的resolve参数不一定真正能达到解析的效果~_~),供继承用
protectedsynchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{//…}
//findClass方法一般被loadClass方法调用去加载指定名称类,供继承用
protected Class<?> findClass(String name) throws ClassNotFoundException {//…}
//定义类型,一般在findClass方法中读取到对应字节码后调用,可以看出不可继承(说明:JVM已经实现了对应的具体功能,解析对应的字节码,产生对应的内部数据结构放置到方法区,所以无需覆写,直接调用就可以了)
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError{//…}

通过进一步分析标准扩展类加载器(sun.misc.Launcher$ExtClassLoader)和系统类加载器(sun.misc.Launcher$AppClassLoader)的代码以及其公共父类(java.net.URLClassLoader和java.security.SecureClassLoader)的代码可以看出,都没有覆写java.lang.ClassLoader中默认的加载委派规则---loadClass(…)方法。既然这样,我们就可以通过分析java.lang.ClassLoader中的loadClass(String name)方法的代码就可以分析出虚拟机默认采用的双亲委派机制到底是什么模样:

public Class<?> loadClass(String name)throws ClassNotFoundException {
 return loadClass(name, false);
}
protectedsynchronized Class<?> loadClass(String name, boolean resolve)
 throws ClassNotFoundException {
 // 首先判断该类型是否已经被加载
 Class c = findLoadedClass(name);
 if (c == null) {
 //如果没有被加载,就委托给父类加载或者委派给启动类加载器加载
 try {
 if (parent != null) {
//如果存在父类加载器,就委派给父类加载器加载
 c = parent.loadClass(name, false);
 } else {
//如果不存在父类加载器,就检查是否是由启动类加载器加载的类,通过调用本地方法native Class findBootstrapClass(String name)
 c = findBootstrapClass0(name);
 }
 } catch (ClassNotFoundException e) {
 // 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能
 c = findClass(name);
 }
 }
 if (resolve) {
 resolveClass(c);
 }
 return c;
 }

下面我们就接着分析一下启动类加载器标准扩展类加载器系统类加载器三者之间的关系。可能大家已经从各种资料上面看到了如下类似的一幅图片:

下面我们就用代码具体测试一下:

示例代码:

public static void main(String[] args) {
 try {
 System.out.println(ClassLoader.getSystemClassLoader());
 System.out.println(ClassLoader.getSystemClassLoader().getParent();
 System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());
 } catch (Exception e) {
 e.printStackTrace();
 }
}

说明:通过java.lang.ClassLoader.getSystemClassLoader()可以直接获取到系统类加载器。

代码输出如下:

sun.misc.Launcher$AppClassLoader@197d257
sun.misc.Launcher$ExtClassLoader@7259da
null

通过以上的代码输出,我们可以判定系统类加载器的父加载器是标准扩展类加载器,但是我们试图获取标准扩展类加载器的父类加载器时确得到了null,就是说标准扩展类加载器本身强制设定父类加载器为null。但是根据上面提到的loadclass方法的逻辑,只有父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能。所以标准扩展类加载器还是会先判断启动类加载器是否已经加载过,所以上图的委派关系事实上是仍就成立的。

类加载双亲委托举例:

如果将一个简单的JavaBean生成的class文件分别放在输出目录,加载类会是sun.misc.Launcher$AppClassLoader@823d826

拷贝一份到< Java_Runtime_Home >/lib/ext,加载类则变成sun.misc.Launcher$ExtClassLoader@7349de

再拷贝一份到< Java_Runtime_Home >/lib。加载类却还是

sun.misc.Launcher$ExtClassLoader@7298da

说明放置到< Java_Runtime_Home >/lib目录下的TestBean对应的class字节码并没有被加载,这其实和前面讲的双亲委派机制并不矛盾。虚拟机出于安全等因素考虑,不会加载< Java_Runtime_Home >/lib存在的陌生类开发者通过将要加载的非JDK自身的类放置到此目录下期待启动类加载器加载是不可能的。做个进一步验证,删除< Java_Runtime_Home >/lib/ext目录下和工程输出目录下的TestBean对应的class文件,然后再运行测试代码,则将会有ClassNotFoundException异常抛出。有关这个问题,大家可以在java.lang.ClassLoader中的loadClass(String name, boolean resolve)方法中设置相应断点运行测试三进行调试,会发现findBootstrapClass0()会抛出异常,然后在下面的findClass方法中被加载,当前运行的类加载器正是扩展类加载器(sun.misc.Launcher$ExtClassLoader),这一点可以通过JDT中变量视图查看验证。

4、最后总结一下每个ClassLoader加载Class的过程:

1.检测此Class是否载入过(即在cache中是否有此Class),如果有到8,如果没有到2

2.如果parent classloader不存在(没有parent,那parent一定是bootstrap classloader了),到4

3.请求parent classloader载入,如果成功到8,不成功到5

4.请求jvm从bootstrap classloader中载入,如果成功到8

5.寻找Class文件(从与此classloader相关的类路径中寻找)。如果找不到则到7.

6.从文件中载入Class,到8.

7.抛出ClassNotFoundException.

8.返回Class.

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表