深入理解Java虚拟机 ch7 虚拟机加载机制 读书笔记

来源:互联网 发布:mac自带pages 编辑:程序博客网 时间:2024/05/18 05:01

part3 虚拟机执行子系统

  本部分讲虚拟机的执行过程所涉及到的一些问题。这部分详细地说明了Java是如何实现平台无关的:JVM和字节码存储格式。通过设计一个统一的Class文件标准去存储字节码(JVM指令集,符号表及其他辅助信息),并制定规范进行语法和结构化约束,使用JVM的执行引擎去进行解释执行,最终实现平台无关。

  虚拟机执行整个流程:首先,由编译器将java文件编译成Class文件,然后通过一整个类加载过程,将Class文件加载到内存的方法区,最终由执行引擎对字节码指令进行解释执行

ch7 虚拟机加载机制

  上一章的内容是讲Class文件的结构,根据这个结构,可以知道如何将一个类/接口编译成一个Class文件。本章的内容是讲将Class文件加载入JVM的细节。下章的内容则是使用JVM字节码执行引擎对JVM中的栈进行解释执行的细节。另外需要说明的是,一下内容如无特殊说明,也包括接口

  这里总结一下类加载的整个流程。首先,当遇到对类进行主动引用的5种情形时,说明需要进行整个类加载过程。下面,根据类的全限定名找到Class文件,进行文件格式验证。验证通过后,将Class文件转化为方法区的运行时数据结构,并生成Class对象作为类数据的访问入口。然后,对数据进行元数据验证字节码验证,分别验证书序类型和类的方法体。接着,在准备阶段类变量分配内存和设置初始值(零值)。再然后,将常量池中符号引用替换为直接引用,并执行符号引用验证。最后,在初始化阶段执行类构造器<clinit>()方法。

一 基本概念

  类加载的原因:Class文件中描述的各种信息,最终都需要加载到虚拟机中才能运行和使用。

  类加载机制内容:虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转化解析和初始化,并最终形成可以被虚拟机直接使用的Java类型的整个过程。

  时间:具体时机由下一节讲述,这边说明的是,Java中类型的加载连接初始化过程都是在程序运行期间完成的,从而增加灵活性,实现动态扩展

二 类加载时机

  类的生命周期可以分为七个阶段:加载验证准备解析初始化使用卸装。其中,验证、准备和解析这3个阶段统称为连接

  其中,加载、验证、准备、初始化和卸装这5个部分的开始时间是有序的,中间会有所交叉解析阶段有可能在初始化之后再开始——以支持运行时绑定

  Java虚拟机规范并没有规定加载的时机,但是规定了初始化的时机。二加载、验证和准备阶段则在此之前。Java中必须要对类进行初始化的5个情形:

  1. 遇到newgetstaticputstaticinvokestatic这4挑字节码指令时,若类未初始化则需要触发其初始化。也就是实例化对象,读取或设置类静态字段(常量除外)和调用类的静态方法时。

  2. 使用java.lang.reflect包的方法对类进行反射调用时。

  3. 当初始化一个类时,若其父类未初始化,先触发其父类初始化。接口的父接口无此规定。

  4. JVM启动时,用户需指定一个要执行的主类(含main()),JVM会先初始化该类。

  5. 使用JDK1.7的动态语言支持时,若一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,且该句柄对应的类没有初始化过,需要先触发其初始化。

  有且只有以上5种场景会触发类进行初始化,以上5种情形被统称为对一个类进行主动引用。除此以外引用类的方式不会触发类的初始化,被统称为被动引用,下面给出被动引用的3种情形,需要注意比较有迷惑性:

  1. 通过子类引用父类的静态字段->不会导致子类初始化

  2. 通过数组定义来引用类->不会引发该类的初始化

  3. 引用一个类的常量->不会引发该类初始化

三 类加载过程

  类加载的全过程共分为5个阶段:加载验证准备解析初始化

3.1 加载

  加载是类加载的第一个阶段,这里不要混为一谈。在加载阶段,虚拟机需要完成以下三件事:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流(Class文件)

  2. 将这个字节流所代表的静态存储结构转化为方法区运行时数据结构

  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

  这里的3个要求比较宽泛,发挥的余地较大。首先谈获取位置获取方式获取位置非常多样化,可以是ZIP包、网络、数据库等等;获取方式是通过类加载器类加载器可以大致分为两种:引导类加载器自定义类加载器。这里,是开发人员可控性最强的地方。

  加载完成后,JVM外部的Class文件就按照VM规范存储在方法区中。此外,方法区中还有个Class对象用以标识该类数据的访问入口。注意,Class对象并不在Java堆中。

3.2 验证

  目的:确保Class文件的字节流中包含的信息符合当前虚拟机的要求,且不会危害虚拟机自身的安全。说白了,就是进行可用性分析和可靠性验证,保证其能用,不使坏。

  需要说明的是,验证阶段肯定是在加载阶段开始之后再开始的,但不一定是在加载阶段结束之后开始的,会交叉进行。

  验证阶段主要会完成下面4个验证动作:

  1.文件格式验证:本阶段验证字节流是否符合Class文件格式的规范,能否被当前版本虚拟机处理。该阶段是针对二进制字节流进行验证的,只有通过该步,字节流才会进入方法区;后面3步是针对方法区的存储结构进行的,该动作在加载的第一步和第二步中间完成;

  2.元数据验证:对类的元数据信息进行语义校验,确保其符合Java语言规范。

  3.字节码验证:验证过程中最复杂阶段,对类的方法体进行校验分析。为简化,在JDK1.6后在Code属性的属性表中加入StackMapTable属性,只要对其进行类型检查即可,其正确性和安全性还需加强。

  4. 符号引用验证:该步发生在解析阶段——将符号引用转化为直接引用时。该步校验通过对类自身以外的信息进行匹配性校验,确保解析动作能正常执行。

  最后需要说明的是,验证阶段并非必须的,如果代码已经被反复验证过,或确保安全,可以通过关闭验证措施来缩短虚拟机类加载的时间。

3.3 准备

  准备阶段是为类变量服务的,包括为类变量分配内存设置初始值。首先说明的是,这里提到的是类变量,会被分配在方法区;而实例变量会在对象实例化时随对象被分配在Java堆中。其次,通常情况下,这里的初始值是数据类型的默认值,特殊情况是指其为static final时,为其定义值。对类变量的putstatic指令在初始化阶段才会执行。

3.4 解析

  该阶段负责将常量池内的符号引用替换为直接引用

  符号引用:用一组符号来描述所引用的目标,可以是任何形式的字面量,能无歧义的定位到目标即可;

  直接引用:直接指向目标的指针、相对偏移量或能间接定位到目标的句柄;其引用目标变得已经在内存中存在。

  解析分以下7种:

  1. 类或接口的解析

  2. 字段解析

  3. 类方法解析

  4. 接口方法解析

  5. 方法类型解析 ; 6. 方法句柄 ;7. 调用点限定符

3.5 初始化

  初始化阶段是执行类构造器<clinit>()的过程。是整个类加载过程中真正开始执行Java程序代码的过程,当然,只执行了类构造器部分,只做了这么点微小的工作。下面详述类构造器<clinit>()

  1. <clinit>()是有编译器自动手机类中所有类变量的赋值动作静态语句块合并产生的,收集顺序由源代码顺序为准。

  2.<clinit>()与类的构造函数(也就是实例构造器<init>()不同。且先父类再子类,也就是第一个肯定为java.lang.Object。

  3. 接口中执行<clinit>()不一定要先执行父接口;而且,接口的实现类在初始化时也不一定会执行接口的<clinit>()方法。只有当用到其定义的变量时,才会执行。

  4. VM保证一个类的<clinit>()在多线程环境中会被正确地加锁、同步。

四 类加载器

  整个类加载过程中,唯一在JVM外部实现的动作为通过一个类的全限定名来获取描述此类的二进制字节流。该动作由类加载器ClassLoader完成,类加载器在类层次划分、OSGi、热部署、代码加密等领域大放异彩。

4.1 类&类加载器

  对于任意一个类,需要由其类加载器类本身确定其在JVM中的唯一性;每一个类加载器,都拥有一个独立的类名称空间。也就是说,由不同ClassLoader加载的同一个类,并不相等。

4.2 双亲委派模型

  从JVM角度来看,类加载器只分为两种:启动类加载器其他类加载器。前者由C++实现(HotSpot JVM中),是JVM的一部分;后者由Java语言实现,位于JVM外部,全部继承自ClassLoader类。

  从Java开发人员角度看,系统提供以下3种类加载器:

  1. 启动类加载器Bootstrap ClassLoader:负责加载\lib目录中类库

  2. 扩展类加载器Extension ClassLoader:负责加载\lib\ext目录中类库

  3. 应用程序类加载器Application ClassLoader:负责加载用户类路径上类库

  Java设计着推荐的类加载器的层次关系为双亲委派模型,这里类加载器直接的父子关系是通过组合实现的,该模型并非强制。

图片来自:http://blog.csdn.net/u011080472/article/details/51332866” title=”” /></p><h4 id=  双亲委派模型的工作过程:当类加载器受到类加载请求,会将其请求往上传递,直至启动类加载器,当其无法加载时,再交给其子类加载器,以此类推。这样保证Java内置类库始终由特定类加载器加载,不会生成多个不同的类对象。

  这里,出个典型题目:能不能自己写个类叫java.lang.System?

  答案:通常不可以,但可以采取另类方法达到这个需求。 为了不让我们写System类,类加载采用委托机制,这样可以保证爸爸们优先,爸爸们能找到的类,儿子就没有机会加载。而System类是Bootstrap加载器加载的,就算自己重写,也总是使用Java系统提供的System类,自己写的System类根本没有机会得到加载。

  但是,我们可以自己定义一个类加载器来达到这个目的,为了避免双亲委托机制,这个类加载器也必须是特殊的。由于系统自带的三个类加载器都加载特定目录下的类,如果我们自己的类加载器放在一个特殊的目录,那么系统的加载器就无法加载,也就是最终还是由我们自己的加载器加载。

4.3 OSGi

  OSGi是重要的Java模块化标准,OSGi实现模块化热部署的关键在于其自定义的类加载机制的实现。其每一个程序模块Bundle都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle联通类加载器一起换掉以实现代码的热替换

阅读全文
0 0