超详细java中的ClassLoader详解_classloader 支持正则-程序员宅基地

技术标签: java  

作者简介 原创微信公众号郭霖 WeChat ID: guolin_blog

转自:https://blog.csdn.net/briblue/article/details/54973413

前言

ClassLoader 翻译过来就是 类加载器,普通的Java开发者其实用到的不多,但对于某些框架开发者来说却非常常见。理解 ClassLoader 的加载机制,也有利于我们编写出更高效的代码。

ClassLoader 的具体作用就是将 class文件 加载到 jvm虚拟机 中去,程序就可以正确运行了。但是,jvm 启动的时候,并不会一次性加载所有的class文件,而是根据需要去动态加载。想想也是的,一次性加载那么多jar包那么多class,那内存不崩溃。本文的目的也是学习 ClassLoader 这种加载机制。

备注:本文篇幅比较长,但内容简单,大家不要恐慌,安静地耐心翻阅就是

Class文件的认识

我们都知道在Java中程序是运行在虚拟机中,我们平常用文本编辑器或者是IDE编写的程序都是.java格式的文件,这是最基础的源码,但这类文件是不能直接运行的。如我们编写一个简单的程序 HelloWorld.java

如图:

然后,我们需要在命令行中进行java文件的编译:

javacHelloWorld.java

可以看到目录下生成了.class文件。我们再从命令行中执行命令:

javaHelloWorld

上面是基本代码示例,是所有入门JAVA语言时都学过的东西,这里重新拿出来是想让大家将焦点回到 class文件 上,class文件 是字节码格式文件,java虚拟机并不能直接识别我们平常编写的 .java源文件,所以需要javac这个命令转换成 .class文件。另外,如果用 C 或者 Python 编写的程序正确转换成 .class文件后,java虚拟机也是可以识别运行的。更多信息大家可以参考这篇:

http://blog.csdn.net/zhangjg_blog/article/details/21486985

了解了 .class文件后,我们再来思考下,我们平常在 Eclipse 中编写的 java程序 是如何运行的,也就是我们自己编写的各种类是如何被加载到 jvm(java虚拟机) 中去的。

你还记得Java环境变量吗

初学java的时候,最害怕的就是下载 JDK 后要配置环境变量了,关键是当时不理解,所以战战兢兢地照着书籍上或者是网络上的介绍进行操作。然后下次再弄的时候,又忘记了而且是必忘。当时,心里的想法很气愤的,想着是–这东西一点也不人性化,为什么非要自己配置环境变量呢?太不照顾菜鸟和新手了,很多菜鸟就是因为卡在环境变量的配置上,遭受了太多的挫败感。

因为我是在Windows下编程的,所以只讲Window平台上的环境变量,主要有3个:JAVA_HOMEPATHCLASSPATH

JAVA_HOME

指的是你JDK安装的位置,一般默认安装在C盘,如:

C:\ProgramFiles\Java\jdk1.8.0_91

PATH

将程序路径包含在 PATH 当中后,在命令行窗口就可以直接键入它的名字了,而不再需要键入它的全路径,比如上面代码中我用的到javacjava两个命令。一般的:

PATH=%JAVA_HOME%\bin;%JAVA_HOME%\jre\bin;%PATH%;

也就是在原来的PATH路径上添加JDK目录下的bin目录jre目录的bin.

CLASSPATH

CLASSPATH=.;%JAVA_HOME%\lib;%JAVA_HOME%\lib\tools.jar

一看就是指向jar包路径。需要注意的是前面的.;.代表当前目录。

环境变量的设置与查看

设置可以右击我的电脑,然后点击属性,再点击高级,然后点击环境变量,具体不明白的自行查阅文档。查看的话可以打开命令行窗口:

echo%JAVA_HOME%

echo%PATH%

echo%CLASSPATH%

好了,扯远了,知道了环境变量,特别是 CLASSPATH 时,我们进入今天的主题Classloader.

Java类加载流程

Java语言系统自带有三个类加载器:

Bootstrap ClassLoader最顶层的加载类,主要加载核心类库,%JRE_HOME%\lib 下的 rt.jar、resources.jar、charsets.jar 和 class等。另外需要注意的是可以通过启动jvm时指定 -Xbootclasspath 和 路径 来改变 Bootstrap ClassLoader 的加载目录。比如 java -Xbootclasspath/a:path 被指定的文件追加到默认的 bootstrap 路径中。我们可以打开我的电脑,在上面的目录下查看,看看这些jar包是不是存在于这个目录。

Extention ClassLoader扩展的类加载器,加载目录 %JRE_HOME%\lib\ext 目录下的jar包和class文件。还可以加载 -D java.ext.dirs 选项指定的目录。

Appclass Loader也称为 SystemAppClass 加载当前应用的classpath的所有类。

我们上面简单介绍了 3个ClassLoader。说明了它们加载的路径。并且还提到了-Xbootclasspath-D java.ext.dirs这两个虚拟机参数选项。

加载顺序

我们看到了系统的3个类加载器,但我们可能不知道具体哪个先行呢?我可以先告诉你答案

1. Bootstrap CLassloder

2. Extention ClassLoader

3. AppClassLoader

为了更好的理解,我们可以查看源码,sun.misc.Launcher

http://www.grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/8u40-b25/sun/misc/Launcher.java

它是一个 java虚拟机 的入口应用:

源码有精简,我们可以得到相关的信息。

1.Launcher 初始化了 ExtClassLoader 和 AppClassLoader。

2.Launcher 中并没有看见 BootstrapClassLoader,但通过 System.getProperty("sun.boot.class.path") 得到了字符串 bootClassPath,这个应该就是 BootstrapClassLoader 加载的jar包路径。

我们可以先代码测试一下sun.boot.class.path是什么内容。

System.out.println(System.getProperty("sun.boot.class.path"));

得到的结果是:

可以看到,这些全是JRE目录下的jar包或者是class文件。

ExtClassLoader源码

如果你有足够的好奇心,你应该会对它的源码感兴趣:

我们先前的内容有说过,可以指定-D java.ext.dirs参数来添加和改变 ExtClassLoader 的加载路径。这里我们通过可以编写测试代码:

System.out.println(System.getProperty("java.ext.dirs"));

结果如下:

C:\ProgramFiles\Java\jre1.8.0_91\lib\ext;C:\Windows\Sun\Java\lib\ext

AppClassLoader源码

可以看到 AppClassLoader 加载的就是java.class.path下的路径。我们同样打印它的值:

System.out.println(System.getProperty("java.class.path"));

结果:

D:\workspace\ClassLoaderDemo\bin

这个路径其实就是当前 java工程目录bin,里面存放的是编译生成的class文件。

好了,自此我们已经知道了 BootstrapClassLoader、ExtClassLoader、AppClassLoader 实际是查阅相应的环境属性 sun.boot.class.path、java.ext.dirs 和 java.class.path 来加载资源文件的。

接下来我们探讨它们的加载顺序,我们先用 Eclipse 建立一个java工程:

然后创建一个Test.java文件:

publicclassTest{}

然后,编写一个 ClassLoaderTest.java 文件:

我们获取到了 Test.class 文件的类加载器,然后打印出来。结果是:

ClassLoaderis:sun.misc.Launcher$AppClassLoader@73d16e93

也就是说明 Test.class文件 是由 AppClassLoader 加载的。

这个 Test类 是我们自己编写的,那么 int.class 或者是 String.class 的加载是由谁完成的呢?我们可以在代码中尝试:

运行一下,却报错了:

提示的是空指针,意思是 int.class 这类基础类没有类加载器加载?

当然不是!int.class 是由 Bootstrap ClassLoader 加载的。要想弄明白这些,我们首先得知道一个前提。

每个类加载器都有一个父加载器

每个类加载器都有一个父加载器,比如加载 Test.class 是由 AppClassLoader 完成,那么 AppClassLoader 也有一个父加载器,怎么样获取呢?很简单,通过 getParent 方法。比如代码可以这样编写:

运行结果如下:

这个说明,AppClassLoader 的父加载器是 ExtClassLoader。那么 ExtClassLoader 的父加载器又是谁呢?

运行结果:

又是一个空指针异常,这表明 ExtClassLoader 也没有父加载器。那么,为什么标题又是每一个加载器都有一个父加载器呢?这不矛盾吗?为了解释这一点,我们还需要看下面的一个基础前提。

父加载器不是父类

我们先前已经粘贴了 ExtClassLoader 和 AppClassLoader 的代码:

可以看见 ExtClassLoader 和 AppClassLoader 同样继承自 URLClassLoader,但上面一小节代码中,为什么调用 AppClassLoader的getParent() 代码会得到 ExtClassLoader 的实例呢?先从 URLClassLoader 说起,这个类又是什么?先上一张类的继承关系图:

URLClassLoader 的源码中并没有找到 getParent() 方法。这个方法在 ClassLoader.java 中:

我们可以看到 getParent() 实际上返回的就是一个 ClassLoader 对象 parent,parent 的赋值是在 ClassLoader 对象的构造方法中,它有两个情况:

1.由外部类创建 ClassLoader 时直接指定一个 ClassLoader 为 parent。

2.由 getSystemClassLoader() 方法生成,也就是在 sun.misc.Laucher 通过 getClassLoader() 获取,也就是 AppClassLoader。直白的说,一个 ClassLoader 创建时如果没有指定 parent,那么它的 parent 默认就是 AppClassLoader。

我们主要研究的是 ExtClassLoader 与 AppClassLoader 的 parent 的来源,正好它们与 Launcher类 有关,我们上面已经粘贴过 Launcher 的部分代码。

我们需要注意的是:

代码已经说明了问题 AppClassLoader 的 paren t是一个 ExtClassLoader 实例。

ExtClassLoader 并没有直接找到对 parent 的赋值。它调用了它的父类也就是 URLClassLoder 的构造方法并传递了3个参数。

对应的代码:

答案已经很明了了,ExtClassLoader 的 parent 为null

上面张贴这么多代码也是为了说明 AppClassLoader的parent 是 ExtClassLoader,ExtClassLoader 的 parent 是null。这符合我们之前编写的测试代码。

不过,细心的同学发现,还是有疑问的我们只看到 ExtClassLoader 和 AppClassLoader 的创建,那么 BootstrapClassLoader 呢?

还有,ExtClassLoader 的父加载器为 null,但是 Bootstrap CLassLoader 却可以当成它的父加载器这又是为何呢?

我们继续往下进行。

Bootstrap ClassLoader是由C++编写的

Bootstrap ClassLoader 是由C/C++编写的,它本身是虚拟机的一部分,所以它并不是一个JAVA类,也就是无法在java代码中获取它的引用,JVM 启动时通过 Bootstrap类 加载器加载 rt.jar 等核心jar包中的 class文件,之前的int.class,String.class都是由它加载。

然后呢,我们前面已经分析了,JVM 初始化 sun.misc.Launcher 并创建 Extension ClassLoader 和 AppClassLoader实例。并将 ExtClassLoader 设置为 AppClassLoader 的父加载器。Bootstrap 没有父加载器,但是它却可以作用一个 ClassLoader 的父加载器。比如 ExtClassLoader。这也可以解释之前通过 ExtClassLoader 的 getParent方法 获取为null的现象。具体是什么原因,很快就知道答案了。

双亲委托

我们终于来到了这一步了。

一个类加载器查找 class 和 resource 时,是通过“委托模式”进行的,它首先判断这个class是不是已经加载成功,如果没有的话它并不是自己进行查找,而是先通过父加载器,然后递归下去,直到 Bootstrap ClassLoader,如果 Bootstrap classloader 找到了,直接返回,如果没有找到,则一级一级返回,最后到达自身去查找这些对象。这种机制就叫做双亲委托

整个流程可以如下图所示:

这张图是用时序图画出来的,不过画出来的结果我却自己都觉得不理想。

大家可以看到2根箭头蓝色的代表类加载器向上委托的方向,如果当前的类加载器没有查询到这个 class对象 已经加载就请求父加载器(不一定是父类)进行操作,然后以此类推。直到 Bootstrap ClassLoader。如果 Bootstrap ClassLoader 也没有加载过此class实例,那么它就会从它指定的路径中去查找,如果查找成功则返回,如果没有查找成功则交给子类加载器,也就是ExtClassLoader,这样类似操作直到终点,也就是我上图中的红色箭头示例。用序列描述一下:

1.一个 AppClassLoader 查找资源时,先看看缓存是否有,缓存有从缓存中获取,否则委托给父加载器。

2.递归,重复第1部的操作。

3.如果 ExtClassLoader 也没有加载过,则由 Bootstrap ClassLoader 出面,它首先查找缓存,如果没有找到的话,就去找自己的规定的路径下,也就是 sun.mic.boot.class 下面的路径。找到就返回,没有找到,让子加载器自己去找。

4.Bootstrap ClassLoader 如果没有查找成功,则 ExtClassLoader 自己在 java.ext.dirs 路径中去查找,查找成功就返回,查找不成功,再向下让子加载器找。

5.ExtClassLoader 查找不成功,AppClassLoader 就自己查找,在 java.class.path 路径下查找。找到就返回。如果没有找到就让子类找,如果没有子类会怎么样?抛出各种异常。

上面的序列,详细说明了双亲委托的加载流程。我们可以发现委托是从下向上,然后具体查找过程却是自上至下

我说过上面用时序图画的让自己不满意,现在用框图,最原始的方法再画一次:

上面已经详细介绍了加载过程,但具体为什么是这样加载,我们还需要了解几个个重要的方法 loadClass()、findLoadedClass()、findClass()、defineClass()。

重要方法

loadClass()

JDK文档中是这样写的,通过指定的全限定类名加载 class,它通过同名的 loadClass(String,boolean) 方法:

protectedClassloadClass(Stringname,booleanresolve) throwsClassNotFoundException

上面是方法原型,一般实现这个方法的步骤是

1.执行 findLoadedClass(String) 去检测这个class是不是已经加载过了。

2.执行父加载器的 loadClass方法。如果父加载器为null,则jvm内置的加载器去替代,也就是 Bootstrap ClassLoader。这也解释了 ExtClassLoader 的 parent 为 null,但仍然说 Bootstrap ClassLoader 是它的父加载器。

3.如果向上委托父加载器没有加载成功,则通过 findClass(String) 查找。

如果class在上面的步骤中找到了,参数 resolve 又是true的话,那么 loadClass() 又会调用 resolveClass(Class) 这个方法来生成最终的Class对象。 我们可以从源代码看出这个步骤:

代码解释了双亲委托。要注意的是如果要编写一个 classLoader 的子类,也就是自定义一个 classloader,建议覆盖 findClass()方法,而不要直接改写 loadClass()方法。另外:

前面说过 ExtClassLoader 的 parent 为 null,所以它向上委托时,系统会为它指定 Bootstrap ClassLoader。

自定义ClassLoader

不知道大家有没有发现,不管是 Bootstrap ClassLoader 还是 ExtClassLoader等,这些类加载器都只是加载指定的目录下的jar包或者资源。如果在某种情况下,我们需要动态加载一些东西呢?比如从D盘某个文件夹加载一个class文件,或者从网络上下载class主内容然后再进行加载,这样可以吗?

如果要这样做的话,需要我们自定义一个 classloader。

自定义步骤

1.编写一个类继承自 ClassLoader 抽象类。

2.复写它的 findClass() 方法。

3.在 findClass() 方法中调用 defineClass()

defineClass()

这个方法在编写自定义 classloader 的时候非常重要,它能将 class 二进制内容转换成 Class对象,如果不符合要求的会抛出各种异常。

注意点

一个 ClassLoader 创建时如果没有指定 parent,那么它的 parent 默认就是 AppClassLoader。

上面说的是,如果自定义一个 ClassLoader,默认的 parent 父加载器是 AppClassLoader,因为这样就能够保证它能访问系统内置加载器加载成功的class文件。

自定义ClassLoader示例之DiskClassLoader

假设我们需要一个自定义的classloader,默认加载路径为 D:\lib 下的jar包和资源。

我们写编写一个测试用的类文件,Test.java:

然后将它编译过年class文件Test.class放到D:\lib这个路径下。

我们编写DiskClassLoader的代码:

我们在 findClass() 方法中定义了查找class的方法,然后数据通过 defineClass() 生成了Class对象。

现在我们要编写测试代码。我们知道如果调用一个 Test对象 的 say方法,它会输出”Say Hello”这条字符串。但现在是我们把 Test.class 放置在应用工程所有的目录之外,我们需要加载它,然后执行它的方法。具体效果如何呢?我们编写的 DiskClassLoader 能不能顺利完成任务呢?我们拭目以待。

我们点击运行按钮,结果显示:

可以看到,Test类的say方法正确执行,也就是我们写的 DiskClassLoader 编写成功。

回首

讲了这么大的篇幅,自定义ClassLoader才姗姗来迟。 很多同学可能觉得前面有些啰嗦,但我按照自己的思路,我觉得还是有必要的。因为我是围绕一个关键字进行讲解的。

关键字是什么?关键字路径

从开篇的环境变量

到3个主要的JDK自带的类加载器

到自定义的ClassLoader

它们的关联部分就是路径,也就是要加载的class或者是资源的路径。

BootStrap ClassLoader、ExtClassLoader、AppClassLoader 都是加载指定路径下的jar包。如果我们要突破这种限制,实现自己某些特殊的需求,我们就得自定义ClassLoader,自已指定加载的路径,可以是磁盘、内存、网络或者其它。



作者:木木00
链接:https://www.jianshu.com/p/48aaf87e1e82
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/ws6013480777777/article/details/86604245

智能推荐

FTP命令字和返回码_ftp 登录返回230-程序员宅基地

文章浏览阅读3.5k次,点赞2次,收藏13次。为了从FTP服务器下载文件,需要要实现一个简单的FTP客户端。FTP(文件传输协议) 是 TCP/IP 协议组中的应用层协议。FTP协议使用字符串格式命令字,每条命令都是一行字符串,以“\r\n”结尾。客户端发送格式是:命令+空格+参数+"\r\n"的格式服务器返回格式是以:状态码+空格+提示字符串+"\r\n"的格式,代码只要解析状态码就可以了。读写文件需要登陆服务器,特殊用..._ftp 登录返回230

centos7安装rabbitmq3.6.5_centos7 安装rabbitmq3.6.5-程序员宅基地

文章浏览阅读648次。前提:systemctl stop firewalld 关闭防火墙关闭selinux查看getenforce临时关闭setenforce 0永久关闭sed-i'/SELINUX/s/enforcing/disabled/'/etc/selinux/configselinux的三种模式enforcing:强制模式,SELinux 运作中,且已经正确的开始限制..._centos7 安装rabbitmq3.6.5

idea导入android工程,idea怎样导入Android studio 项目?-程序员宅基地

文章浏览阅读5.8k次。满意答案s55f2avsx2017.09.05采纳率:46%等级:12已帮助:5646人新版Android Studio/IntelliJ IDEA可以直接导入eclipse项目,不再推荐使用eclipse导出gradle的方式2启动Android Studio/IntelliJ IDEA,选择 import project3选择eclipse 项目4选择 create project f..._android studio 项目导入idea 看不懂安卓项目

浅谈AI大模型技术:概念、发展和应用_ai大模型应用开发-程序员宅基地

文章浏览阅读860次,点赞2次,收藏6次。AI大模型技术已经在自然语言处理、计算机视觉、多模态交互等领域取得了显著的进展和成果,同时也引发了一系列新的挑战和问题,如数据质量、计算效率、知识可解释性、安全可靠性等。城市运维涉及到多个方面,如交通管理、环境监测、公共安全、社会治理等,它们需要处理和分析大量的多模态数据,如图像、视频、语音、文本等,并根据不同的场景和需求,提供合适的决策和响应。知识搜索有多种形式,如语义搜索、对话搜索、图像搜索、视频搜索等,它们可以根据用户的输入和意图,从海量的数据源中检索出最相关的信息,并以友好的方式呈现给用户。_ai大模型应用开发

非常详细的阻抗测试基础知识_阻抗实部和虚部-程序员宅基地

文章浏览阅读8.2k次,点赞12次,收藏121次。为什么要测量阻抗呢?阻抗能代表什么?阻抗测量的注意事项... ...很多人可能会带着一系列的问题来阅读本文。不管是数字电路工程师还是射频工程师,都在关注各类器件的阻抗,本文非常值得一读。全文13000多字,认真读完大概需要2小时。一、阻抗测试基本概念阻抗定义:阻抗是元器件或电路对周期的交流信号的总的反作用。AC 交流测试信号 (幅度和频率)。包括实部和虚部。​图1 阻抗的定义阻抗是评测电路、元件以及制作元件材料的重要参数。那么什么是阻抗呢?让我们先来看一下阻抗的定义。首先阻抗是一个矢量。通常,阻抗是_阻抗实部和虚部

小学生python游戏编程arcade----基本知识1_arcade语言 like-程序员宅基地

文章浏览阅读955次。前面章节分享试用了pyzero,pygame但随着想增加更丰富的游戏内容,好多还要进行自己编写类,从今天开始解绍一个新的python游戏库arcade模块。通过此次的《连连看》游戏实现,让我对swing的相关知识有了进一步的了解,对java这门语言也有了比以前更深刻的认识。java的一些基本语法,比如数据类型、运算符、程序流程控制和数组等,理解更加透彻。java最核心的核心就是面向对象思想,对于这一个概念,终于悟到了一些。_arcade语言 like

随便推点

【增强版短视频去水印源码】去水印微信小程序+去水印软件源码_去水印机要增强版-程序员宅基地

文章浏览阅读1.1k次。源码简介与安装说明:2021增强版短视频去水印源码 去水印微信小程序源码网站 去水印软件源码安装环境(需要材料):备案域名–服务器安装宝塔-安装 Nginx 或者 Apachephp5.6 以上-安装 sg11 插件小程序已自带解析接口,支持全网主流短视频平台,搭建好了就能用注:接口是公益的,那么多人用解析慢是肯定的,前段和后端源码已经打包,上传服务器之后在配置文件修改数据库密码。然后输入自己的域名,进入后台,创建小程序,输入自己的小程序配置即可安装说明:上传源码,修改data/_去水印机要增强版

verilog进阶语法-触发器原语_fdre #(.init(1'b0) // initial value of register (1-程序员宅基地

文章浏览阅读557次。1. 触发器是FPGA存储数据的基本单元2. 触发器作为时序逻辑的基本元件,官方提供了丰富的配置方式,以适应各种可能的应用场景。_fdre #(.init(1'b0) // initial value of register (1'b0 or 1'b1) ) fdce_osc (

嵌入式面试/笔试C相关总结_嵌入式面试笔试c语言知识点-程序员宅基地

文章浏览阅读560次。本该是不同编译器结果不同,但是尝试了g++ msvc都是先计算c,再计算b,最后得到a+b+c是经过赋值以后的b和c参与计算而不是6。由上表可知,将q复制到p数组可以表示为:*p++=*q++,*优先级高,先取到对应q数组的值,然后两个++都是在后面,该行运算完后执行++。在电脑端编译完后会分为text data bss三种,其中text为可执行程序,data为初始化过的ro+rw变量,bss为未初始化或初始化为0变量。_嵌入式面试笔试c语言知识点

57 Things I've Learned Founding 3 Tech Companies_mature-程序员宅基地

文章浏览阅读2.3k次。57 Things I've Learned Founding 3 Tech CompaniesJason Goldberg, Betashop | Oct. 29, 2010, 1:29 PMI’ve been founding andhelping run techn_mature

一个脚本搞定文件合并去重,大数据处理,可以合并几个G以上的文件_python 超大文本合并-程序员宅基地

文章浏览阅读1.9k次。问题:先讲下需求,有若干个文本文件(txt或者csv文件等),每行代表一条数据,现在希望能合并成 1 个文本文件,且需要去除重复行。分析:一向奉行简单原则,如无必要,绝不复杂。如果数据量不大,那么如下两条命令就可以搞定合并:cat a.txt >> new.txtcat b.txt >> new.txt……去重:cat new...._python 超大文本合并

支付宝小程序iOS端过渡页DFLoadingPageRootController分析_类似支付宝页面过度加载页-程序员宅基地

文章浏览阅读489次。这个过渡页是第一次打开小程序展示的,点击某个小程序前把手机的开发者->network link conditioner->enable & very bad network 就会在停在此页。比如《支付宝运动》这个小程序先看这个类的.h可以看到它继承于DTViewController点击左上角返回的方法- (void)back;#import "DTViewController.h"#import "APBaseLoadingV..._类似支付宝页面过度加载页

推荐文章

热门文章

相关标签