-
深入理解jvm-2Edition-虚拟机类加载机制
1、概述-什么是类加载?
将Class文件从其他地方(外存、字节流甚至是网络流中)载入内存,
并对其中数据进行校验、转换解析和初始化,最终从其中提取出能够被虚拟机使用的Java类型。
用图纸造模子,该模子能够用于生产对象。
运行时再进行类型的加载、链接和初始化虽然带来了一些性能上的影响,
但是也使得Java可以动态扩展。这也是反射等特性的支撑。
类的生命周期:(宏观上的,具体可能会相互交叉嵌套)
1、加载(载入内存,真正被虚拟机看见)
2、验证(格式、内容逻辑)
3、准备
4、解析
5、初始化
6、使用
7、卸载
2、3、4也被统称为链接阶段。
2、什么时候要进行类加载?
虚拟机规范里面没有规定何时加载,只确定了这五种情况要初始化(那就肯定要先加载啦):
1、遇到new、getstatic、putstatic或invokestatic字节码指令时,如果类没有进行过初始化,则要出发其初始化。
就是使用new实例化、访问静态字段/方法时。
2、使用java.lang.reflect包的方法对类进行反射调用时。如果没有初始化,也要触发初始化。
3、初始化一个类,但是其父类没有初始化过,也要先对父类进行初始化。
(父类初始化一定在子类之前,Object类是最先初始化的)。
4、虚拟机启动时,需指定要执行的主类,虚拟机会先初始化主类。
5、使用动态语言支持时,如果一个java.lang.invoke.MethodHandle实例
解析结果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,
并且该句柄对应的类没有初始化过,那么就要初始化。
以上五种行为称为对一个类进行主动引用,当且仅当这些情况会出生发初始化。
如以下情况不是主动引用(那就是被动引用啰):
1、子类引用父类的静态字段,不会初始化子类,但是会初始化父类。
2、通过数组定义引用类,不会出发被引用类的初始化。
因为类和引用该类创建的数组不是一个类,虚拟机会生成一个直接继承自Object的类来表示数组。
创建指令为newarray。该类封装了对数组的访问,而不是向C/C++一样直接去操纵指针,因此更安全。
3、对类的编译时常量(static final修饰,并且值在编译时可以确定的字段)的访问不会导致初始化,
因为编译时常量会直接存入类的常量池中,对它的访问本质上没有引用到定义它的类。
接口的加载过程与类的加载过程有一点不同,接口也有初始化过程,
但是接口初始化时不要求其父接口都完成了初始化。父接口只有在真正用到时才被初始化。
3、深入类加载过程
1、加载
加载是类加载过程的一个阶段,在这个阶段需要完成三件事:
1、通过类的全限定名来获取此类的二进制流。
没有说从哪里获取,那就大有可为了,
可以从Jar包、网络、由其他文件生成(JSP)、数据库中读取、甚至运行时生成(动态代理)。
2、将二进制流中表示的静态的存储结构转化为方法区中的运行时数据结构。
3、在内存中(具体是堆还是方法区由JVM具体实现决定)生成一个代表此类的java.lang.Class对象。
此对象作为方法区中的数据的访问入口。
但是如果是数组类呢?数组类是由JVM直接创建的,
但是毕竟还是要用到最内层的元素类型(Element Type)的类,所以与类加载器由密切关系。
数组的创建过程:
1、如果该数组类的组件类型(Component Type,指该数组去掉一个维度的类型)
是引用类型,那就递归的去加载这个组件类型。
该数组类会和加载它的组件类型的类加载器关联(类的唯一性由它本身和它的类加载器一起确定)。
2、如果组件不是引用类型,JVM会将该数组类和引导类加载器(Bootstrap ClassLoader)关联。
3、数组类的可见性(访问权限)和它的组件类型一致。
如果组件类型不是引用类型,那么访问权限默认为public。
加载阶段和链接阶段是交叉进行的,还有可能加载阶段尚未完成,链接阶段就已经开始了。
2、验证
确保字节流中的内容是符合规范的,是JVM安全性的保证之一。
1、文件格式验证
验证字节流符合Class文件规范。
包括:魔数、版本号、常量池常量类型、索引值的指向等。
2、元数据验证
对字节码进行语义分析,保证其信息符合Java语言规范的要求。
包括:类是否有父类(唯一根类要求)、父类是否允许被继承(继承关系的正确性)、
非抽象类是否实现了其父类或接口中要求实现的所有方法(abstract方法)、类的字段是否冲突等。
3、字节码验证
通过数据流和控制流分析,对类的方法体进行校验分析,确保被验证类的方法没有安全隐患。
JDK1.6后加入了StackMapTable属性,描述了方法体中所有基本块(Basic Block,按照控制流拆分的代码块)
开始时本地变量表和操作数栈应有的状态,用于辅助验证。
包括:任意时刻操作数栈的数据类型是否和字节码指令匹配,跳转指令的跳转位置是否恰当, 类型转换是否有效等。
4、符号引用验证
发生在JVM将符号引用转换为直接引用的时候,在解析阶段中发生。对类自身以外的信息进行匹配项校验。
包括:符号引用中全限定名是否能找到指定类、
类中描述符和简单名描述的方法和字段是否存在、符号引用中的类、方法、字段是否能被访问等。
3、准备
正式为类变量(static)分配内存及设定初始值。类变量使用的内存在方法区中分配。
类变量的初始值就是把内存区域置零,除非类变量为编译时常量。
编译时常量会在字段属性表中有ConstantValue属性记录它在编译期确定的值,此时它能被直接初始化为该值。
4、解析
将常量池中的符号引用替换为直接引用的过程。
符号引用Symbolic References:用符号来描述所引用的目标,
与虚拟机的内存布局无关,引用目标并不一定已经加载到内存中。
直接引用Direct Reference:直接指向目标所在地址的指针、相对偏移量或间接定位的句柄。
和虚拟机内存布局相关。
没有规定解析阶段发生的具体时间,但是在执行操纵符号引用的字节码指令之前,先要对它们使用的符号引用进行解析。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符等7类符号引用进行。
就是CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、
CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、
CONSTANT_MethodHandle_info、CONSTANT_InvokeDynamic_info 这7个常量类型。
1、类或接口解析 类D 要把符号引用N 解析为对类或接口C的直接引用
1、如果C不是数组类型,那么JVM会将N中C的全限定名传递给D的类加载器,让其去加载C。
加载过程可能触发其他类或接口的加载,如C的父类或接口。
2、如果C是数组类型,并且C的元素类型为对象,也就是N类似于"[Ljava/lang/String"的形式,
那么会以(1)的方式来加载元素类型,如"java.lang.String"。
接着JVM生成一个C类型和维度的数组对象。
3、上面两部无异常,则会进行最后一步:符号引用验证,验证C能被D访问(访问权限)。
不满足则抛出java.lang.IllegalAccessError异常。
2、字段解析
首先会对字段表内class_index项中的索引CONSTANT_Class_info进行解析,确定字段所属的类。
用C表示字段的类:
1、如果C中存在简单名和字段描述符都与目标字段匹配的字段,那么,返回该字段的直接引用,查找结束。
2、否则,到C的接口树上找。找到(简单名、字段描述符)则返回。
3、否则,到C的继承链上找。找到(简单名、字段描述符)则返回。
4、否则,找不到啦,直接报错!java.lang.NoSuchFieldError。
同样,最后也要验证访问权限。不满足则抛出java.lang.IllegalAccessError异常。
3、类方法解析
首先确定方法所属的类,即对方法表内class_index项中的索引CONSTANT_Class_info进行解析。
C表示方法所属类:
1、类方法和接口方法的符号引用的常量类型是分开的,
分别是CONSTANT_Methodref_info和CONSTANT_InterfaceMethodref_info,
如果发现方法表内class_index项中的索引CONSTANT_Class_info指向的是一个接口,
则抛出java.lang.IncompatibleClassChangeError。
2、否则,如果C中存在简单名和方法描述符都与目标方法匹配的字段,
那么,返回该方法的直接引用,查找结束。
3、否则,到C的接口树上找。找到(简单名、方法描述符)则返回。
4、否则,到C的继承链上找。找到(简单名、方法描述符)则返回。
5、否则,找不到啦,直接报错!java.lang.NoSuchMethodError。
同样,最后也要验证访问权限。不满足则抛出java.lang.IllegalAccessError异常。
4、接口方法解析
首先确定方法所属的接口,即对方法表内class_index项中的索引CONSTANT_Class_info进行解析。
C表示方法所属接口:
1、类方法和接口方法的符号引用的常量类型是分开的,
分别是CONSTANT_Methodref_info和CONSTANT_InterfaceMethodref_info,
如果发现方法表内class_index项中的索引CONSTANT_Class_info指向的是一个类,
则抛出java.lang.IncompatibleClassChangeError。
2、否则,如果C中存在简单名和方法描述符都与目标方法匹配的字段,
那么,返回该方法的直接引用,查找结束。
3、否则,到C的接口树上找。找到(简单名、方法描述符)则返回。
4、否则,找不到啦,直接报错!java.lang.NoSuchMethodError。
接口默认都是public的,不存在访问问题,因此不会抛出java.lang.IllegalAccessError异常。
5、初始化
类加载的最后一步。到了初始化,才真正开始执行Java字节码。
在准备阶段,类在分配类变量内存时被初始化了一次,那是为了让类变量的初始值满足系统要求。
而初始化阶段,是为了让类变量(static)的初始值满足程序员预先定义的初始值。
初始化可以看作是执行类构造器<clinit>的过程。
<clinit>方法特点:
1、<clinit>方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块(static{})
中的语句合并产生的。收集顺序按在源文件中的出现顺序决定。
静态语句块中,只能访问到定义在该静态语句块之前的变量,之后的可以赋值,但不能访问(不能取出值)。
2、与构造函数不同,<clinit>方法不用显示调用父类构造器,
因为父类的初始化一定在子类方法之前完成,第一个执行<clinit>方法的类肯定是Object类。
3、由于父类的<clinit>方法先执行,因此父类的类变量和静态语句中的语句先被执行。
4、<clinit>方法不是必须的,
如果没有类变量的赋值操做,也没有静态语句块,那么就不会生成<clinit>方法。
5、虚拟机要保证<clinit>方法在多线程环境下的线程安全性。
因此,如果多个线程同时初始化一个类,那么只有一个线程会去执行<clinit>方法。
如果<clinit>方法要耗费很长时间,则可能会造成多线程阻塞。
4、类加载器
通过类的全限定名来获取类的二进制字节流是放在虚拟机之外实现的,程序员可以自己决定怎么去加载。
1、类和类加载器的关系
每一个类加载器都有一个独立的类名称空间。
任意一个类,都要由它本身和加载它的类加载器一起来确定它在虚拟机中的唯一性。
因此,比较两个类是否相等,要在它们都是同一个类加载器加载的才有意义。
2、双亲委派模型
从虚拟机角度看,只有两种类加载器:
1、Bootstrap ClassLoader 启动类加载器
JVM一部分,用于JVM启动时的依赖类加载。
2、其他类加载器
不属于JVM,都继承自java.lang.ClassLoader。
从开发人员角度:
1、Bootstrap ClassLoader 启动类加载器
加载<JAVA_HOME>\lib目录下,
或者被-Xbootclasspath参数指定的目录下的虚拟机识别(仅按照文件名识别)的类库。
启动类加载器无法被Java程序直接引用。
2、Extension ClassLoader 扩展类加载器
加载<JAVA_HOME>\lib\ext目录下,
或者被java.ext.dirs系统变量所指定的路径下的所有类库。
开发人员可直接使用。
3、Application ClassLoader 应用程序类加载器
也叫系统类加载器,因为它是ClassLoader类中getSystemClassLoader() 方法的返回值。
负责加载用户类路径ClassPath上指定的类库,开发人员可直接使用。
应用程序如果没有自定义自己的类加载器,那么默认就是这个。
双亲委派模型:
当一个类加载器收到加载请求时,它把请求委托给它的父类加载器去完成,
直到父类无法完成该请求时,它才会尝试自己去完成。
【职责链设计模式】:事件沿职责链往上走,直到遇到能完成它的类。
这里也差不多,只是变成了直到遇到不能完成它的类。
双亲委派有什么好处?
因为类的唯一性要由类加载器参与确认,因此如果我们用不同类加载器加载一个Class文件,那么会产生不同的类。
对于java.lang.Object这些底层的类而言,就很要命了。。。
JVM中出现了很多职责行为一样,但是却是不同的类。混乱了!唯一根类也没办法满足了。
双亲委派模型使得Java类和它的类加载器一起具备了一种带优先级的层级关系。
<JAVA_HOME>\lib下的类都是由Bootstrap ClassLoader加载的,在程序中只会有一份。
<JAVA_HOME>\lib\ext下的类都是由Extension ClassLoader加载的,也只有一份。
用户指定类路径上的都是由Application ClassLoader加载。
双亲委派模型实现:
3、破坏双亲委派模型
历史上的三次破坏:
1、双亲委派模型在JDK1.2才引入,之前就有很多代码是继承ClassLoader而没有实现双亲委派的。
2、由于模型缺陷。
在一些集成架构中(如JDBC、JNDI),架构的主体是在Java JDK类库中,
由Bootstrap ClassLoader来加载。
但是,架构的具体的模块却是由独立厂商实现并部署在应用程序的ClassPath下的。
由Bootstrap ClassLoader加载的代码要调用应用程序的ClassPath下的代码,
怎么办?Bootstrap ClassLoader不能加载这些代码啊。。
因此,引入了Thread Context ClassLoader线程上下文类加载器。
Thread Context ClassLoader可由java.lang.Thread类的setContextClassLoader() 方法进行设置,
如果该线程没有设置,那么它会从父线程那里继承。
如果全局都没有设置,那么默认值是Application ClassLoader。
现在执行过程就变成了:
1、架构主体代码由Bootstrap ClassLoader来加载。
当要加载厂商模块时:
2、先一个方法将线程的类加载器设置为自己想要的类加载器,并保存线程之前的类加载器。
3、加载时,用Thread.currentThread.getContextClassLoader()方法取得类加载器。
4、加载完成,将线程的类加载器还原。
3、为了实现热替换HotSwap
即插即用,热部署。OSGi模块化标准。
每一个程序模块(OSGi称为Bundle)都有一个自己的类加载器,
当要替换一个Bundle时,连同它的类加载器一起替换。
OSGi类加载委派模型:
1、以java.*开头的类委派给父类加载器。
2、否则,将委派列表名单内的类委派给父类加载器加载。
3、否则,将import列表中的类委派给export这个类的Bundle的类加载器加载。
4、否则,查找当前Bundle的ClassPath,使用自己的类加载器加载。
5、否则,查找类是否在自己的Fragment Bundle中,如果在,则委托给Fragment Bundle的类加载器加载。
6、否则,查找Dynamic Import列表的Bundle,委派给对应的Bundle的类加载器加载。
1、2仍然符合双亲委派,其余都是平级查找。
出处:https://www.cnblogs.com/lqblala/p/15141415.html