JVM 类加载器分类1. 前言我们之前对类加载子系统进行过简要的介绍,此处我们将会进行更加细致的讲解。本节主要知识点如下: 启动(Bootstrap)类加载器的作用及代码验证,为本节重点内容之一; 扩展(Extension)类加载器的作用及代码验证,为本节重点内容之一; 系统(System Application)类加载器的作用及代码验证,为本节重点内容之一。
通篇皆为重点内容,都是学习者需要重点掌握的。并且此节的内容也是后续内容的知识基础,为了更顺利的进行学习,次节内容需要重点掌握。 2. 类加载子系统知识回顾我们在JVM 总体架构的讲解过程中,提到过类加载子系统的工作流程分为三步:加载->链接->初始化。如下图所示: 本节我们所讨论的内容都是围绕第一步“加载(Loading)” 进行的。对于链接和初始化,我们会在后边的章节进行讲解。 我们将加载(Loading)这一步,再进行下细致的模块划分,如下图所示: 从上图中我们可看到,加载(loading)这一步,里边包含了三个更加细粒度的模块,分别为 BootStrap Class Loader,Extention Class Loader 和 Application Class Loader,这三个 Class Loader 就是我们加载过程中必须要使用到的三大类加载器。 3. 启动(Bootstrap)类加载器定义:启动(Bootstrap)类加载器也称为引导类加载器,该加载器是用本地代码实现的类加载器,它所加载的类库绝大多数都是出自 %JAVA_HOME%/lib 下面的核心类库,当然还有其他少部分所需类库。 由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。 Tips:从上述定义的描述中,我们可以看到一个特别需要关注的点:启动类加载器加载的绝对大多数是
%JAVA_HOME%/lib
下边的核心类库。这句话完完全全的体现出了启动(Bootstrap)类加载器存在的意义。对于其他少部分核心类的加载,我们在代码验证过程中来讲解。接下来,让我们通过示例代码进行下验证。
示例:通过编写一个 main 函数,打印出通过启动(Bootstrap)类加载器加载的所有的类库信息,以证实启动(Bootstrap)类加载器加载的是 %JAVA_HOME%/lib 下边的核心类库。 Tips:注意下 main 函数代码的第二行代码 URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs(); 这是通过 sun 公司提供的 Launcher 包获取 Bootstrap 类加载器下 ClassPath 下的所有的 URL。
import java.net.URL;
public class LoaderDemo { public static void main(String[] args) { System.out.println("BootstrapClassLoader 的加载路径: "); URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs(); for(URL url : urls) System.out.println(url); } }
结果验证:运行 main 函数。 Tips:此处运行结果所打印的类库的绝对路径为本人本机的安装路径,学习者应按照自己真实的JDK安装路径以及版本对号入座,此处仅为示例。
BootstrapClassLoader 的加载路径: file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/resources.jar file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/rt.jar file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/sunrsasign.jar file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/jsse.jar file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/jce.jar file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/charsets.jar file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/jfr.jar file:/D:/Programs/Java/jdk1.8.0_111/jre/classes
结果解析:我们可以看到,运行结果中的前 7
个类库(不同的JDK版本会有差异,此处我们讨论的是JDK 1.8版本),都是出自lib下的核心类库。但是对于最后一条加载信息却不是 lib
下的类库。我们仔细看下最后这条信息的加载 file:/D:/Programs/Java/jdk1.8.0_111/jre/classes。 这就是前文我们所提到的其他少部分的核心类库加载,学习者可以根据自己真实的安装位置打开
/jre 文件夹,看看是否存在 /classes 路径。结果是 /classes 文件夹路径并不存在,除非我们进行特殊的参数创建才可以出现
/classes 路径。此处并非我们主要讨论的问题,我们关注的是lib文件夹下的核心类库加载,这里仅做了解即可。 4. 扩展(Extension)类加载器定义:扩展类加载器是由
Sun 公司提供的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的,它负责将
%JAVA_HOME%/lib/ext 或者少数由系统变量 -Djava.ext.dir
指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。 Tips:此处我们依旧对大多数的核心类库加载位置进行讨论,即 %JAVA_HOME%/lib/ext 文件夹下的扩展核心类库。对于系统变量指定的类库,稍作了解即可。下边进行示例代码验证
示例: import java.net.URL; import java.net.URLClassLoader; public class LoaderDemo { public static void main(String[] args) { //取得扩展类加载器 URLClassLoader extClassLoader = (URLClassLoader)ClassLoader.getSystemClassLoader().getParent(); System.out.println(extClassLoader); System.out.println("扩展类加载器 的加载路径: "); URL[] urls = extClassLoader.getURLs(); for(URL url : urls) System.out.println(url); } }
结果验证:运行 main 函数。 扩展类加载器 的加载路径: file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/access-bridge-64.jar file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/cldrdata.jar file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/dnsns.jar file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/jaccess.jar file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/jfxrt.jar file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/localedata.jar file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/nashorn.jar file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/sunec.jar file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/sunjce_provider.jar file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/sunmscapi.jar file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/sunpkcs11.jar file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/zipfs.jar
结果解析:我们可以看到,运行结果中所有的核心类库均来自 %JAVA_HOME%/lib/ext 的文件夹。 5. 系统(System Application)类加载器定义:系统类加载器是由
Sun 公司提供的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的,它负责将
用户类路径(java
-classpath或-Djava.class.path变量所指的目录,即当前类所在路径及其引用的第三方类库的路径)下的类库加载到内存中。开发者可以直接使用系统类加载器。 Tips:系统(System Application)类加载器加载的核心类库类型比较多,也会加载 lib
下的未被 BootStrap 类加载器加载的类库,还会加载 ext 文件夹下的未被 Extension
类加载器加载的类库,以及其他类库。总而言之一句话,加载除了 BootStrap 类加载器和 Extension
类加载器所加载的其余的所有的核心类库。
示例: import java.net.URL; import java.net.URLClassLoader; public class LoaderDemo { public static void main(String[] args) { //取得应用(系统)类加载器 URLClassLoader appClassLoader = (URLClassLoader)ClassLoader.getSystemClassLoader(); System.out.println(appClassLoader); System.out.println("应用(系统)类加载器 的加载路径: "); URL[] urls = appClassLoader.getURLs(); for(URL url : urls) System.out.println(url); } }
结果验证:运行 main 函数。 应用(系统)类加载器 的加载路径: file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/charsets.jar file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/deploy.jar file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/access-bridge-64.jar file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/cldrdata.jar file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/dnsns.jar file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/jaccess.jar file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/jfxrt.jar file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/localedata.jar file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/nashorn.jar file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/sunec.jar file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/sunjce_provider.jar file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/sunmscapi.jar file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/sunpkcs11.jar file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/zipfs.jar file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/javaws.jar file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/jce.jar file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/jfr.jar file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/jfxswt.jar file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/jsse.jar file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/management-agent.jar file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/plugin.jar file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/resources.jar file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/rt.jar file:/E:/IdeaWorkspace/LeeCode/target/classes/ file:/D:/Programs/IntelliJ%20IDEA%20Educational%20Edition%202019.3.1/lib/idea_rt.jar
结果解析:我们可以看到, 系统(System Application)类加载器加载的类库种类很多,除了之前两种类加载器加载的类库,其余必须的核心类库,都由系统类加载器加载。 6. 小结对于类加载器中的第一步加载(Loading),我们主要讲解了 3 种类加载器。并且对不同的类加载器所加载的类库进行了讲解以及代码验证。通篇皆为重点知识,需要学习者用心学习。 对于加载(Loading)这一步,我们还未讲解完,下节课程会讲解加载(Loading)这一步所遵循的双亲委派模型,本节作为下一节的知识基础,更需要着重理解、掌握。 JVM 双亲委派模型1. 前言上节课程的小结部分提到,双亲委派模型是加载(Loading)步骤中所使用的模型,上节内容为本节内容的基础知识,学习本节内容之前,要确保已经掌握了上节所讲解的内容。本节主要知识点如下: 2. 双亲委派模型在讲解双亲委派模型之前,我们先来看看双亲委派模型的示意图,相信看到如下示意图中的模块信息,学习者会感到莫名的亲切感。示意图如下: 上图中的亲切感来自哪里呢?我们可以看到在双亲委派模型中,有三种类加载器是我们上节课程中所讲解的,有了上节课程的知识积淀,再理解双亲委派模型会非常的容易。 双亲委派模型原理: 如果对于原理性的描述还是比较模糊的话,不要着急,我们继续学习下边的内容,通过案例体会双亲委派模型的原理。 3. 案例 1:加载 /jre/lib/resources.jar通过上节课程的学习,我们能够知道 /jre/lib/resources.jar 是需要被启动(BootStrap)类加载器加载的核心类库,那么我们来看看它的加载流程图。 加载流程图:根据双亲委派模型,我们来看下 resources.jar 的完整加载过程。 从上图中我们可以看到,对于核心类库 resources.jar 的加载,分为以下 4 步: 步骤 1:resources.jar 会先通过自定义类加载器(前提是我们实现了自定义类加载器),自定义类加载器不会做处理,直接向上委托给系统(System Application)类加载器; 步骤 2:系统(System Application)类加载器接到委托后,也不做任何处理,直接向上委托给扩展(Extension)类加载器; 步骤 3:扩展(Extension)类加载器接到委托后,也不做任何处理,直接向上委托给启动(Bootstrap)类加载器; 步骤 4:启动(Bootstrap)类加载器接到委托后,发现 resources.jar 是自己负责加载的核心类库,于是进行加载,最后成功加载了 resources.jar。
4. 案例 2:加载 /jre/lib/ext/cldrdata.jar通过上节课程的学习,我们能够知道 /jre/lib/ext/cldrdata.jar 是需要被扩展(Extension)类加载器加载的核心类库,那么我们来看看它的加载流程图。 加载流程图:根据双亲委派模型,我们来看下 cldrdata.jar 的完整加载过程。 从上图中我们可以看到,对于核心类库 cldrdata.jar 的加载,分为以下 5 步: 步骤 1:cldrdata.jar 会先通过自定义类加载器(前提是我们实现了自定义类加载器),自定义类加载器不会做处理,直接向上委托给系统(System Application)类加载器; 步骤 2:系统(System Application)类加载器接到委托后,也不做任何处理,直接向上委托给扩展(Extension)类加载器; 步骤 3:扩展(Extension)类加载器接到委托后,也不做任何处理,直接向上委托给启动(Bootstrap)类加载器; 步骤 4:启动(Bootstrap)类加载器接到委托后,发现 cldrdata.jar 不是自己负责加载的核心类库,于是进行向下委派,委派给扩展(Extension)类加载器; 步骤 5:扩展(Extension)类加载器接到委派后,发现 cldrdata.jar 是自己负责加载的核心类库,于是进行加载,最后成功加载了 cldrdata.jar。
Tips:我们可以看到,在向上委托的过程中,cldrdata.jar 虽然在步骤 3
已经达到了扩展(Extension)类加载器,但是由于扩展(Extension)类加载器需要遵循向上委托的原则,必须要将
cldrdata.jar 向上委托给启动(Bootstrap)类加载器,直到启动(Bootstrap)类加载器向下委派 cldrdata.jar
到扩展(Extension)类加载器才进行类库的加载。
5. 案例 3:加载 /jre/lib/plugin.jar通过上节课程的学习,我们能够知道 /jre/lib/plugin.jar 是需要被系统(System Application)类加载器加载的核心类库,那么我们来看看它的加载流程图。 加载流程图:根据双亲委派模型,我们来看下 plugin.jar 的完整加载过程。 从上图中我们可以看到,对于核心类库 plugin.jar 的加载,分为以下 6 步: 步骤 1:plugin.jar 会先通过自定义类加载器(前提是我们实现了自定义类加载器),自定义类加载器不会做处理,直接向上委托给系统(System Application)类加载器; 步骤 2:系统(System Application)类加载器接到委托后,也不做任何处理,直接向上委托给扩展(Extension)类加载器; 步骤 3:扩展(Extension)类加载器接到委托后,也不做任何处理,直接向上委托给启动(Bootstrap)类加载器; 步骤 4:启动(Bootstrap)类加载器接到委托后,发现 plugin.jar 不是自己负责加载的核心类库,于是进行向下委派,委派给扩展(Extension)类加载器; 步骤 5:扩展(Extension)类加载器接到委派后,发现 plugin.jar 也不是自己负责加载的核心类库,于是进行向下委派,委派给系统(System Application)类加载器; 步骤 6:系统(System Application)类加载器接到委派后,发现 plugin.jar 是自己负责加载的核心类库,于是进行加载,最后成功加载了 plugin.jar。
Tips:类似于案例 2 的讲解,虽然 plugin.jar 是系统(System Application)类加载器负责加载的,但是要遵循向上委托的原则,因此在步骤 2 不能够实时加载,只能等待父加载器向下委派时加载。
6. 小结通过对双亲委派模型的讲解,我们了解到了双亲委派模型的定义以及原理,并通过 3 个案例详细的讲述了在双亲委派模型下是如何进行的类库的加载。 通篇皆为重点内容,所有的内容都是围绕双亲委派模型的原理展开的,需要学习者仔细品味案例,掌握双亲委派模型的原理。 JVM 中类加载的链接与初始化1. 前言对于类加载子系统,前边的课程已经对加载(Loading)这一步做了详细的讲解,本节主要对类加载子系统加载步骤中的链接与初始化进行讲解。本节主要知识点如下: 链接(Linking)步骤更加详细的模块划分:验证,准备和解析,为本节基础知识点; 掌握在链接(Linking)步骤中的第一步验证的详细验证内容,为本节重点内容之一; 掌握在链接(Linking)步骤中的第二步准备的准备内容,为本节重点内容之一; 掌握在链接(Linking)步骤中的第三步解析的具体解析内容,为本节重点内容之一; 掌握初始化(Init)步骤中的规则以及实例初始化顺序,为本节重点内容之一。
通篇皆为重点内容,本节知识也会为类加载子系统部分画上一个完美的句号,一定要认真对待。 2. 类加载子系统知识回顾我们在JVM 总体架构的讲解过程中,提到过类加载子系统的工作流程分为三步:加载->链接->初始化。如下图所示: 本节我们所讨论的内容都是围绕第二步“链接(Linking)” 和第三步“初始化(Init)”进行的。 我们将链接(Linking)这一步,再进行下细致的模块划分,如下图所示: 从上图中我们可看到,链接(Linking)这一步,里边包含了三个更加细致的步骤,分别为验证(verify),准备(prepare)和解析(resolve)。后文我们会对这三个步骤进行讲解。 3. 链接-验证(verify)定义:验证是连接阶段的第一步,这一阶段的目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并不会危害虚拟机的自身安全。 验证过程的主要验证信息:验证过程中,主要对三种类型的数据进行验证,分别是“元数据验证,字节码验证和符号引用验证”。具体内容请看下边的讲解。 元数据验证: 验证这个类是否有父类(除了 java.lang.Object 之外,所有类都应当有父类); 验证这个类是否继承了不允许被继承的类(被 final 修饰的类); 如果这个类不是抽象类,验证该类是否实现了其父类或接口之中所要求实现的所有方法; 验证类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的 final 字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等等)。
字节码验证:字节码验证主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。这个阶段将对类的方法体进行校验分析,保证被校验类的方法在运行时不会产生危害虚拟机安全的事件,例如: 符号引用验证:符号引用验证可以看作是类对自身以外(常量池中的各种符号引用)的信息进行匹配性校验,通常需要校验以下内容: 符号引用中通过字符串描述的全限定名是否能够找到对应的类; 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段; 符号引用中的类、字段、方法的访问性(private、default、protected、public)是否可被当前类访问。
4. 链接-准备(prepare)定义:准备阶段是正式为类变量分配内存并设置类变量默认值(通常情况下是数据类型的零值)的阶段,这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化的时候随着对象一起分配在Java堆中。 Tips:准备阶段是设置类变量的默认值,不同类型的类变量的默认值是不同的。变量默认值的对照表请参看下表:
变量类型 默认值 int 0 long 0L short 0 char '\u0000’ byte 0 boolean false folat 0.0f double 0.0d reference null 5. 链接-解析(resolve)定义:解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。 Tips:定义中又引出了两个新的概念:符号引用和直接引用。想要理解解析,必须要先搞明白什么是符号引用和直接引用。
符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。 直接引用(Direct References):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那么引用的目标一定是已经存在于内存中。 解析过程具体的解析内容:解析过程中,主要对如下4种类型的数据进行验证: 类或接口的解析; 字段解析; 类方法解析; 接口方法解析。
6. 初始化定义:进行准备阶段时,变量已经赋过一次系统要求的初始零值,而在初始化阶段,则会根据程序员通过程序编码制定的主观计划去初始化类变量和其他资源。 类的初始化阶段是类加载过程的最后一个步骤,之前介绍的几个类加载的动作里,除了在加载阶段用户应用程序可以通过自定义类加载器的方式局部参与外,其余动作都完全由Java虚拟机来主导控制。直到初始化阶段,Java虚拟机才真正开始执行类中编写的
Java 程序代码,将主导权移交给应用程序。 实例的初始化顺序:在进行初始化时,实例变量的初始化顺序如下图所示: 实例的初始化顺序是非常重要的知识点,在面试过程中也经常涉及到这个知识点,上图的加载顺序需要重点掌握。 7. 小结到目前为止,类加载器子系统就全部讲解完成了。我们学习了类的加载,三种类加载器,双亲委派模型以及本节所讲述的链接与初始化,其中对链接有细分了三个步骤进行了讲解。 类加载器子系统是非常重要的 JVM 模块,需要用心学习,对于一些概念性知识要增强理解,原理性知识要深入思索。后续我们会继续讲解 JVM 的其他重要模块。
|