虚拟机类加载机制
文章目录
1.概述
- 类加载机制:虚拟机描述类的数据从Class文件中加载到内存,并对数据进行校验、转换、解析和初始化,最终形成可被虚拟机直接使用的Java类型
- Java中类的加载、连接和初始化在程序运行期间完成,这种策略在加载时稍显笨重,但为Java应用程序提供了高度灵活性
2.类加载时机
-
类从加载到卸载出内存的生命周期
7个阶段:加载=> {验证=>准备=>解析}(连接) =>初始化=>使用=>卸载
-
加载、验证、准备、初始化、卸载顺序固定,解析可在初始化之后(支持了动态绑定或者说叫晚期绑定)
2.1 加载的时机
2.1.1 主动引用
- 遇到:new, getstatis, putstatic, invokestatic,类未初始化则初始化
- 这些指令生成场景:
- new 实例化对象
- 读取或设置类的静态字段(被final修饰,已在编译器把结果放入常量池的静态字段除外)
- 调用一个类的静态方法
- 这些指令生成场景:
- java.lang.reflect包的方法对类进行反射的时候,未初始化则初始化
- 将要初始化类的父类未初始化,则先初始化其父类
- 虚拟机启动,用户需要指定要执行的主类(包含main的类),虚拟机会先初始化这个类
- JDK1.7动态语言支持时
- 一个java.lang.invok.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,且这个方法的句柄对应的类未初始化则初始化
2.1.2 被动引用
- 除以上5种情况外的所有情况
2.1.3 细节
- 静态字段,直接定义此字段的类才被初始化
- 子类引用父类静态字段,只初始化父类初始化,至于子类的初始化与否取决于具体实现,虚拟机未明确规定
- 常量传播优化
- 一个类中引用了另一个类中的常量,编译时会直接将此常量优化添加到此类常量池中,也就是说编译结束,生成Class后,两个类再无联系
>>加载过程<<
Start----
3加载
3.1 类加载的过程
- 加载是类加载的过程
- 此阶段虚拟机的任务:
- 通过类的全限定名获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区的这个类的各种数据的方位入口
- 以上是虚拟规范的显示,但并不具体,但称为了之后一些技术的基础
- 从zip包中读取=>JAR, WAR, EAR格式
- 网络中获取,Applet
- 运行时计算而成=>动态代理
- java.lang.Proxy中就是使用ProxyGenerateProxyClass来为特定接口生成形式为"*$Proxy"代理类的二进制字节流
- 其它文件生成=>JSP
- 数据库读取=>中间件服务器把程序安装到数据库完成程序代码在集群间的分发
3.2 其它过程
- 非数组类的加载阶段(加载阶段中获取二进制字节流的动作)
- 由系统提供的引导类加载器完成
- 用户自定义类加载器完成,重写(loadClass())
- 数组类
- 本身不由类加载器创建,而由虚拟机直接创建
- 但数组的元素类型(Element Type,指去掉所有维度后的类型)要靠虚拟机类加载器创建
- 创建遵循规则:
- 数组的组件类型是引用类型,则递归采用加载对象的类加载器加载组件类型,同时数组将在加载该组件类型的类加载器的名称空间上被表示
- 数组组件不是引用类型,把数组类标记为与引导类加载器关联
- 数组类的可见性与组件的可见性一致,非引用类型,数组类的默认可见性为public
3.2 加载完成
- 虚拟机外部二进制字节流按照虚拟机要求格式存储在方法区
- 内存中实例化一个java.lang.Class对象(HotSpot中Class是对象但放在方法区中),作为程序访问方法区中类型数据的外部接口
3.3 与连接
- 加载和连接的部分内容交替进行
- 加载未完成阶段可能已经开始,这些夹在加载阶段的动作属于连接阶段内容
- 两个阶段仍然有固定的先后顺序
4.验证
-
确保Class文件字节流的信息符合虚拟机要求,并且不会危害虚拟机安全
-
检验动作:
-
文件格式验证
- 检查字节流是否符合Class文件规范,并且能被当前虚拟机处理
- 例如,下面示例了部分检查项目
- 是否以0xCAFEBABE开头
- 主次版本是否在当前虚拟机处理范围之内
- 常量池中是否有不被支持的常量类型
- 指向常量的各种索引值中是否有纸箱部存在的常量或者不符合类型的常量
- 此阶段基于二进制字节流进行,之后验证基于方法区存储结构进行,不再操作字节流
-
元数据验证
- 对字节码描述信息进行语义分析
- 方式:对类的元数据信息进行语义校验,保证不存在不符合Java语言规范的元数据信息
- 对字节码描述信息进行语义分析
-
字节码验证
即使此阶段的校验通过也不能说明程序正确
“Halting Problem":通俗点就是用程序去校验程序逻辑无法做到绝对准确
-
确认语言是否合法,符合逻辑
- 方式:分析数据流,控制流
-
对方法体校验
-
JDK1.6之后Javac编译器和Java虚拟机进行优化,方法体Code属性中添加“StackMapTable"(描述方法体基本块开始时本地变量表和操作栈应有的状态)字节码验证期间通过检查这个表属性是否合法即可,即就是将类型推导转换为类型检查
HotSpot中提供
- -XX:-UseSplitVerfier关闭此优化
- -XX:+FailOverToOkdVerifier要求类型校验失败时退回到旧的类型推导方式
-
JDK1.7之后类型检查完成校验是唯一选择,不再允许退回到旧的类型推导方式
-
-
符号引用验证
- 发生时机:符号引用—>直接引用,连接第三阶段——解析阶段中发生
- 失败:
- throws:java.lang.IncompatibleClassChangeError的子类
- 地位:此阶段重要单非必要,所运行代码被反复验证过则可考虑使用-Xverify:none关闭大部分类验证措施,缩短加载时间
-
5.准备
-
为类变量分配内存并设置类变量初始值,方法区中分配
-
类变量:仅包括类变量(static修饰的变量)
-
初始化:此阶段是初始化值的阶段,一般置0
-
类字段属性表中存在ConstantValue,准备阶段变量便会被初始化为ConstantValue指定的值
例如:public static final int value = 111; //准备阶段此value = 111而不是0
-
-
6.解析
-
任务:常量池中符号引用—>直接引用
- 符号引用(Symblic References):
- 一组符号描述所引用的目标,可以是任何类型字面量,无歧义即可
- 与虚拟机内存布局无关
- 直接引用(Direct References):
- 类型:指针、相对偏移量、间接定位到目标的句柄
- 与虚拟机内存布局有关
- 符号引用(Symblic References):
-
发生时机:
- 执行 anewarray,checkcast,getfield, getstatic, instanceof, invokedynamic, invokeinterface, invokespecial, invokestatic, invokevirtual, Idc, Idc_w, multianewarray, new, putfield, putstatic,操作(这16个操作符号引用的)操字节码指令之前
- 除invokedynamic指令之外,虚拟机可实现对第一次解析结果进行缓存(运行时常量池中直记录接引用,并将常量标识为已解析状态)
- Invokedynamic对应的引用称为”动态调用点限定符“(Dynamic Call Site Specifier),程序运行到这条指令时解析
-
解析针对类型
-
类或接口
-
示例:当前代码D,需要将符号引用N引用解析为C的直接引用
-
1.C非数组类型
代表N的全限定名传给D的类加载器加载类C,加载出现任何异常=>解析失败
-
2.C是数组类型,且数组元素类型为对象
采用1.的加载方式
-
3.无异常,C在虚拟机中已经成为有效的类或接口
-
4.符号引用验证,确认D是否具有对C的访问权限
否=>throws java.lang.IllegalAccessError
是=>解析完成
-
-
-
字段
- 解析流程:
- 1.对表中字段内class_index索引的符号引用进行解析,即字段所属的类或接口的符号引用
- 有异常=>解析失败
- 2.将此字段所属的类或接口用C表示
- C本身包含简单名称和字段描述符和目标匹配的字段=>返回直接引用=>查找结束
- C中实现了接口,则自下而上对接口进行递归搜索,找到匹配字段=>查找结束
- C不是java.langObject,自下而上递归搜索其父类,找到匹配字段=>查找结束
- 查找失败=>抛出java.lang.NoSuchFieldError异常
- C本身包含简单名称和字段描述符和目标匹配的字段=>返回直接引用=>查找结束
- 1.对表中字段内class_index索引的符号引用进行解析,即字段所属的类或接口的符号引用
- 解析流程:
-
类方法
-
接口方法
-
方法类型
-
方法句柄
-
调用点限定符
-
7.初始化
- 类加载过程最后一步,依据程序员定义的主观计划初始化变量和其它资源,也就是执行类构造器的()方法
- ()方法产生:
- 由编译器自动搜集类中的所有类变量的赋值动作和静态语句块中的语句合并而成
- 搜集顺序:源文件中定义顺序
- 变量:
- 静态语句块可方位到定义在其之前的变量,定义在其之后的变量,前面的静态语句块可赋值,但不可访问
- ()父子调用顺序:
- 虚拟机保证调用子类的()方法前,父类的已经执行完毕,故,虚拟机中第一个执行的是java.lang.Object的()
- 父类的静态语句块先于子类
- ()的必要性:
- 非必须,类中无静态语句块可不生成()方法
- 虚拟机保证一个类的()方法在多线程环境下被正确的加锁、同步
- 多个线程初始化一个类,只有一个线程执行这个方法,其它线程阻塞,直至线程活动执行完成
- ()方法产生:
End—
8.类加载器
- 概念:实现类加载阶段中“通过一个类的全限定名获取描述此类的二进制字节流动作”(Java虚拟机外部实现),的代码模块
8.1 类与类加载器
- 确立一个类在虚拟机中的唯一性:类+加载其的类加载器
- 比较两个类是否相等:同一个类(来源是同一个Class文件)+ 同一个类加载器
8.2 双亲委派机制
8.2.1 类加载器类别
- 对虚拟机而言:
- 启动类加载器(Bootstrap ClassLoader):C++实现,虚拟机的一部分
- 其它所有类加载器:Java实现,独立于虚拟机,继承:java.lang.ClassLoader
- 对程序员而言:
- 启动类加载器:
- 负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数化指定的路径下的,虚拟机能识别的(文件名作为识别方式)的类库加载到虚拟机内存中
- 无法被开发者直接引用
- 用户编写自定义类加载器时,若要将请求委派给引导类加载器,用null代替
- 扩展类加载器(Extension ClassLoader):
- 加载器由sun.misc.Launcher$ExtClassLoader实现
- 加载<JAVA_HOME>\lib\ext目录中,或者被java.ext.dirs系统变量指定的路径下的所有类库
- 开发者可以直接使用
- 应用类加载器(Application ClassLoader):
- 由sun.misc.Launcher$AppClassLoader实现,这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,也称为系统类加载器
- 负责加载用户路径指定的类库
- 开发者可直接使用,(应用程序未自定义类加载器时,默认使用)
- 启动类加载器:
8.2.1 工作原理
类加载器的父子关系一般以组合关系来复用父加载器代码
-
工作过程:
- 收到类加载请求—>请求委派给父类加载器(直至顶层启动类加载器)—>父类无法加载时,子类尝试加载
-
原理:
自己编写和rt.jar类库中重名的类,可正常编译,但永远无法加载成功
若强行加载(使用defineClass())加载=>虚拟机抛出java.langSecurityException:Prohibited pakage…异常
- 检查类是否以被加载
- 否=>调用父类加载器,父类加载器为null则启动类加载器作为父类加载器
- 父类加载失败=>抛出ClassNotFoundException后,调动自己的findClass()加载
- 检查类是否以被加载
8.3 委派机制被破坏
- 引入:
- JDK1.2时引入双亲委派机制,面对已存在的用户自定义类加载器,java.lang.ClassLoader添加protected 的 findClass()方法
- 模型自身缺陷:
- 实现基础类调用回用户的代码,引入上下文类加载器(Thread Context ClassLoader),可通过java.lang.Thread类的setContextClassLoader()方法设置
- 设置线程时未创建则从父类继承
- 全局未设置,则使用应用程序类加载器
- 实现基础类调用回用户的代码,引入上下文类加载器(Thread Context ClassLoader),可通过java.lang.Thread类的setContextClassLoader()方法设置
- 追求动态性:
- 每个程序模块(OSGi 中称为Bundle)有自己的类加载器,需要更换Bundle时,连同其类加载器一起替换,实现热替换
- OSGi类搜索流程:
- java.*开头的—>委派给父类
- 否则,将委派列表中的类委派给父类加载器加载
- 否则,Import列表中的类委派给Export类的Bundle的类加载器加载
- 否则,查找当前Bundle的ClassPath,使用自己的类加载器加载
- 否则,查找类是否在自己的Fragment Bundle中,在=>委派给F人阿根廷Bundle的类加载器加载
- 否则,查找Dynamic Import列表的Bundle,委派给对应的Bundle类加载器加载
- 否则=>类加载失败
- OSGi类搜索流程:
- 每个程序模块(OSGi 中称为Bundle)有自己的类加载器,需要更换Bundle时,连同其类加载器一起替换,实现热替换
参考书籍:周志明《深入理解Java虚拟机》