JVM 类的加载
1类加载过程
2、概述
a类加载必须按加载、验证、准备、初始化、卸载顺序按部就班的开始,但有可能会在一个阶段执行的过程中调用、激活另一个阶段
b解析在一些情况下可以在 初始化 阶段以后开始
c加载阶段和连接阶段部分工作交叉进行
d创建好类以后,随时可以进入准备阶段,但必须在初始化阶段开始之前完成(Preparation may occur at any time following creation but must be completed prior to initialization.)
?
?
3、加载(Loading)
3.1、概述
a什么时候开始加载,JVM规范没有强制约束????
b接口或类C的创建时由另外个类或接口D触发或D调用JSE类库中的某些方法触发,比如反射等
c如果C是数组,则因数组类没有外部的二进制表示,所以都是JVM创建,不通过类加载器加载(created by the Java Virtual Machine rather than by a class loader)
d无论加载器L直接创建C或授权其他加载器创建C,都称L是C的初始加载器
e JVM支持两种加载器:JVM提供的引导类加载器、用户自定义的类加载器
f在C(或它的父类)的初始加载器抛出ClassNotFoundException错误时,JVM试图验证或解析C,必须抛出NoClassDefFoundError(If the Java Virtual Machine ever attempts to load a class C during verification or resolution (but not initialization), and the class loader that is used to initiate loading of C throws an instance of ClassNotFoundException, then the Java Virtual Machine must throw an instance of NoClassDefFoundError whose cause is the instance of ClassNotFoundException)
g一个功能良好的加载器应当保证三个行为:
g.1如果名称相同,返回的class对象必定相同
g.2无论是L1加载,还是L1委托给L2加载,都必须返回相同Class对象
g.3用户自定义加载器在加载出错时,必须反应加载出错点
h加载数组类A创建过程遵循以下规则
i加载阶段结束后,class文件就会被以指定格式存储在方法区,并在类型数据存放好后,会在堆中生成对应的Class类对象,作为访问方法区中类型数据的外部接口
?
?
3.2类加载器
a、类加载器(Class Loader): 实现类加载阶段中的"通过一个类的全限定名来获取描述该类的二进制字节流"这个动作的代码
b、一个JVM可以有多个类加载器,每个类加载器都有一个独立的类名称空间,仅当两个类被同一类加载器加载,才有可能相同,否则必定不同
c、站在JVM角度,加载器有两种:
c1、启动类加载器(Bootstrap ClassLoader),是JVM自身的一部分,具体实现语言根据JVM自身实现决定,可能是JAVA、C、C++等
c2、非启动类加载器,都有JAVA语言实现,独立于JVM,并全部继承java.lang.ClassLoader
d、站在JAVA开发人员的角度,有三层类加载器:
d.1、启动类加载器(Bootstrap ClassLoader):存放在${JAVAHOME}/lib目录或者通过-XBootclasspath参数指定路径,且是JVM指定的文件名(如rt.jar、tool.jar);无法被JAVA直接引用,但可以通过在自定义类加载器中返回null,把加载请求委派给引导类加载器去处理
d.2、扩展类加载器(Extension Class Loader):在sun.misc.Launcher$ExtClassLoader中以JVM代码实现;负责加载${JAVAHOME}/lib/ext目录或java.ext.dirs系统变量指定路径中所有类库,用户可以将通用的类库放置此处来扩展JAVA。可以在程序中直接使用此加载器加载类。JDK9之后被平台类加载器(Platform Class Loader)代替
d.3、平台类加载器(Platform Class Loader)
d.4、应用程序类加载器(Application Class Loader):由sun.misc.Launcher$AppClassLoader实现。负责加载用户类路径(classPath)上所有的类库,如果没有自定义类加载器,则默认为此。
?
3.3双亲委派
a、工作过程:收到一个类加载请求,先将其委派给父类加载器加载,直到最顶层的类启动加载器中,当父类加载器无法完成加载(找不到类),子类加载器才会尝试加载类
b、并不是一个有强制性约束的模型,而是一种类加载器实现的推荐实现
c、能保证类在各个环境中保持唯一
d、自定义类加载器实现代码实例
e、JDK 9模块化后,类加载器的双亲委派机制被第四次破坏
?
4、验证(Verification)
4.1、概述
a因为JVM虚拟机规范和《深入理解JAVA虚拟机规范(第三版)》对这块介绍差异较大,这里以JVM规范为主要介绍内容, 《深入理解JAVA虚拟机规范(第三版)》内容作为补充
b如果类或接口不满足静态或结构的约束,必须在失败处抛出VerifyError异常
c理解这块,最好先对JVM指令集有初步了解。
?
4.2格式验证
a检查项包括但不限于:
a.1是否魔数开头
a.2主、次版本是否符合当前JVM范围
a.3常量池中是否存在 不存在或类型错误的常量
a.4常量类型的数据结构是否正确
a.5所有属性的数据结构是否正确
a.6不能缺失内容,尾部不能有多余数据
a.7所有字段/方法引用,必须有有效的名称、类、描述符
b 格式检查不保证某个字段/方法/描述符是否真实存在,只保证格式正确
?
?
4.3静态约束(static constraint)
- 来定义文件是否编排良好,确定了jvm指令在code数组中是如何编排,以及某些特殊指令必须带有哪些操作数等
- 数组中只能出现指定的指令;使用保留的指令或其他JVM规范外的指令,不能出现在code中
- class大于51,则jsr和jsr_w也不能出现在code数组中
- 操作码从code数组的下标0开始
- 除最后条指令,当前指令索引+指令长度(包含操作数)可以获取到下条指令索引
- 所有跳转和分支指令的跳转目标必须是本方法内某个指令的操作码,可以是wide指令本身,但一定不能是被wide指令修饰的操作码
- 指令tableswitch跳转目标必须是当前方法内;必须包含和自己的low和high操作数值相等的条目,且low必须小于high
- 指令lookupswitch条抓你目标必须是方法内;必须包含与npairs值相等的match-offset键值对;match-offset必须根据match值升序排列
- 指令ldc和ldc_w的操作数必须是常量池中一个有效索引,常量不能是以下类型(这里跟jdk8有差异):
- 常量CONSTANT_Long 或 CONSTANT_Double类型
- 如果是CONSTANT_Dynamic类型,其CONSTANT_NameAndType_info是J(long)或者D(double)
- 指令ldc2_w的操作数必须是常量池中一个有效索引,紧随其后的索引也必须是个有效索引并不能被使用;其本身必须是下面之一:
- 常量是CONSTANT_Long or CONSTANT_Double.类型
- 如果是CONSTANT_Dynamic类型,其CONSTANT_NameAndType_info是J(long)或者D(double)
- 指令getfield, putfield, getstatic和putstatic的操作数必须是常量池一个有效的CONSTANT_Fieldref类型索引
- 指令invokevirtual的indexbyte必须是常量池有效的CONSTANT_Methodref索引
- 指令invokespecial和invokestatic的indexbyte必须是常量池一个有效索引。如果class版本小于52,常量类型必须是CONSTANT_Methodref;如果大于52则必须是CONSTANT_Methodref或CONSTANT_InterfaceMethodref
- 指令invokeinterface的indexbyte必须是常量池一个有效的CONSTANT_InterfaceMethodref类型索引。第四个操作数必须是0;其count操作数值必须等于接口方法的参数数量
- 指令invokedynamic的indexbyte必须是常量池一个有效的CONSTANT_InvokeDynamic类型索引;并且第三和第四个操作数必须是0
- 只有invokespecial指令可以调用实例初始化方法
- 指令instanceof, checkcast, new, anewarray的操作数以及multianewarray的indexbyte操作数必须是常量池一个有效的CONSTANT_Class类型索引
- 指令new不能用于创建数组
- 指令anewarray不能创建超过255个维度的数组
- 指令multianewarray创建的数组,维度可以比indexbyte值小,但不能比操作数值的多。同时其dimensions不能为0
- 指令newarray的atype操作数值类型必须是:T_BOOLEAN (4), T_CHAR (5), T_FLOAT (6), T_DOUBLE (7), T_BYTE (8), T_SHORT (9),T_INT (10), T_LONG (11)之一
- 指令iload, fload, aload, istore, fstore, astore, iinc, ret的索引操作数必须是非负且不大于max_locals – 1;
指令iload_<n>, fload_<n>, aload_<n>, istore_<n>, fstore_<n>, and astore_<n>默认包含的索引必须小于等于max_locals – 1
- 指令lload, dload, lstore, dstore的索引操作数必须小于等于max_locals – 2;
指令lload_<n>, dload_<n>, lstore_<n>, and dstore_<n>默认包含的索引必须小于等于max_locals – 2
- 修饰指令iload, fload, aload, istore, fstore, astore, iinc, ret的wide指令的indexbyte操作数值必须是非负且不大于max_locals – 1;
修饰指令lload, dload, lstore, dstore的wide指令的indexbyte操作数值必须是非负且不大于max_locals – 2;
?
4.4结构化约束
- 任何指令只能在操作数栈和局部变量表中具备类型和数量均合适的操作数时执行,不关心调用它的执行路径(Each instruction must only be executed with the appropriate type and number of arguments in the operand stack and local variable array, regardless of the execution path that leads to its invocation.)
- 可以操作int值的指令同样可以操作boolean, byte, char, short类型
- 如果指令通过不同执行路径执行,在执行之前,每条路径的操作数栈必须有相同的深度
- 在执行过程中,操作数栈深度不能大于max_stack,不允许从栈中去除比其包含的全部数据还多的数据(At no point during execution can more values be popped from the operand stack than it contains)
- 在执行过程中,long或double的局部变量的存储顺序绝不能倒置或割裂,也不能分开使用
- 在正式赋值前,不能访问局部变量
- 指令invokespecial必须指明下列方法之一:
- 实例初始化方法
- 当前类或接口的方法
- 当前父类的方法
- 当前类或接口的直接父接口中的方法
- Object中的方法
- 指令invokespecial并遵循以下规则
- 调用实例初始化的时候,其目标引用必须是尚未初始化的类实例
- 调用未初始化的类实例的初始化方法,必须是当前类或直接父类的方法
- 调用之前new指令创建的类实例的初始化方法,必须是类实例所属的类的方法
- 如果调用非初始化方法,目标引用必须跟当前类兼容
- 除了Object的实例化方法外,所有实例初始化方法在访问实例成员之前必须通过this调用其他初始化方法或通过super调用父类的初始化方法,并可以在调用实例化方法(包括Object)之前,给当前实例中的字段赋值
- 如果一个未初始化的局部变量实例抛出一个错误:
- 如果是在<init>方法中,则必须抛出错误或循环尝试
- 如果非<init>方法中,此实例必须被标记为未初始化
- 执行指令jsr或jsr_w时,在操作数栈或局部变量表中不能有未初始化的实例
- 方法调用指令的目标实例类型必须与指令所指定的类或接口的类型相兼容
- 所有方法调用的参数,其类型都必须与方法描述符相兼容
- 所有返回指令必须与方法的返回类型匹配:
- 返回boolean, byte, char, short, int,只能用指令ireturn
- 返回float, long, double,只能用指令freturn, lreturn, dreturn
- 返回reference,只能用areturn指令,且返回值类型必须与方法的返回描述符类型相兼容
- 所有实例初始化方法,类或接口的初始化方法,声明返回void的方法,都必须用return指令
- 所有被getfield指令访问或被putfield指令修改的类实例,其类型都必须与指定类的类型相兼容
- 通过putfield或putstatic指令保存的值的类型必须与正在操作的实例或类的字段描述符相兼容
- 由aastore指令存储到数组中的每个值必须是引用类型,其正在操作的数组的组件类型也必须时引用类型
- 指令athrow只能抛出Throwable或其子类
- 方法异常表中catch_type项,必须是Throwable或其子类
- 通过getfield/putfield访问不在同包父类中的protected字段或通过invokevirtual/ invokespecial指令访问protected方法,那么目标实例必须是当前类或当前类的子类
- 程序执行不能超过code数组末端
- 返回地址不可以从局部变量表中加载
- 只能通过ret指令返回jsr或jsr_w指令后面的那条指令
- 不能通过jsr或jsr_w指令递归调用已位于子例程调用链(subroutine call chain)中的子例程(subroutine)。
- 所有returnAddress类型的实例最多返回一次
- 如果某条ret指令可以返回到子例程调用链中位于该指令上方的某个点,那么与此指令相对应的那个returnAddress类型的实例,绝不能用作返回地址
?
4.5Class文件校验——涉及太晦涩,暂时不整理
这块我看着有点晦涩,后续再搞
?
4.6JVM限制
a、每个类/接口的常量池最多65535个,因为constant_pool_count是16位
b、类/接口可以声明的字段数最多65535,因为fields_count是16位,但fields_count不包含父类/接口继承下来的字段
c、类/接口可以声明的方法数最多65535,methods_count限制,不包括父类/接口继承下来的方法
d、类/接口可以声明的直接父接口最多65535,interfaces_count限制
e、方法调用时创建的栈帧里局部变量表中最大局部变量数位65535个,max_locals和JVM指令集的16位局部变量索引限制。注意long和double会占用两个槽,会减少局部最大数量值
f、方法帧中的操作数栈的最大深度为65535,max_stack限制,同样long和double会占用两个槽,减少最大值
g、方法参数最多255个,由方法描述符的定义限制,如果调用实例/接口方法,这个限制包含this,同样注意long和double
h、数组维度最大255
i、字段/方法名、字段/方法描述符以及其他常量字符串最大长度65535个字符,因为CONSTANT_Utf8_info的16为无符号length限制。注意Utf-8一般用两个或三个字节来编码字符,所以实际最大长度很可能少于65535
?
5、准备(Preparation)
a此阶段时正式为类中定义的静态变量在方法区中分配内存并设置变量初始值的阶段
b此阶段仅处理类变量,不包括实例变量
c当用final static 声明常量,则会在此此阶段赋具体值
d JVM在此阶段也有些强制约束,这块有点晦涩,,建议看JVM规范英文原文:
d.1假定L1是C的加载器,对于每个在C中的方法m,m覆盖了C的父类或父接口<D,L2>中某个方法,m返回值是Tr,形参从个Tf1....Tfn。
d.1.1如果Tr不是数组,则用T0代替Tr;否则,T0就表示Tr的元素类型
d.1.2从1到n的每个i来说,如果Tfi不是数组类型,Ti表示Tfi,否则Ti就是Tfi的元素类型
d.1.3对于0到n的每个i来说,Ti(L1)=Ti(L2)都该成立
d.2假定C实现了父接口<I,L3>中的方法m,但c自己却没有声明这个方法m,那么就把声明了由C所继承的方法m实现的那个超类标记为<D,L2>( For each instance method m declared in a superinterface <I, L3> of C, if C does not itself declare an instance method that can override m, then a method is selected with respect to C and the method m in <I, L3>. Let <D, L2> be the class or interface that declares the selected method)。JVM也加了同d.1相同的约束。
?
6、解析(Resolution)
6.1、概述
a解析阶段是JVM将常量池内的符号引用替换为直接引用的过程(Resolution is the process of dynamically determining one or more concrete values from a symbolic reference in the run-time constant pool)
b符号引用(symbolic reference):
b.1以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标就行
b.2与JVM实现的内存布局无关,引用的目标不一定是已经加载到JVM内存当中的内容
b.3JVM实现的内存布局可以各不相同,但其能接受的符号引用必须是一致的
c直接引用(Direct reference):
c.1是可以直接指向目标的指针、相对偏移量或者是个能间接定位到目标的句柄
c.2和JVM的内存布局直接相关,同个符号在不同JVM上翻译出来的直接引用一般不相同。
c.3如果有了直接引用,那引用的目标一定已存在于JVM的内存中
d JVM规范未规定解析阶段具体发生时间,只要求了在执行anewarray, checkcast, getfield, getstatic, instanceof, invokedynamic, invokeinterface, invokespecial, invokestatic, invokevirtual, ldc, ldc_w, ldc2_w, multianewarray, new, putfield, putstatic等指令时,需要先对符号引用进行解析(Execution of any of these instructions requires resolution of the symbolic reference)
e非invokedynamic指令以外的多次解析同一个符号引用,JVM需要保证在同一个实体中,解析结果必须与第一次解析结果相同,即使第一次解析失败,后续解析成功。
f对于invokedynamic指令发生的符号引用解析,其解析结果仅仅限于当前指令,当前符号引用对于其他指令有效,均为未解析状态(The symbolic reference is still unresolved for all other instructions in the class file, of any opcode, which indicate the same entry in the run-time constant pool as the invokedynamic instruction above)
g符号引用解析过程发生错误,抛出IncompatibleClassChangeError或它子类错误
?
6.2、解析类和接口
a假设当前代码所处的类为D,需要将未解析的符号引用N解析为接口或类C的引用,解析过程
a.1如果C不是数组类型,JVM会把N的全限定名传给D的类加载器去加载C。在加载C过程中,出现任何失败,均认为当前解析过程失败
a.2如果C是一个数组类型,并且数组的元素类型为对象,JVM将a.1形式加载数组的元素类型,接着JVM生成一个代表数组维度和元素的对象
a.3当执行完前两点,并且无异常,那么C在JVM中已经是个有效的类或接口,但还需要进行访问权限验证。如果访问权限验证失败,则这将抛出IllegalAccessError错误
b D可访问类或接口C的权限条件,需满足以下之一:
b.1 C是Public,且与D处于同一个模块
b.2 C是Public,与D不处于同一个模块,但C所处模块允许被D所处的模块访问
b.3 C不是public,但C和D处于同一个包
?
6.3、解析字段
a要解析一个未被解析过的字段符号引用
a.1首先将会对字段表中class_index项中索引的CONSTANT_Class_info符号(字段所属的类或接口C)引用进行解析,如果解析C失败,则抛出NoSuchFieldError错误
a.2如果C解析成功,且C本身包含了匹配的字段,则返回字段的直接引用。
a.3如果C本身未包含匹配的字段,会按照继承关系,从下往上递归搜索父接口或父类,查找匹配的字段。如果找到了匹配字段,则返回字段的直接引用。否则抛出NoSuchFieldError错误。
a.4如果字段查找成功,则判断字段访问权限,如果不具备访问权限,则抛出IllegalAccessError异常
b字段或方法R可访问权限,需要满足以下条件之一
b.1 R是public
b.2 R是protected,定义在类C中,且D是C的子类或本身
b.3 R是protected或者默认权限,所在类C和类D在同一个包中
b.4 R是private,被声明在D类中
?
6.4、解析方法
a解析方法开始的前提是,所在类C能被正常解析
b如果在类的方法表中发现class_index中索引的C是接口的话,直接抛出IncompatibleClassChangeError错误
c解析过程:
c.1如果C中有同名的方法引用且是签名多态方法(signature polymorphic method declaration),则查找成功。方法描述符中所提到的全部类名也被解析
c.2如果C中方法引用存在相同的名称和描述符,那么查找也成功
c.3如果C有父类,递归重复c.1-c.3步骤,在父类中查找
c.4否则C尝试从父接口去定位方法
c.5如果C中某些最具体的父接口方法(maximally-specific superinterface methods)与方法引用所指定的描述符和名称相符,且存在有且只有一个方法未设置ACC_ABSTRACT标志,那么返回次方法,查找结束
c.6如果C的任意父接口中存在一个与方法引用相同的描述符以及名称的方法,该方法即无ACC_STATIC标志,有没ACC_PRIVATE标志,那么返回此方法,并成功结束
c.7上诉步骤仍找不到,宣告查找失败
d如果方法查找失败,抛出NoSuchMethodError
e如果方法查找成功,但无访问权限,则抛出IllegalAccessError
f类或接口C的最具体的父接口方法,是指满足以下所有条件的任意方法:
f.1方法是C的直接或间接父接口
f.2此方法是根据指定的名称和描述符来声明的
f.3此方法即无ACC_STATIC标志,也没ACC_PRIVATE标志
f.4此方法是唯一存在的
?
6.5、解析接口方法,只列出与普通方法差异的
a.解析接口方法开始的前提是,所在接口I能被正常解析
b如果在接口的方法表中发现I是类的话,直接抛出IncompatibleClassChangeError错误
c否则,在I的父接口中递归查找,直到Object类,如果存在相同方法,这直接返回
?
?
?
7、初始化(Initialization)
7.1、概述
a、初始化阶段,就是执行构造器<clinit>()方法的过程
b、只有发生以下行为时,类或接口才会初始化(A class or interface C may be initialized only as a result of:)
b.1执行指定指令:new、getstatic,putstatic、invokestatic
b.2在初次调用java.lang.invoke.MethodHandle实例时,方法句柄是2 (REF_getStatic), 4 (REF_putStatic), 6 (REF_invokeStatic), or 8 (REF_newInvokeSpecial)
b.3调用反射方法
b.4对某个类的子类进行初始化
b.5如果接口c声明了一个非抽象、非静态的方法,有类直接或间接引用此接口的类初始化(If C is an interface that declares a non-abstract, non-static method, the initialization of a class that implements C directly or indirectly.)
b.6被指定为jvm启动时初始类
c在类或接口初始化之前,当前类或接口必须已经通过验证、准备阶段。
d直到初始化阶段,JVM才真正开始执行类中编写的Java代码,将主导权交给app
e每个类或接口都有个唯一的初始化锁,锁的实现有JVM实现自定义
f JVM必须保证一个类的初始化线程安全。
?
7.2、初始化过程
注:
1:执行初始化过程,线程的中断状态不受影响
2:抛出的异常E,不是Error或其子类,那就为E创建个ExceptionInInitializerError实例代替它,如果OOM问题,则用OOM错误代替E
3:利用ConstantValue属性初始化类中每个final static字段(Then, initialize each final static field of C with the constant value in its ConstantValue attribute, in the order the fields appear in the ClassFile structure.)
4:初始化方法<clinit>