本文将由浅及深,介绍Java
类加载的过程和原理,进一步对类加载器的进行源码分析,完成一个自定义的类加载器。java
类加载器简言之,就是用于把.class
文件中的字节码信息转化为具体的java.lang.Class
对象的过程的工具。编程
具体过程:后端
JVM
会将全部的.class
字节码文件中的二进制数据读入内存中,导入运行时数据区的方法区中。Class
对象,Class
对象封装了类在方法区内的数据结构。Class
对象的建立过程描述:数组
类加载的过程分为三个步骤(五个阶段) :加载 -> 链接(验证、准备、解析)-> 初始化。缓存
加载、验证、准备和初始化这四个阶段发生的顺序是肯定的,而解析阶段能够在初始化阶段以后发生,也称为动态绑定或晚期绑定。数据结构
加载:查找并加载类的二进制数据的过程。架构
.class
文件,并获取其二进制字节流。Java
堆中生成一个此类的java.lang.Class
对象,做为方法区中这些数据的访问入口。链接:包括验证、准备、解析三步。框架
验证:确保被加载的类的正确性。验证是链接阶段的第一步,用于确保Class
字节流中的信息是否符合虚拟机的要求。异步
Class
文件格式的规范;例如:是否以0xCAFEBABE
开头、主次版本号是否在当前虚拟机的处理范围以内、常量池中的常量是否有不被支持的类型。javac
编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object
以外。准备:为类的静态变量分配内存,并将其初始化为默认值。准备过程一般分配一个结构用来存储类信息,这个结构中包含了类中定义的成员变量,方法和接口信息等。
static
),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java
堆中。0
、0L
、null
、false
等),而不是被在Java
代码中被显式赋值。解析:把类中对常量池内的符号引用转换为直接引用。
解析动做主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符等7类符号引用进行。
初始化:对类静态变量赋予正确的初始值 (注意和链接时的解析过程区分开)。
new
关键字);java.lang.reflect
包中的方法(如:Class.forName(“xxx”)
);main
方法(如:SpringBoot
入口类)。主动引用:在类加载阶段,只执行加载、链接操做,不执行初始化操做。
new
关键字);java.lang.reflect
包中的方法(如:Class.forName(“xxx”)
);main
方法(如:SpringBoot
入口类)。代码示例:
1 |
public class OptimisticReference0 { |
运行结果:
OptimisticReference0 is referred!
代码示例:
1 |
public class OptimisticReference1 { |
运行结果:
Parent is referred!
Child is referred!
代码示例:
1 |
public class OptimisticReference2 { |
运行结果:
Child is referred!
Child
代码示例:
1 |
public class OptimisticReference3 { |
运行结果:
Child is referred!
代码示例:
1 |
public class OptimisticReference4 { |
运行结果:
Child is referred!
被动引用: 在类加载阶段,会执行加载、链接和初始化操做。
被动引用的几种形式:
代码示例:
1 |
public class NegativeReference0 { |
运行结果:
Parent is referred!
Parent
代码示例:
1 |
public class NegativeReference1 { |
运行结果:
无输出
示例代码:
1 |
public class NegativeReference2 { |
运行结果:
Child
类加载器:类加载器负责加载程序中的类型(类和接口),并赋予惟一的名字予以标识。
Bootstrap Classloader
是在Java
虚拟机启动后初始化的。Bootstrap Classloader
负责加载 ExtClassLoader
,而且将 ExtClassLoader
的父加载器设置为 Bootstrap Classloader
Bootstrap Classloader
加载完 ExtClassLoader
后,就会加载 AppClassLoader
,而且将 AppClassLoader
的父加载器指定为 ExtClassLoader
。Class Loader | 实现方式 | 具体实现类 | 负责加载的目标 |
---|---|---|---|
Bootstrap Loader | C++ | 由C++实现 | %JAVA_HOME%/jre/lib/rt.jar 以及-Xbootclasspath 参数指定的路径以及中的类库 |
Extension ClassLoader | Java | sun.misc.Launcher$ExtClassLoader | %JAVA_HOME%/jre/lib/ext 路径下以及java.ext.dirs 系统变量指定的路径中类库 |
Application ClassLoader | Java | sun.misc.Launcher$AppClassLoader | Classpath 以及-classpath 、-cp 指定目录所指定的位置的类或者是jar 文档,它也是Java 程序默认的类加载器 |
每一个类装载器都有一个本身的命名空间用来保存已装载的类。当一个类装载器装载一个类时,它会经过保存在命名空间里的类全局限定名(Fully Qualified Class Name
) 进行搜索来检测这个类是否已经被加载了。
JVM
及 Dalvik
对类惟一的识别是 ClassLoader id
+ PackageName
+ ClassName
,因此一个运行程序中是有可能存在两个包名和类名彻底一致的类的。而且若是这两个类不是由一个 ClassLoader
加载,是没法将一个类的实例强转为另一个类的,这就是 ClassLoader
隔离性。
为了解决类加载器的隔离问题,JVM
引入了双亲委托机制。
核心思想:其一,自底向上检查类是否已加载;其二,自顶向下尝试加载类。
AppClassLoader
加载一个class
时,它首先不会本身去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader
去完成。ExtClassLoader
加载一个class
时,它首先也不会本身去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader
去完成。BootStrapClassLoader
加载失败(例如在%JAVA_HOME%/jre/lib
里未查找到该class
),会使用ExtClassLoader
来尝试加载;ExtClassLoader
也加载失败,则会使用AppClassLoader
来加载,若是AppClassLoader
也加载失败,则会报出异常ClassNotFoundException
。ClassLoader.class
java.lang.Class
对象。
ClassLoader
经过loadClass()
方法实现了双亲委托机制,用于类的动态加载。
loadClass()
自己是一个递归向上调用的过程。
自底向上检查类是否已加载
findLoadedClass()
方法从最底端类加载器开始检查类是否已经加载。resolve
参数决定是否要执行链接过程,并返回Class
对象。parent.loadClass()
委托其父类加载器执行相同的检查操做(默认不作链接处理)。parent
为空时,由findBootstrapClassOrNull()
方法尝试到Bootstrap ClassLoader
中检查目标类。自顶向下尝试加载类
Bootstrap ClassLoader
开始,经过findClass()
方法尝试到对应的类目录下去加载目标类。resolve
参数决定是否要执行链接过程,并返回Class
对象。ClassNotFoundException
。查找最顶端
Bootstrap
类加载器的是否已经加载目标类。一样,findBootstrapClassOrNull()
实际调用了底层的native
方法findBootstrapClass()
。
ClassLoader
是java.lang
包下的抽象类,也是全部类加载器(除了Bootstrap
)的基类,findClass()
是ClassLoader
对子类提供的加载目标类的抽象方法。注意:
Bootstrap ClassLoader
并不属于JVM
的层次,它不遵照ClassLoader
的加载规则,Bootstrap classLoader
并无子类。
JVM
初始化加载;Class.forName()
方法动态加载;ClassLoader.loadClass()
方法动态加载。.class
文件加载到JVM
中,对类进行解释的同时执行类中的static
静态代码块;JVM
中,不会执行static
代码块中的内容,只有在newInstance
才会去执行。静态变量/静态代码块 -> 普通代码块 -> 构造函数
测试结果代表:JVM
在建立对象时,遵照以上对象的初始化顺序。
在源码分析阶段,咱们已经解读了如何实现自定义类加载器,如今咱们开始怼本身的类加载器。
Step 1:定义待加载的目标类
Parent.java
和Children.java
。
Parent.java
1 |
package org.ostenant.jdk8.learning.examples.classloader.custom; |
Children.java
1 |
package org.ostenant.jdk8.learning.examples.classloader.custom; |
Step 2:实现自定义类加载器
CustomClassLoader
CustomClassLoader.java
1 |
public class CustomClassLoader extends ClassLoader { |
Step 3:测试类加载器的加载过程
CustomerClassLoaderTester.java
测试程序启动时,逐一拷贝并加载待加载的目标类源文件。
1 |
private static final String CHILDREN_SOURCE_CODE_NAME = SOURCE_CODE_LOCATION + "Children.java"; |
拷贝单一源文件到自定义类加载器的类加载目录。
1 |
protected static File copySourceFile(File f) { |
对拷贝后的.java
源文件执行手动编译,在同级目录下生成.class
文件。
1 |
protected static void compileSourceFile(File f) { |
经过自定义类加载器加载Children
的java.lang.Class<?>
对象,而后用反射机制建立Children
的实例对象。
1 |
|
static
代码块,把目标类Children.java
和Parent.java
拷贝到类加载的目录,而后进行手动编译。Children.java
和Parent.java
。测试结果分析:
咱们成功建立了
Children
对象,并经过反射调用了它的say()
方法。
然而查看控制台日志,能够发现类加载使用的仍然是AppClassLoader
,CustomClassLoader
并无生效。
类目录下有咱们拷贝并编译的
Parent
和Chidren
文件。
分析缘由:
因为项目空间中的
Parent.java
和Children.java
,在拷贝后并无移除。致使AppClassLoader
优先在其Classpath
下面找到并成功加载了目标类。
咱们成功经过自定义类加载器加载了目标类。建立了
Children
对象,并经过反射调用了它的say()
方法。
至此,咱们本身的一个简单的类加载器就完成了!
周志明,深刻理解Java虚拟机:JVM高级特性与最佳实践,机械工业出版社
欢迎关注技术公众号: 零壹技术栈
本账号将持续分享后端技术干货,包括虚拟机基础,多线程编程,高性能框架,异步、缓存和消息中间件,分布式和微服务,架构学习和进阶等学习资料和文章。