Learn && Live
虚度年华浮萍于世,勤学善思至死不渝
前言
Hey,欢迎阅读Connor学JVM系列,这个系列记录了我的JVM基础知识学习、复盘过程,欢迎各位大佬阅读斧正!原创不易,转载请注明出处:http://t.csdn.cn/3T7JH,话不多说我们马上开始!
1.类加载时机
1.1 类生命周期
注意
(1)加载、验证、准备、初始化和卸载顺序确定,类型的加载必须按照此顺序按部就班的开始
(2)解析可能在初始化之后再开始,这是为了支持Java语言的运行时绑定特性,即动态绑定
(3)这些阶段通常都是互相交叉地混合进行的,会在一个阶段执行的过程中调用、激活另一个阶段
1.2 加载阶段开始时机
什么情况下开始加载阶段并没有强制约束
1.3 解析阶段开始时机
(1)执行anewarray、checkcast、getfield、getstatic、instanceof等17个用于操作符号引用的字节码指令之前,需要先对它们所使用的符号引用进行解析
(2)除此之外,到底是在类被加载器加载时就对常量池中进行解析还是等符号引用将要被使用前才去解析,并未明确规定
1.4 初始化阶段开始时机
只有对一个类型进行主动引用时才会触发初始化阶段,被动引用不会触发类型的初始化
主动引用与被动引用
有且只有下述六种情况属于对一个类型的主动引用,必须立即对类进行初始化:
(1)遇到new、getstatic、putstatic、invokestatic这四条字节码指令时,如果类型没有进行过初始化,则需要先进行初始化
- new实例化对象
- 调用或设置一个static变量
- 调用一个static方法
(2)使用java.lang.reflect包的方法对类型进行反射调用,如果类型没有初始化,需要先初始化
(3)初始化类时,如果其父类还没有进行过初始化,如果类型没有初始化,需要先初始化。但接口在初始化时不要求其父接口全部完成初始化,只有在真正用到父接口时才会初始化
(4)虚拟机启动时先初始化主类(包含main()方法的类)
(5)使用JDK7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,且这个方法句柄对应的类没有初始化,需要先初始化
(6)当一个接口中定义类默认方法,如果这个接口的实现类发生了初始化,那该接口要在其之前被初始化
除上述情况外均属于被动引用
2.类加载过程
2.1 加载
非数组类型的加载
加载阶段,JVM需要完成三件事
(1)通过一个类的全限定名来获取定义此类的二进制字节流
(2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
(3)在内存中生成一个代表这个类的Class对象,作为方法区这个类的各种数据的访问入口
获取二进制字节流(加载.class文件)的方式
(1)从ZIP压缩包中读取,JAR、EAR、WAR格式的基础
(2)从网络中获取,Web Applet
(3)运行时计算生成,动态代理技术,用ProxyGenerator.generateProxyClass()为特定接口生成形式为“*$Proxy”的代理类的二进制字节流
(4)由其他文件生成,如由JSP生成对应的Class文件
(5)从数据库中读取,相对少见
(6)从加密文件中获取,如防Class文件被反编译,通过解密获取
(7)……
数组类型的加载
数组类本身不通过类加载器创建,它是由JVM直接在内存中动态构造出来的,但数组类的元素类型还需要依靠类加载器完成
(1)如果数组的组件类型(数组去掉一个维度后的类型)是引用类型,则递归采用非数组类型的加载过程加载这个组件类型,数组将被标识在加载该组件类型的类加载器的类名称空间上
(2)如果数组的组件类型不是引用类型,JVM将会把数组标记为与启动类加载器关联
(3)数组类的可访问性与它的组件类型的可访问性一致,如果组件类型不是引用类型,则它的数组类默认为public
注意
(1)相对于类加载过程的其他阶段,非数组类型的加载阶段(读取二进制字节流的动作)可控性最强
(2)加载阶段既可以由JVM内置的启动类加载器完成,也可以由自定义的类加载器完成,开发人员可通过自定义类加载器来控制字节流的获取方式(重写一个类加载器的findClass()或loadClass()方法)
(3)加载阶段与连接阶段的部分动作(如一部分字节码文件格式验证动作)是交叉进行的
2.2 验证
(1)目的在于确保Class文件的字节流中包含的信息符合当前虚拟机的要求,保证这些信息运行后不会危害虚拟机自身的安全
(2)大致分为四个阶段:文件格式验证、元数据验证、字节码验证和符号引用验证四个阶段
(3)验证阶段并不是必须要执行的阶段,这就有点类似部分游戏安装前的文件验证,如果代码已经被反复使用和验证过,我们就可以通过-Xverify:none参数关闭大部分的类验证阶段,以缩短虚拟机类加载的时间
文件格式验证
(1)验证字节流是否符合Class文件格式规范且满足JVM运行版本要求,保证输入的字节流能正确地解析并存储于方法区之内
(2)包含的验证点如:
-
是否以魔数 0xCAFEBABE 开头
-
主、次版本号是否在当前JVM接受范围之内
-
常量索引是否指向不存在的常量或不符合类型的常量
-
……
元数据验证
(1)对类的元数据信息进行语义分析,保证不存在与Java语言规范相悖的元数据信息
(2)包含的验证点如:
-
这个类是否有父类(除Object)
-
这个类是否继承了final类
-
非抽象类是否实现了父类或接口中的要求实现的抽象方法
-
……
字节码验证
(1)对类的方法体(Class文件中的Code属性)进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为
(2)包含的验证点如:
-
保证任何时刻操作数栈的数据类型与指令代码序列都能配合工作,不会出现“在操作栈放置了一个int类型的数据,却按long类型加载到本地变量表中”
-
保证任何跳转指令都不会跳转到方法体以外的字节码指令上
-
保证方法体内的类型转换总是有效的
-
……
(3)JDK6在Code属性中增加了StackMapTable属性,描述了方法体内代码块开始时本地变量表和操作栈应有的状态,在字节码验证期间,JVM就不需要根据程序推导这些状态的合法性,只需要检查该属性中的记录是否合法即可,将类型推导转变类类型检查,节省大量校验时间
符号引用验证
(1)发生在JVM将符号引用转化为直接引用时,即与解析同时进行,保证解析能正常执行
(2)主要验证该类是否缺少或被禁止访问它依赖的某些外部类、方法等资源,包含的验证点如:
- 符号引用中通过全限定名是否能找到对应的类
- 在指定类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段
- 符号引用中的类、字段、方法的可访问性是否可被当前类访问到
- ……
(3)符号引用验证不通过,JVM抛出IncompatibleClassChangeError的子类错误,如IllegalAccessError、NoSuchFieldError
2.3 准备
(1)正式为类变量(static)分配内存并设置初始值
(2)主要有两种情况
- 通常情况下,会设置为对应类型的默认初始值
public static int value = 123
变量value在准备阶段后的初始值为0,而非123,把123赋值给value需要使用putstatic指令,该指令被编译后存放在类构造器<clinit>()方法中,在初始化阶段才会执行
- 特殊情况,如果该类字段的字段属性表中存在ConstantValue属性,则会初始化为这个属性所指定的值,对应const常量的初始化
public static final int value = 123
编译时Javac会为value生成ConstantValue属性,在准备阶段会根据ConstantValue的设置赋值为123
2.4 解析
(1)解析阶段是JVM将常量池内的符号引用替换为直接引用,并对方法或字段的可访问性进行检查的过程
(2)解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这7类符号引用
符号引用
- 以一组符号来描述所引用的目标,符号可以是任何形式的字面量
- 符号引用与虚拟机实现的内存布局无关,引用的目标不一定是已经加载到虚拟机内存中的内容,且不同的虚拟机能接受的符号引用必须一致
直接引用
- 是可以直接指向目标的指针、相对偏移量或间接定位目标的句柄
- 直接引用是和虚拟机实现的内存布局直接相关的,同一符号引用在不通虚拟机上转换的直接引用一般不同
类或接口的解析
假设当前代码所处的类为D,现将符号引用N解析为一个类或接口C的直接引用,步骤为
(1)如果C不是一个数组类型
- 虚拟机将代表N的全限定名传递给D的类加载器去加载类C
- 由于元数据验证、字节码验证的需要、加载过程中可能会触发其他相关类的加载动作,如加载这个类的父类或实现的接口
- 一旦这个加载过程出现任何异常,解析失败
(2)如果C是一个数组类型,且数组元素的类型为对象
- 按照(1)加载对应的数组元素类型
- 由虚拟机生成一个代表该数组维度和元素的数组类型
- 过程出现异常,解析失败
(3)上述两步均未出现异常,那么C在虚拟机中已经是一个有效的类或接口了,但还要进行符号引用验证,确认D是否具有对C的访问权限。如果不具备访问权限,抛出IllegalAccessError
字段解析
假设这个字段所属的类或接口用C表示,步骤如下
(1)解析字段表中字段所属的类或接口的符号引用,获得C,对C进行后续字段的搜索
(2)如果C本身就包含了简单名称和字段描述符都与目标匹配的字段,则返回这个字段的直接引用,搜索结束
(3)否则,如果C实现了接口,按照继承关系从下往上递归搜索各个接口及其父接口,如果找到,则返回这个字段的直接引用,搜索结束
(4)否则,如果C不是Object类,按照继承关系从下往上递归搜索其父类,如果找到,则返回这个字段的直接引用,搜索结束
(5)否则,搜索失败,抛出NoSuchFieldError
(6)若搜索成功,将会对这个字段进行权限验证,若不具备访问权限,抛出IllegalAccessError
方法解析
假设这个方法所属的类或接口用C表示,步骤如下
(1)解析方法表中方法所属的类或接口的符号引用,获得C,对C进行后续的搜索
(2)由于Class文件中类的方法和接口的方法符号引用的常量类型定义是分开的,故如果C是个接口,则直接抛出IncompatibleClassChangeError
(3)否则,在C中递归搜索是否有简单名称和描述符都与目标匹配的方法,有则返回这个方法的直接引用,搜索结束
(4)否则,在C的父类中递归搜索,如果有则返回这个方法的直接引用,搜索结束
(5)否则,在C实现的接口列表及其父接口中递归搜索,如果有则说明C是一个抽象类,搜索结束,抛出AbstractMethodError
(6)否则,搜索失败,抛出NoSuchMethodError
(7)若搜索成功,将会对这个字段进行权限验证,若不具备访问权限,抛出IllegalAccessError
接口方法解析
假设这个方法所属的类或接口用C表示,步骤如下
(1)解析方法表中方法所属的类或接口的符号引用,获得C,对C进行后续的搜索
(2)与类的方法相反,如果C是个类,则直接抛出IncompatibleClassChangeError
(3)否则,在C中递归搜索是否有简单名称和描述符都与目标匹配的方法,有则返回这个方法的直接引用,搜索结束
(4)否则,在C的父接口中递归搜索,直到Object类为止,如果有则返回这个方法的直接引用,搜索结束
(5)如果C的不同父接口中存在多个匹配的方法,那么将会返回其中一个并结束搜索,但并没有明确的规则规定返回哪一个,但实际中,不同的发行商实现的Javac编译器会按照更严格的约束拒绝编译这种代码来避免
(6)否则,搜索失败,抛出NoSuchMethodError
(7)若搜索成功,将会对这个字段进行权限验证,若不具备访问权限,抛出IllegalAccessError
2.5 初始化
初始化阶段JVM才真正开始执行类中的Java程序代码,而初始化阶段就是执行类构造器<clinit>()方法的过程。注意这里的类构造器并不是Java代码中定义的类构造器,而是由Javac编译器的自动生成的,下面介绍
(1)<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并生成,收集顺序由语句在源文件中的出现顺序决定的
(2)<clinit>()方法与类的构造器(实例构造器<clinit>())不同,它不需要显示地调用父类的构造器。JVM会保证初始化子类是父类的<clinit>()已执行完毕
(3)<clinit>()方法对于类或接口来说并不必需,若一个类中没有静态语句块,也没有对变量的赋值操作,可以不生成对应的方法
(4)接口的<clinit>()方法不需要先执行父接口的<clinit>()方法,接口的实现类在初始化时也一样不会执行接口的<clinit>(),只有在被使用时才会执行
(5)JVM必须保证一个类的<clinit>()方法在多线程环境中被正确地加锁同步,即只能有一个线程同时执行这个类的<clinit>()方法,其他线程阻塞等待