1.概述
- Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载机制。
- Java的动态扩展的语言特征就是依赖运行期动态加载和动态链接实现的。
2.类加载时机
加载生命周期如下:
- 加载,验证,准备,初始化都严格按照顺序执行,解析在一些情况下可以位于初始化后进行,原因是为了支持Java语言的运行时绑定。
- 《Java虚拟机规范》严格规定有且只有六种情况必须立即对类进行“初始化”。这些行为被称为对一个类型的主动引用。
- 遇到new,getstatic,putstatic或invokestatic字节码指令。
new关键字实例化对象;
读取或设置一个类型的静态字段;
调用一个类型的静态方法。 - 使用java.lang.reflect包的方法对类型进行反射调用。
- 如果父类没有进行初始化,优先初始化父类。
- 当虚拟机启动时,用户需要指定一个要执行的主类。
- 使用JDK 7新加入的动态支持时,如果一个java。狼。invoke。MethodHandle实例最后的解析结果为REF_getStatic,REF_putStatic,REF_invokeS他提出,REF_newInvokeSpecial四种类型的方法句柄,并且这些句柄对应的类没有初始化时。
- 当一个接口定义了JDK 8新加入的默认方式时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
- 遇到new,getstatic,putstatic或invokestatic字节码指令。
3.类加载的过程
1.加载
- 在加载阶段,Java虚拟机需要完成以下三件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流.
- 将这个字节流所代表的静态存储结构转化为方法区的运行数据结构.
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口.
- <<Java虚拟机规范>>并没有对着三点具体的要求.它没有指明二进制字节流必须得从某个Class文件获取,因此就形成了很多技术.
- 从ZIP压缩包中读取,JAR.EAR,WAR格式的基础.
- 从网络获取,Web Applet.
- 运行时计算生成,动态代理技术.
- 由其他文件生成,JSP文件.
- 数据库获取,中间服务器.
- 加密文件获取,防Class文件反编译保护措施.
- …
- 非数组类型的加载阶段,既可以使用Java虚拟机里内置的引导类加载器,也可使用用户自定义的类加载器.
- 数组类本身不通过类加载器创建,是由Java虚拟机直接在内存中动态构建,但其元素最终是靠类加载器来完成加载,需要遵循以下原则:
- 如果数组的组件类型是引用类型,那就递归采用本节中的加载过程去加载这个组件类型,数组C将被表示在加载该组件类型的类加载器的类名称空间上.
- 如果不是引用类型,Java虚拟机就会把数组C标记为与引导类加载器关联.
- 数组类的可访问性与它的组件类型的可访问性一致,如果组件类型不是引用类型,它的数组类的可访问性将默认为public,可被所有的类和接口访问到.
- 加载结束后二进制字节流就按虚拟机所设定的格式存储在方法区中,此区域的存储数据结构因具体虚拟机而定,在安置在方法区后,会在Java堆内存中实例化对象,并作为程序访问方法区中类型数据的外部接口.
- 加载阶段和连接阶段的部分动作是交叉进行的.但所属的归类还是不变的,具有先后顺序.
2.验证
- 目的:是确保Class文件的字节流中包含的信息符合<<Java虚拟机规范>>的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身安全.
- 大致的验证四个阶段:文件格式验证,元数据验证,字节码验证,符号引用验证.
- 文件格式验证
需要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理.
主要目的:保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个Java类型信息的要求. - 元数据验证
对字节码描述的信息进行语义分析,以保证其描述的信息符合<<Java语言规范>>的要求.
主要目的:对类的元数据信息进行语义校验,保证不存在与<<Java语言规范>>定义相悖的元数据信息. - 字节码验证
对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为.
主要目的:通过数据流分析和控制流分析,确定程序语义是否合法,符合逻辑.
但是检查没问题也不能代表安全的,“停机问题”----不能用一个程序准确的检查出程序是否能在有限的时间之内结束运行.解决方案(也存在篡改的可能)----StackMapTable属性中的记录是否合法即可,这样就由类型推导转变为类型检查. - 符号引用验证
该类是否缺少或者被禁止访问它依赖的某些外部类,方法,字段等资源.
主要目的:确保解析行为能正常执行,如果无法通过符号引用验证,Java虚拟机将会抛出一个java.lang.IncompatibleClassChangeError的子类异常.
3.准备
- 准备是正式为类中定义的变量分配内存并设置类变量初始值的阶段.
4.解析
Java虚拟机将常量池内的符号引用替换为直接引用的过程.
- 符号引用:符号引用可以一组符号来描述所引用的目标,符号可以是任何形式的自变量,只要使用时能无歧义地定位到目标即可.
- 直接引用:直接引用是可以直接指向目标的指针,相对偏移量或者是一个能间接定位到目标的句柄.
- 除invokedynamic指令外,虚拟机可以对第一次解析的结果进行缓存.一次成功,后续成功,一次失败,同样失败.
- 解析动作主要针对:类,接口,字段,类方法,接口方法,方法类型,方法句柄和调用点限定符.
5.初始化
在准备阶段时,变量已经赋值过一次系统要求的初始零值,而在初始化阶段,则会根据程序员通过程序编码指定的主观计划去初始化类变量和其他资源,初始化阶段就是执行类构造器<clinit>()
方法的过程.
6.类加载器
Java虚拟机设计团队有意把类加载阶段中的"通过一个类的全限定名来获取描述该类的二进制字节流"这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类.实现这个动作的代码被称为"类加载器".
1.类与类加载器
比较两个类是否"相等",只有在两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等.
2.双亲委派模型
-
站在Java虚拟机的角度来看,只存在两种不同的类加载器:一种是启动类加载器,这个类加载器使用C++语言实现,是虚拟机自身的一部分;另外一种就是其他所有的类加载器,这些类加载器都由Java语言实现,独立存在于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader.
-
站在Java开发人员的角度来看,三层类加载器,双亲委派的类加载架构.
- 启动类加载器(Bootstrap ClassLoader):前面已经大致介绍过了,这个类加载器负责将存放在 <JRE_HOME>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可。
- 扩展类加载器(Extension ClassLoader):这个类加载器是由
sun.misc.LauncherExtClassLoader
实现的。它负责将<JAVA_HOME>/lib/ext
或者被java.ext.dir
系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。 - 应用程序类加载器(Application ClassLoader):这个类加载器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此一般称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
- 工作流程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此。因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
- 好处:使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一
7.Java模块化系统
1.模块的兼容性
- JDK9开始引入,目的:为了能够实现模块化的关键目标:可配置的封装隔离机制。
- 包含内容:
1.依赖其他模块的列表;
2.导出的包列表,即其他模块可以使用的列表;
3.开放的包列表,即其他模块可反射访问模块的列表;
4.使用的服务列表;
5.提供服务的实现列表; - 可配置的封装隔离机制要解决的问题:
1.解决JDK 9之前基于类路径来查找依赖的可靠性问题;
2.解决原来类路径上跨JAR文件的public类型的可访问性问题. - 模块的兼容性
为了使可配置的封装隔离机制能够兼顾传统的类路径查找机制,JDK 9提出了与"类路径"相对应的"模块路径". - 模块化系统将按照以下三种规则来保证使用传统类路径依赖的Java程序可以不经修改地直接运行在JDK9及以后的版本中:
1.JAR文件在类路径的访问规则
2.模块在模块路径的访问规则
3.JAR文件在模块路径的访问规则
- 除了向后兼容外,随着JDK 9模块化系统的引入,更值得关注的是它本省面临的模块间的管理和兼容性问题:如果同一个模块发行了多个不同的版本,那只能由开发者在编译打包时人工选择好正确版本的模块来保证依赖的正确性.Java模块化系统目前不支持在模块定义中加入版本号来管理和约束依赖,本身也不支持多版本号的概念和版本选择功能.
2.模块化下的类加载器
- 改动:
1.扩展类加载器被平台类加载器取代.
2.平台类加载器和应用程序类加载器都不在派生java.net.URLClassLoader,如果有程序直接依赖了这种继承关系,或者依赖了URLClassLoader类的特定方法,那代码很可能会在JDK 9及更高版本的JDK中崩溃. - 启动类加载器现在是在Java虚拟机内部和Java类库共同协作实现的类加载器,尽管有了BootClassLoader这样的Java类,但为了与之前的代码保持兼容,所有在获取启动类加载器的场景中任然会返回null来代替,而不会得到BootClassLoader的实例.