JVM类加载JVM整个流程图 一个java文件被编译为class文件后,剩下的操作都交给jvm来执行,其中第一步就是将class文件加载到jvm,而这一步就是由类加载器来完成的 类加载的流程又分为加载(Loading),验证(Verification),准备(Preparation),解析(Resolution),初始化(Initialization) 而其中验证,准备,解析这三步统称为连接(Linking) 类加载器只负责加载class文件,至于加载的class是否能正常执行则是由执行引擎决定 加载这里的加载只是类加载器中的一步操作,也叫加载而已,这一步完成三个事情
其中这三步在java虚拟机规范中并没有要求特别具体,java虚拟机实现的灵活度相当大,例如第一步中获取二进制字节流
相对于类加载其他阶段,非数组类型的加载阶段,加载阶段中是开发人员可控性最强的阶段,加载阶段既可以使用java虚拟机内置的引导类加载 器来完成,也可以使用用户自定义加载器来完成 连接验证验证是连接阶段的第一步,目的是为了确保Class文件的字节流中包含的信息符合java虚拟机规范的全部约束要求,保证这些信息被当做代码后运行不会对java虚拟机产生危害 而验证有包含四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证 文件格式验证第一阶段要确保字节流符合Class文件格式规范,并能被当前版本虚拟机处理,这一阶段可能包含下面这些验证点
实际上第一阶段的验证点远远不止这些,上面所列出的只是从HotSpot虚拟机中的一小部分,该验证阶段的主要目的就是为了保证输入的字节流能正确的解析并存储在方法区中,格式上符合描述一个java类型信息的要求. 这个阶段验证是基于二进制字节流进行的,只有通过这个阶段的验证过后,这段字节流才会被允许进入java虚拟机内存的方法区中进行存储,后面的三个验证阶段全部都是基于方法区中的存储结构上进行的,不会再进行直接读取,操作字节流了 元数据验证第二阶段是对字节码描述的信息进行语义分析,以确保其描述的信息符合java语言的规范,这个阶段可能包括验证点如下
字节码验证第三阶段是整个验证过程最复杂的一个阶段,目的是通过数据类分析和控制流分析,确定程序语义是合法的,符合逻辑的,在第二阶段对元数据信息中的数据类型校验完毕后,这个阶段就是要对类的方法体(Class文件中的Code属性)进行校验分析,保证被校验类的方法在运行时不会出现对虚拟机危害的行为,例如
如果一个类型中有方法体的字节码没有通过字节码验证,那么它肯定是有问题的,但是如果一个方法体通过了字节码验证,也并不能保证它一定就是安全的,即使字节码验证阶段进行再大量,再严密的检查,也不能保证这一点 符号引用验证最后一个阶段的校验行为发生在虚拟机将符号
符号引用验证主要目的就是确保解析行为能正常运行,如果无法通过符号引用验证,java虚拟机将抛出一个java.lang.IncompatibleCLassChangeError的子类异常 验证阶段对于虚拟机的类加载机制来说,是非常重要的,但却不是必须执行的阶段,因为验证阶段只有通过或者不通过的区别,只要通过了验证,其后就对程序运行期没有任何影响了,如果程序运行的全部代码都已经被反复使用和验证过,在生成环境的实施阶段可以考虑使用-Xverify:none参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间 准备准备阶段是正式的为类中定义的变量(即静态变量,被static修饰的变量),分配内存并设置变量的初始值的阶段 关于准备阶段,首先是这时候进行内存分配仅包括类变量,而不是实例变量,实例变量将会在对象实例化时随着对象一起分配在java堆中 在这个阶段中,所有的基本类型都会被赋值为 基本数据类型的零值
这里不包含final修饰的static,因为final在编译期间就已经分配值了,准备阶段会显式初始化 解析解析阶段是java虚拟机将常量池内的符号引用替换为直接引用的过程
解析动作一般针对类或接口,字段,类方法,接口方法,方法类型等,对应常量池中的CONSTANT_Class_info,CONSTANT_Fieldref_info CONSTANT_MethodHandle_info等 初始化类的初始化时类加载的最后一个步骤,进行准备阶段时,变量已经赋值过一次系统要求的零值,而在初始化阶段,则会根据程序员通过程序编码制定的主观计划去初始化类变量和其他资源,初始化阶段其实就是执行类构造器clinit方法的过程,clinit并不是程序员在java代码中编写的方法,它是由javac编译器的自动产生物 clinit方法是由编译器自动收集类中所有类变量赋值(静态的变量)的动作和静态语句块 static{} 块中的合并产生的,收集的顺序是由语句在源文件中出现的顺序决定的 静态语句块中只能访问到定义静态语句块之前的变量,而定义在它之后的变量,可以赋值,不能访问
clinit方法和类的构造函数(即在虚拟机视角中的实例构造器init方法)不同,它不需要显示调用父类构造器,java虚拟机会保证在调用子类clinit方法前,父类的clinit方法已经执行完毕,因此java虚拟机中第一个执行的clinit方法的类型一定是java.long.Object clinit方法对于类来说并不是必须的,如果类中没有对类变量赋值的操作,同时也没有静态代码块,那么编译器可以不为这个类生成clinit方法 接口中不能使用静态语句块,但仍然有变量赋值的操作,因此接口与类一样都会生成clinit方法,但与接口不同的是,执行接口clinit方法不需要先执行父接口的clinit方法,因为只有父接口中定义的变量被使用时,父接口才会被实例化,此外,接口的实现类初始化时也一样不会执行接口中的clinit方法 java虚拟机必须保证一个类的clinit方法在多线程情况下能够正确的加锁同步,如果有多个线程同时初始化一个类,那么只会有其中一个线程执行类的clinit方法,其他线程都需要阻塞等待,知道活动线程执行完毕clinit方法 类加载器从java虚拟机角度来看,只存在两种类加载器器:一种启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++实现,是虚拟机的一部分,另一种就是其他所有的类加载器,这些都由java实现,独立于虚拟机外部,并且全部继承于java.lang.ClassLoader 而从程序角度来划分了类加载器 启动类加载器(引导类加载器,Bootstrap ClassLoader)使用C++实现,嵌套在JVM内部,这个类加载器用来负责加载存放在<JAVA_HOME>\lib目录,用于加载JVM自身需要的类,并不继承java.lang.ClassLoader,没有父加载器,加载应用类加载器和扩展类加载器,并指定为它们的父类加载器 扩展类加载器(Extension ClassLoader)这个类加载器在类sun.misc.Launcher$ExtClassLoader中以java代码实现,它负责加载<JAVA_HOME>\lib\ext目录中,或被java.ext.dirs系统变量指定的路径中所有类库 程序类加载器(系统类加载器 Application ClassLoader)这个类加载器由sun.misc.Launcher$AppClassLoader实现,负责加载用户类路径ClassPath上所有类库,父类加载器为扩展类加载器,该类是程序中的默认加载器,一般情况java应用的类都是由它来完成,通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器 继承关系 ClassLoader常用方法
双亲委派机制双亲委派模型的工作过程:当接收到类加载请求时,类加载器首先会委托父类加载器(注意这里说的父类一般不是继承关系,而是通常使用组合关系来复用父类加载器的代码)进行加载,每一层都是如此,因此所有类加载请求最后都会到达启动类加载器(Bootstrap ClassLoader),只有父类加载器无法完成这个加载请求时(在它的搜索范围内没有找到所需的类),子类加载器才会尝试自己去完成加载 使用双亲委派模型来组织类加载器之间的关系,一个显而易见的好处就是java中的类随着它的类加载器一起具备了一种带有优先级的层次关系 例如java.lang.Object,它存放在rt.jar中,无论哪一个类加载器要加载这个类,最终委托还是派给模型中处于顶端的类加载器进行加载,因此Object类在程序的各种类加载器环境中都能保证是同一个类,同时双亲委派模型也可以保证java核心API的安全性,例如自己也在项目中创建一个java.lang.String,如果加载了自定义的String类,程序将变得十分混乱 双亲委派模型实现全部集中在loadClass()方法中
例如下面自定义一个String,全限定名为java.lang.String
运行出现下面错误 原因就是类加载器向上委托直到引导类加载器,引导类加载器发现可以加载java.lang下的类,于是加载了java自带的String类,之后调用java自带的String类的main方法,发现没有该方法,于是抛出异常 其他在JVM中判断两个类是否相同有两个条件
即使两个类的类对象(Class对象)来源于同一个class文件,被同一个的类加载器加载,只要加载它们的ClassLoader对象实例不同,那么这两个类对象也是不相等的 对类加载器的引用JVM必须知道一个类型是由启动加载器还是用户类加载器加载的,如果一个类型是用户类加载器加载的,那么JVM会将这个类加载器的一个引用类型信息的一部分保存在方法区中,当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器时相同的 类的使用和被动使用java程序对类的使用方式分为:主动使用和被动使用
这里的类的初始化指的是类加载中3大步骤中的最后一步 本文仅个人理解,如果有不对的地方欢迎评论指出或私信,谢谢٩(๑>◡<๑)۶ |
|