一、类加载器的组织结构
1、Bootstrap ClassLoader
:根类(或叫启动、引导类加载器)加载器
它负责加载Java的核心类(如String、System等)。
它比较特殊,因为它是由`原生C++`代码实现的,并不是java.lang.ClassLoader的子类,
所以下面的输出结果为null:
System.out.println(String.class.getClassLoader());
2、Extension ClassLoader
:扩展类加载器。
它负责加载JRE的`扩展目录(%JAVA_HOME%/jre/lib/ext)`中JAR包的类,我们可以通过把自己开发的类打包成JAR文件
放入扩展目录来为Java扩展核心类以外的新功能。
3、System ClassLoader(或Application ClassLoader)
:系统类加载器
它负责在JVM启动时加载来自Java命令的`-classpath选项`、`java.class.path`系统属性,
或`CLASSPATH环境变量`所指定的JAR包和类路径。
程序可以通过ClassLoader的静态方法getSystemClassLoader来获取系统类加载器:
4、类加载机制
JVM的类加载机制主要有以下3种
`全盘负责`:
当一个类加载器加载某个Class时,该Class所依赖和引用的其它Class也将由该类加载器负责载入,除非显式的使用另外一个类加载器来载入。
`双亲委派`:
当一个类加载器收到了类加载请求,它会把这个请求委派给父(parent)类加载器去完成,依次递归,
因此所有的加载请求最终都被传送到顶层的启动类加载器中。
只有在父类加载器无法加载该类时子类才尝试从自己类的路径中加载该类。
(注意:类加载器中的父子关系并不是类继承上的父子关系,而是类加载器实例之间的关系。)
`缓存机制`:
缓存机制会保证所有加载过的Class都会被缓存,当程序中需要使用某个类时,类加载器先从缓冲区中搜寻该类,
若搜寻不到将读取该类的二进制数据,并转换成Class对象存入缓冲区中。这就是为什么修改了Class后需重启JVM才能生效的原因。
4、补充案例(类加载器)
Test loader = new Test();
System.out.println("A:"+loader.getClass().getClassLoader()); // AppClassLoader
System.out.println("B:"+loader.getClass().getClassLoader().getParent()); // ExtClassLoader
System.out.println("C:"+loader.getClass().getClassLoader().getParent().getParent()); // null(Bootstrap ClassLoader)
System.out.println("=======================================================");
`执行结果:`
A:sun.misc.Launcher$AppClassLoader@18b4aac2
B:sun.misc.Launcher$ExtClassLoader@6d86b085
C:null
=======================================================
5、Java9的改变
JDK 9保持三级分层类加载器架构以实现向后兼容。但是,从模块系统加载类的方式有一些变化。
且新增Platform ClassLoader:平台类加载器,用于加载一些平台相关的模块,
例如: java.activation 、 java.se 、 jdk.desktop 、 java.compiler 等,双亲是BootClassLoader。
JDK 9类加载器层次结构如下图所示:
可见:
在JDK 9中,应用程序类加载器可以委托给平台类加载器以及引导类加载器;平台类加载器可以委托给引导类加载器和应用程序类加载器。
此外,JDK 9不再支持扩展机制。 但是,它将扩展类加载器保留在名为平台类加载器的新名称下。
ClassLoader类包含一个名为getPlatformClassLoader()的静态方法,该方法返回对平台类加载器的引用。
在JDK 9之前,扩展类加载器和应用程序类加载器都是`java.net.URLClassLoader`类的一个实例。
而在JDK 9中,平台类加载器(以前的扩展类加载器)和应用程序类加载器是内部JDK类的实例。
如果你的代码依赖于URLClassLoader类的特定方法,代码可能会在JDK 9中崩溃。
JDK 9中的类加载机制有所改变。 三个内置的类加载器一起协作来加载类。
当应用程序类加载器需要加载类时,它将搜索定义到所有类加载器的模块。
如果有合适的模块定义在这些类加载器中,则该类加载器将加载类,这意味着应用程序类加载器现在可以委托给引导类加载器和平台类加载器。
如果在为这些类加载器定义的命名模块中找不到类,则应用程序类加载器将委托给其父类,即平台类加载器。
如果类尚未加载,则应用程序类加载器将搜索类路径。
如果它在类路径中找到类,它将作为其未命名模块的成员加载该类。
如果在类路径中找不到类,则抛出ClassNotFoundException异常。
当平台类加载器需要加载类时,它将搜索定义到所有类加载器的模块。
如果一个合适的模块被定义为这些类加载器中,则该类加载器加载该类。 这意味着平台类加载器可以委托给引导类加载器以及应用程序类加载器。
如果在为这些类加载器定义的命名模块中找不到一个类,那么平台类加载器将委托给它的父类,即引导类加载器。
当引导类加载器需要加载一个类时,它会搜索自己的命名模块列表。
如果找不到类,它将通过命令行选项-Xbootclasspath/a指定的文件和目录列表进行搜索。
如果它在引导类路径上找到一个类,它将作为其未命名模块的成员加载该类。
二、类的具体加载机制
类被加载到虚拟机内存包括`加载`、`链接`、`初始化`几个阶段。其中`链接`又细化分为`验证`、`准备`、`解析`。
这里需要注意的是,'解析阶段在某些情况下可以在初始化阶段之后再开始',这是为了支持Java的'运行时绑定'。各个阶段的作用整理如下
2.1 加载
简单来说,加载指的是把class字节码文件从各个来源通过类加载器装载入内存中。
这里有两个重点:
`字节码来源`。
一般的加载来源包括从本地路径下编译生成的.class文件,从jar包中的.class文件,从远程网络,以及动态代理实时编译
`类加载器`。
一般包括启动类加载器,扩展类加载器,应用类加载器,以及用户的自定义类加载器。
'注:为什么会有自定义类加载器?'
`一方面:`由于java代码很容易被反编译,如果需要对自己的代码加密的话,可以对编译后的代码进行加密,
然后再通过实现自己的自定义类加载器进行解密,最后再加载。
`另一方面:`有可能从非标准的来源加载代码,比如从网络来源,那就需要自己实现一个类加载器,从指定源进行加载。
2.2 链接阶段
<1> 验证
主要是为了保证加载进来的字节流符合虚拟机规范,不会造成安全错误。
`包括:`
'对于文件格式的验证:'
比如常量中是否有不被支持的常量?文件中是否有不规范的或者附加的其他信息?
'对于元数据的验证:'
比如该类是否继承了被final修饰的类?类中的字段,方法是否与父类冲突?是否出现了不合理的重载?
'对于字节码的验证:'
保证程序语义的合理性,比如要保证类型转换的合理性。如是否存在父类对象赋值给子类数据类型?
'对于符号引用的验证:'
比如校验符号引用中通过全限定名是否能够找到对应的类?校验符号引用中的访问性(private,public等)是否可被当前类访问?
<2> 准备
准备阶段为方法区中的'静态变量分配内存空间'。并将其赋值为初始值,所有原始类型的值都为0。
如float为0f、 int为0、boolean为0、引用类型为null。
<3> 解析
将常量池内的符号引用替换为直接引用的过程。
两个重点:
`符号引用:`
即一个字符串,但是这个字符串给出了一些能够唯一性识别一个方法,一个变量,一个类的相关信息。
`直接引用:`
可以理解为一个内存地址,或者一个偏移量。比如类方法,类变量的直接引用是指向方法区的指针;
而实例方法,实例变量的直接引用则是从实例的头指针开始算起到这个实例变量位置的偏移量
比如一个实例方法,子类中方法表中的偏移量和父类是一致的,这个偏移量可以确定某个方法的位置。
举个例子来说,现在调用方法hello(),这个方法的地址是1234567,那么hello就是符号引用,1234567就是直接引用。
在解析阶段,虚拟机会把所有的类名,方法名,字段名这些符号引用替换为具体的内存地址或偏移量,也就是直接引用。
2.3 初始化阶段
这个阶段主要是对类变量初始化,是`执行类构造器`的过程。
换句话说,只对static修饰的变量或语句进行初始化。
-- 如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。
-- 如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。
初始化过程会`被触发`的条件汇总:
(1)使用new关键字实例化对象、访问一个类的静态字段、静态方法的时候。
(2)对类进行反射调用的时候。
(3)当初始化子类时,如果发现其父类还没有进行过初始化,则进行父类的初始化。
三、类加载的三种方式
(1)由 new 关键字创建一个类的实例。
(2)调用 Class.forName() 方法,通过反射加载类。
(3)调用某个ClassLoader实例的loadClass()方法。
三者的区别汇总如下:
(1)方法1和2都是使用的当前类加载器。方法3是由用户指定的类加载器加载。
(2)方法1是静态加载,2、3是动态加载。
(3)对于两种动态加载,如果程序需要类被初始化,就必须使用Class.forName(name)的方式。
参考文章:
https://blog.youkuaiyun.com/seu_calvin/article/details/52301541
https://blog.youkuaiyun.com/CNAHYZ/article/details/82219210#%E7%B1%BB%E5%8A%A0%E8%BD%BD%E5%99%A8