白话说Java虚拟机原理系列【第三章】:类加载器详解
文章目录
- jvm.dll
- BootstrapLoader:装载系统类
- ExtClassLoader:装载扩展类
- AppClassLoader:装载自定义类
- 双亲委派模型
- 类加载器加载类的方式
- 类加载器特性
- 类加载器加载字节码到JVM的过程
- 自定义/第三方类加载器
- `类加载器加载字节码到哪?`
- Class类对象
- 破坏双亲委派机制:spi机制
前导说明:
本文基于《深入理解Java虚拟机》第二版
和个人理解完成,
以大白话的形式期望能用大家都看懂的描述讲清楚虚拟机内幕,
后续会增加基于《深入理解Java虚拟机》第三版
内容,并进行二个版本的对比
。
jvm.dll
首次要执行JAVA程序就都需要启动JVM虚拟机,由java.exe负责获取JRE目录位置中的jvm.dll动态链接库,然后加载运行此库,即JVM虚拟机。
BootstrapLoader:装载系统类
- JVM启动初始化后,第一创建的类加载器为BootstrapLoader,此由C++语言实现,加载运行后它会加载Launcher.class字节码文件,Launcher内部有ExtClassLoader、AppClassLoader两个内部类,它会先创建ExtClassLoader并将它的parent设置成null,然后再创建AppClassLoader,并将它的parent设置成ExtClassLoader。至此完成了BootstrapLoader的初始化,同时也完成了类加载器的初始化。
- BootstrapLoader加载器负责加载JAVA安装目录中的类,即核心JAVA API对应的类。如JRE目录下的rt.jar、charsets.jar等,BootstrapLoader由C++编写实现,由于不是JAVA体系,所以JVM选择了透明化该类,所以ExtClassLoader的parent设置成了null,其实代表的就是ExtClassLoader的parent为BootstrapLoader,只不过透明化了这个C++实现的类。
ExtClassLoader:装载扩展类
负责加载JRE安装目录的"\lib\ext"目录下的类。
AppClassLoader:装载自定义类
- 负责加载自己开发的类,就是环境变量中配置的"."点对应的目录了。其实是java.class.path对应的目录,即CLASSPATH目录
- 默认情况下使用AppClassLoader装载应用程序中的类。
双亲委派模型
问题:有这么写类加载器,实际中我们要用哪个最合理呢?
这就要看一下他的运行机制:双亲委派机制
原理:就是如果有类加载的需求时,加载器先通过parent去尝试在对应的路径下加载,如果parent得不到类文件,再由子加载器去加载,直到加载到文件。比如一个自定义的类,加载时,先由BootstrapLoader加载,加载不到再由ExtClassLoader加载,加载不到最后才由AppClassLoader进行加载。这就是委托模型机制,分工明确,各司其职。
原因:出于安全、保护JVM考虑,比如我自己实现了一个较差的String类,如果没有此机制,那么就可以随意加载到JVM,那可能导致一些问题,有了此机制,那么String永远都是有BootstrapLoader加载,且加载的是JAVA提供的核心类库中的类。
证明:要想证明累的加载过程是不是对,只需建一个main方法,运行一下,只不过增加一个java的启动参数:设置虚拟机参数"-XX:+TraceClassLoading"来获取类加载信息,信息将会打印在控制台或log
。
类加载器加载类的方式
- 隐式装载:程序运行过程中碰到new关键字,则会先装载对应的类。
- 显示装载:通过Class.forName()等类似方法显示的调用装载。
类加载途径:
虚拟机对类加载的规范很模糊,即最终加载成二进制流,但是二进制流从哪来没有规定,所以可以有如下途径获取类的字节码文件:
- ①从zip包中获取,这就是以后jar、ear、war格式的基础
- ②从网络中获取,典型应用就是Applet
- ③运行时计算生成,典型应用就是动态代理技术
- ④由其他文件生成,典型应用就是JSP,即由JSP生成对应的.class文件
- ⑤从数据库中读取,这种场景比较少见
类加载器特性
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这句话表达地再简单一点就是:比较两个类是否"相等",只有在这两个类是由同一个类加载器加载的前提下才有意义,否则即使这两个类来源于同一个.class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,这两个类必定不相等。
类加载器加载字节码到JVM的过程
此处我们忽略“使用”和“卸载”两个过程,”使用“就是我们代码中实现的功能,卸载将会交由GC处理。
1.装载:查找和导入class字节码文件到运行时数据区的方法区。
2.链接:检查:检查载入的class文件数据的正确性。
准备:给类的静态变量分配存储空间,存于方法区。此过程还会对(虚)方发表完成初始化。
解析:将符号引用转成直接引用,常量池解析。3.初始化:对静态变量、静态代码块(即静态方法)执行初始化工作。
最后通过加载器创建Class类对象存于堆,并指向方法区该类的存储区域。
自定义/第三方类加载器
类加载器可以由第三方自己实现一些特殊的功能。
- ClassLoader抽象类:类加载由此开始,会调用它的loadClass方法,查看源码可以发现双亲委派的具体实现,以及各个类加载器加载的目录对应的环境变量具体是哪个。
- 自定义加载器:如果我们要保留双亲委派机制,那么需要继承ClassLoader类并实现findClass方法(从源码可知这个方法默认是空实现,为自定义准备的扩展方法)来加载jvm默认加载器处理范围之外的类,如果我们想更广范围的加载类,比如不让AppClassLoader生效,那么只能自己重写ClassLoader类的loadClass方法了。
类加载器加载字节码到哪?
类加载器加载字节码文件到运行时数据区的
方法区
中,然后再完成一系列字节码文件的解析(常量池解析等),最后会创建一个与类文件相关的对象存入堆(即类对象)并通过指针指向方法区当前类的定义(指针指的是堆中的对象头中会有一个引用指向方法区类字节码定义的位置,方法区章节会详细讲解,此处知道即可
)。
Class类对象
- 类加载器加载class文件后,最终都会创建一个Class类的对象存于堆中,用于和方法区中的class文件内容关联。
- Class类没有public的构造方法,Class类的对象是在装载类时由JVM通过调用类装载器中的defineClass()方法自动构建的。
- 方法区中也会保存加载当前class文件的加载器信息,因为Class类的对象是由加载器创建的,所以通过Class类对象可以得知用的哪个加载器。
破坏双亲委派机制:spi机制
上边说了,双亲委派的作用就是安全,保障jdk的类只有自己的加载器能加载,但是有些时候我们需要让我们自己提供的类被加载,该怎么办呢?
几种方法: 比如
- 就是重写源码,这个不太可取
就是重写类加载器的loadClass方法,以此改变类加载的方式;- 通过jdk提供的spi方式,我们提供自己的实现类,这样通过spi的配置,就可以成功通过双亲委派来加载到我们提供的类了。
简单介绍SPI: spi的方法就是定义自己的实现类,比如数据库驱动Driver接口的实现类,然后将实现类放到META-INF/services目录的Driver全名的配置文件中,这样就可以了,当然使用的时候要用ServiceLoader来加载具体的实现类(spi内容可以到dubbo章节的讲解中学习)。