目录
目标
详细介绍类加载器的3个阶段。
what
JVM 动态的loading加载 linking链接 initializing初始化 class and interfacese,其不是一次串行执行,而是分阶段触发的。
JVM启动过程概述
通过使用bootstrap class loader 加载和创建 初始化类initial class 的方式启动JVM,之后JVM链接并初始化这个类,并调用该类的public static void main方法。main方法将驱动后续的执行,执行main方法中的JVM指令,将引起JVM去加载链接初始化其他的类和接口。
loading and creation阶段
作用
加载与创建,输出Class object
概述
JVM通过[类或接口的全路径名称]+[定义类加载器]找到对应的字节码表现形式(.class),然后JVM使用字节码来创建classes和interfaces
前置知识
类或接口被加载的原因
类或接口C的创建,是其他的类或接口D触发的,即D在运行时使用C时。
- 运行时,D的code使用jvm指令直接调用常量池中有C的符号引用,引起D去解析C,
- 运行时,D通过JavaSE的反射机制加载C。
类/接口/数组是由谁创建的
- 类和接口是使用class loader加载其字节码进行加载创建的;
- 数组是JVM创建,不是加载器。
类加载器分两种
- 由JVM提供的bootstrap类加载器
- 用户定义的类加载器
- 继承抽象类ClassLoader
- 用于扩展JVM的动态加载;用于从特定的资源(如特定的package,network,加密文件等)中加载类;
定义类加载器与初始化类加载器
- 定义类加载器
- 完成class C加载和定义的类加载器L,则L是C的定义类加载器
- 初始化类加载器
- 参与class C的加载过程的所有类加载器,都称为C的初始化类加载器。
运行时,如何唯一确定类和接口
(类或接口C的全路径名N,定义类加载器L)
加载和创建全路径N关联的类或接口C的原则
C不是数组
使用用户定义类加载器进行类加载
- JVM判断L是否被记录为[由全路径N表示的类或接口C]的初始化类加载器,如果是则直接返回C;
- 否则JVM将调用L的loadClass方法,如果加载成功,JVM会记录L为C的初始化类加载器。
- 举例,loadClass默认实现(双亲委派)
- 查询加载记录(findLoadedClass(name)),如果找到则直接返回C
- 如果不是,则L委派给parent类加载器或bootstrap类加载器,如果成功加载C,则返回C;
- 如果未加载到C,则L将在其负责的资源范围内进行加载,如果成功加载,则返回C;
- 如果未加载,则抛出ClassNotFoundException。
使用bootstrap类加载器进行类加载
- JVM判断L是否被记录为[由全路径N表示的类或接口C]的初始化类加载器,如果是则直接返回C;
- 如果不是,则JVM调用bootstrap类加载器的方法,在OS的文件系统中搜索C的表现形式。找不到则抛出ClassNotFoundException。
- JVM通过找到的表现形式生成类或接口C
C是数组
由JVM直接创建数组class;在创建数组的过程中,数组成员类型T的定义类加载器将加载创建T。
- JVM判断L是否为[成员类型为T的数组class C]的初始化类加载器,如果是则直接返回C
- 如果成员类型T为引用类型,则使用L来加载T
- JVM将依据[成员类型T]和成员数量N来创建array class
- JVM记录L为成员类型T的初始化类加载器
class file如何派生出Class
- JVM判断L是否已经被记录为[由N表示的C]的初始化类加载器,如果是,则这个创建操作是非法的,抛出LinkedError
- JVM尝试解析C的class file表现形式
- 如果不是class file结构,则抛出ClassFormatError
- 如果class file的版本不在支持的范围内,则抛出UnSupportedClassVersionError
- 如果表现形式并不能代表[N表示的C],则抛出NotClassDefFoundError
- 如果C具有直接父类superclass,则通过superclass的符号引用去load和create父类;
- 如果C具有superinterfaces,则通过符号引用去load和create superinterface
- JVM标记C的定义类加载器为L,并记录L是C的初始化类加载器。
loadClass过程图示
Linking 链接
概述
JVM通过[验证verifying/准备preparing class或者interface以及superclass以及superinterface,(如果是数组calss的话,将包括元素类型)],解决class或interface中的符号引用,将class或者interface组装到运行时run-time state,以便可以被执行。
Linking的过程涉及分配内存空间,可能引起OOM
步骤
verifying
验证字节码表现形式是否结构化正确。验证会引起其他的class或接口被load,但是不会去验证和准备它们。
preparing
创建类或接口的static field,使用系统默认值初始化static field
resolution 贯穿代码执行的持续性阶段
什么时候进行resolution?
class的初始化过程和方法执行时,某些jvm指令被执行,对指令使用的class或接口的符号引用进行解析。
指令:anewarray, checkcast, getfield, getstatic, instanceof, invokedynamic, invokeinterface, invokespecial, invokestatic, invokevirtual, ldc, ldc_w, multianewarray, new, putfield, and putstatic
详情
- 类或接口的解决方式,如D中引用了C
- 使用D的定义类加载器来load和creat C
- 如果C是数组且元素类型是引用类型,则使用D的定义类加载器来load和create该元素类型
- 检查D对C的可访问性,可能抛出IllegalAccessError
- 域field的解决方式,如D中引用了C的field
- 解决类或接口C
- 遍历C以及super中的field
- 在C声明的域中找到匹配的field
- 否则从superinterface中匹配
- 否则从superclass中匹配
- 否则匹配失败, 抛出NoSuchFieldError
- 如果找到field则检查可访问性,若不可访问则抛出IllegalAccessError
- 方法的解决方式,如D中引用了类C的method
- 解决类或接口C
- 检查C是类还是接口,若是接口,则抛出IncompatibleClassChangeError
- 遍历C以及super中的method
- C声明了一个相同名称的多态方法
- C声明了一个具有相同名称和方法签名的方法
- 如果C有superclass,则针对superclass进行3.1
- 如果C有superinterface,则在superinterface中找
- 如果找不到,则抛出NoSuchMethodError
- 如果方法找到了,但是抽象方法,则抛出AbstractMethodError
- 如果方法找到了,但是D不可以访问这个方式,则抛出IllegalAccessError
access control访问控制
D可访问C,需满足其中一个条件
- C是public
- C和D在同一个package中。
D可访问域或者方法R,需满足其中一个条件
- R是public
- R是protected且属于C,需要D是C的子类或者D=C
- R是protected或默认访问性(包级别访问性)且属于C,需要D和C在同一个package中
- R是private且属于D
method overrided方法重写,类C的m1重写了类A的m2方法
C是A的子类,m1与m2具有相同的方法名称和方法声明(m1的访问性<=m2的访问性)
initialization初始化
概念
通过执行class或者interface的初始化方法<clinit>,来初始化class或者interface
‘类或者接口的初始化’的触发原因
- 引用了类或接口的JVM指令被执行:new/getstatic/putstatic/invokesatic
- 执行new指令时,如果符号引用关联的类或接口没有被初始化过,则进行初始化。
- 执行getstatic/putstatic/invokestatic时,声明了这个field或method的类如果没有被初始化,则进行初始化
- java.lang.invoke.MethodHandler实例的第一次调用
- 反射库中具体反射方法的调用
- 子类被初始化
- 作为JVM start-up的初始化类
JVM是多线程,如何保证同步的进行初始化呢?
- 已经完成验证和准备阶段的Class object,可能有如下4种状态
Class object已经被检查和准备,未初始化
Class object正在被某个具体线程进行初始化
Class object已经初始化成功
Class object处于错误状态,可能是初始化正在被执行或者失败
- 对一个具体的类或接口,会对应唯一的初始化锁LC;由JVM负责C与LC的关系;
- 初始化的步骤(指的不是类构造器)
- 在LC上进行同步。当前线程在获取到LC之前会一直waiting
- 如果Class object表明:其初始化正由另一个线程进行。则当前线程会release LC,然后等待被通知这个初始化过程已经完成,然后重复上一步骤。
- 如果Class object表明:其初始化正由当前线程进行。则表明本次递归请求,则release LC并正常结束。
- 如果Class object表明:其初始化已经完成了。则无后续动作,直接release LC并正常结束。
- 如果Class object处于一个错误状态,则表示无法进行初始化,release LC并抛出NoClassDefFoundError
- 上述情况都不成立的话,则记录下Class object的初始化正由当前线程执行,然后release LC
- 按照在classfile中出现的顺序,使用常量数据初始化C中[final static field]
- 递归的进行supercalss和superinterface的初始化过程(所以父类的static块优先执行)
- 执行C的初始化方法<clinit>
- 如果C的初始化方法正常完成,则获取LC,设置Class object已经初始化完毕,notify所有等待的线程,release LC,完成这个过程。
- 如果C的初始化方法由于异常E而僵硬的完成,如果E不是error,则创建ExceptionInInitializerError,进行next step;如果E是OOM,则next step
- 获取LC,设置Class object初始化失败,release LC
几个过程的关系图
类加载器--命名空间--共享访问--安全
- 每个类加载器都有一个独立的命名空间。
- 命名空间概念:虚拟机中存有加载器A的一张表,该表记录了将A视为初始类加载器的所有类型,该表极为A的命名空间。
- 在虚拟机中加载的类是唯一的,这须由加载器命名空间和类全路径名来一起作为限制。
- 类加载器采用双亲委派方式来使用合适的加载器进行加载工作。
- 真正进行加载工作的成为定义类加载器,而之前发起委派的以及定义类加载器都称为初始类加载器。
- 被加载的类A在其初始类加载器B,C,...中共享访问的。
- 加载类A后生成如下约束
- 加载器B是类型A的初始类加载器,加载器C是类型A的初始类加载器,并且这两个类型A是同一个类型。当恶意添加某同名A类(可能输出重要数据)以及重写加载器B(或C)时,这个约束会发现当前加载器B加载的类A和之前加载类A不是一个类型,从而提示错误。如果没有该约束,那么A被加载,重要数据被输出。