加载机制是将Class文件读取到内存,并对数据进行处理,最终形成可以被JVM直接使用的Java类型的过程,主要分为加载,链接和初始化,其中链接又分为验证(Verify),准备(Prepare)和解析(Resolve)
加载 Loading
类或接口C的加载过程
- 由相应的名称获取对应的二进制字节流,可以从rar包中,网络中获取等
- 将字节流存储转换为方法区的数据格式
- 最后实例化一个代表该类的Class对象
public static final java.lang.Class XXX
,通过XXX.class
可以访问到
数组类没有外部二进制表示,它是由Java虚拟机直接创建的,而不是由类加载器创建的,其他的类型都是使用类加载器加载类或接口的C的二进制表示形式
加载器
通过类的全限定名获取该类的二进制字节流
有两种类加载器:Java虚拟机提供的引导类加载器和用户自定义类加载器。
每个用户定义的类加载器都要继承抽象类ClassLoader
。自定义的类加载器可以加载来自网络,加密文件等的二进制字节流。
在Java中,一个类是由其完全限定名来标识的(包名和类名组成)。 但是一个类在JVM中的唯一标识符号,是完全限定的类名以及加载该类的ClassLoader的实例组成的,如果类加载器不同,即使是类型相同,但是Class
类的以下方法返回为false
isInstance(Object obj)// obj是对应的Class类型,等价于 obj instanceof Class
isAssignableFrom(Class<?> cls) // 对应类可以被参数类型赋值,是参数类本身或者超类
三种系统提供的常用类加载器:
- 启动类加载器
BootClassLoader
:是虚拟机的一部分,唯一使用C++实现的加载器,负责加载$JAVA_HOME/lib
目录中的rt.jar
等核心类,以及-Xbootclasspath
指定的路径中的jar/zip归档文件 - 扩展类加载器 Extensions Class Loader:加载
$JAVA_HOME/lib/ext
,以及java.ext.dirs
属性指定路径汇中的所有类库,具体实现sun.misc.Launcher$ExtClassLoader
- 应用程序类加载器
AppClassLoader
:也叫系统类加载器,加载java.class.path
属性或者说是-cp / -classpath
命令参数,指定路径的的所有类库,具体实现sun.misc.Launcher$AppClassLoader
,如果没有自定义类加载器,这个就是程序中默认的类加载器
ClassLoader
的loadClass
函数流程:
// 检查类是否已经被加载
Class<?> c = findLoadedClass(name);
//如果没有被加载,调用父加载器
if (c == null) {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
//如果还没有找到,调用findClass自己查找
if (c == null)
c = findClass(name);
}
类加载器的层次关系称为委派模型,只加载那些在parent中无法加载到的类
线程上下文类加载器,默认继承父线程的上下文加载器,最初原始线程对应的是应用类加载器
Thread.currentThread().getContextClassLoader()
破坏双亲委派模型
- 覆盖掉
loadClass
方法,不让它先去父类检验,而改为直接调用findClass
方法去加载字节的类 - 加载SPI实现类
JDK中提供了一个ServiceLoader<T>
的一个服务加载器来实现服务发现,扫描META-INF/services/service-name
文件,读取其中实现服务的具体类名称
这里需要JDK定义的ServiceLoader
类需要去加载对应的服务提供类的时候,双亲委派就无法实现了
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
这里采用的就是将加载任务交给线程上下文加载器进行加载
- OSGi动态模块系统
OSGi 中的每个模块(bundle
)都包含 Java包和类,模块可以声明它所依赖的需要导入的其它模块的类(通过 Import-Package),也可以声明导出自己的包和类,供其它模块使用(通过 Export-Package),也就是说需要能够隐藏和共享一个模块中的某些 Java 包和类
OSGi 中的每个模块都有对应的一个类加载器,负责加载模块自己包含的类,整体是一个网状结构,当它需要加载 Java 核心库的类时(以java
开头),它会代理给父类加载器(通常是启动类加载器)来完成,也可以通过org.osgi.framework.bootdelegation
显式声明某些 Java 包和类,必须由父类加载器来加载。当它需要加载所导入的 Java 类时,它会代理给导出此 Java 类的模块来完成加载
当需要更换一个bundle时,就把Bundle连同类加载器一起换掉实现代码的热替换
链接 Linking
链接包括验证(Verify),准备(Prepare)类或接口,以及对应的父类,父接口,元素类型(数组类型时),解析(Resolution)符号引用是链接部分的可选操作
验证确保接口或类的二进制表示的结构正确,对于主版本号大于50的Class文件,使用类型检查(Type Checking,和Code属性表的StackMapTable
属性相关)规则进行验证,之前采用的是类型推断(Type Inference)
准备阶段涉及创建静态字段并初始化为默认值(null,false,0等),在初始化阶段才会进行静态字段的显式初始化,如果字段属性表中存在ConstantValue
属性,那在准备阶段就初始化为对应的值
解析是将运行时常量池的符号引用,动态替换为具体值的过程,除了invokedynamic
指令,其他指令解析一次可以重复使用。解析动作主要针对类或接口,字段,方法,接口方法,方法类型和方法句柄,调用点限定符7类的符号引用进行。
初始化 Initialization
初始化阶段执行类或接口的<clinit>函数
接口或者类C进行初始化的触发条件:
- 执行new,getstatic, putstatic, 或者 invokestatic四条 JVM 指令中至少一条
- 调用
java.lang.invoke.MethodHandle
,动态语言支持相关 - 调用反射方法,例如
Class
类和java.lang.reflect
包内的方法 - 初始化C的子类
- 接口C含有非静态非抽象的方法(默认方法)时,初始化实现接口的类,对应接口C进行初始化
- C类是虚拟机启动时要执行的主类(main函数对应的类)
通过子类引用父类的静态字段,不会导致子类初始化
通过数组定义来引用类,不会触发此类的初始化
常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化
<init> :实例初始化方法,名字是一个无效的标识符,程序不能提供,只能由编译器提供
<clinit>:类或接口的初始化方法,最多有一个该方法,参数为空。该方法是类中所有静态变量和静态块语句合并产生的,先执行父类的<clinit>方法,同时在执行该方法的时候会上锁,多个线程初始化时,只有一个线程执行
网友评论