深入理解JVM三:虚拟机执行子系统

代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言的一大步

一、无关性的基石

实现语言无关性的基础仍然是虚拟机和字节码存储格式,Java虚拟机不和包括Java在内的任何语言绑定,它只与Class语言这种特定的二进制文件格式所关联。任一门功能性语言都可以表示为一个能被Java虚拟机所接受的有效的Class文件。

1.1 Class类文件的结构

Class文件是一组以8位字节为基础单位的二进制流,当遇到8位字节以上的数据项,则按照高位在前的方式分割成若干个8位进行存储。只有两种数据类型:无符号数和表

  • 无符号数
    以u1、u2、u4、u8分别代表1个字节,2、4、8个字节的无符号数,可以用来描述数字、索引引用、数量值、UTF-8字符串。

  • 由多个无符号数或者其他表作为数据项构成的复合数据类型,习惯已"_info"结尾,整个Class文件本质上就是一张表

1.2 魔数与Class文件的版本

每个Class文件的头4个字节称为魔数,唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。固定为"0xCAFEBABE",咖啡宝贝。
紧接着魔数的4个字节存储的是Class文件的版本号:第五和第六个字节是次版本号(Minor Version),第七和第八个字节是主版本号(Major Version)。高版本JDK能向下兼容以前版本的Class文件,但拒绝执行以后的版本文件。

1.3 常量池

版本号之后便是常量池,包含两大类常量

  • 字面量
    比较接近Java语言层面的常量概念,文本字符串,final常量值
  • 符号引用
    属于编译原理的概念
    • 类和接口的全限定名
    • 字段的名称和描述符
    • 方法的名称和描述符
      由于Class方法等都需要引用CONSTANT_Utf8_info型常量来描述名称,所以该常量的最大长度就是Java中方法、字段名的最大长度,就是该常量的Length的最大值,65535,就是64KB。

1.4 访问标志

常量池结束后,紧接着两个字节代表访问标志,表示是类还是接口,是否为public,abstract,final等。

二、虚拟机类加载机制

2.1 概述

虚拟机把描述类的数据从Class文件加载到内存中,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
依赖运行期动态加载和动态连接

2.2 类加载的时机

类的整个生命周期包括:

加载
验证
准备
解析
初始化
使用
卸载

其中验证、准备、解析3个部分统称为连接,加载必须按部就班,但是解析则不一定,可以在初始化之后再开始。虚拟机规范严格规定了有且只有5种情况必须立即对类进行初始化

  • 遇到new、getstatic、putstatic、invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。Java场景是:使用new实例化对象、读取或设置一个类的静态字段,调用一个类的静态方法。
  • 使用java.lang.reflect包的方法对类进行反射调用的时候。如果类没有进行过初始化,则需要先触发其初始化
  • 初始化一个类的时候,如果父类还没有初始化,则先触发其父类的初始化
  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类)虚拟机会先初始化这个主类
  • 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

细节问题:

  • 对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。
  • 通过数组来引用类,不会触发此类的初始化
  • 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化
  • 接口与类真正有所区别的是,第三种,当一个类在初始化时,要求其父类全部都已经初始化化过了,但是一个接口在初始化时,并不要求其父类接口全部都完成了初始化,只有在真正使用到父类接口的时候(如引用接口中定义的常量)才会初始化

2.3 类加载的过程

2.3.1 加载

“加载”是“类加载”过程的一个阶段
将类的二进制流加载存储在方法区中,加载尚未完成,连接阶段可能已经开始,但是开始时间保持着固定的先后顺序

2.3.2 验证

这一阶段的目的是保证Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全

2.3.3 准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。
这里进行内存分配的仅包括类变量(static修饰)
public static int value = 123
这里的初始值是0而不是123,把value赋值为123的 putstatic指令是程序被编译后,所以是初始化阶段才会执行。

2.3.4 解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程

  • 符号引用:符号引用是一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位目标即可。
  • 直接应用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

  • 类或接口的解析
  • 字段解析
    • 如果C本身就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束
    • 否则,如果C中实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口
    • 否则,如果C不是java.langObject的话,将会按照继承关系从下往上递归搜索其父类
    • 否则,查找失败,抛出Java.lang.NoSuchFieldError异常
      如果查找过程中返回了引用,如果权限不对,将抛出java.lang.IllegalAccessError异常
      实际应用中,如果一个同名字段同时出现在C的接口和父类中,编译器可能会拒绝编译

2.3.5 初始化

类初始化是类加载过程的最后一步,到了初始化阶段,才真正开始执行类中定义的Java程序代码
初始化阶段是执行类构造器<clinit>()方法的过程。
<clinit>()方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块(static{})中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的。静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值但是不能访问

  • <clinit>()方法与类的构造函数不同,它不需要显示地调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类<clinit>()方法已经执行完毕。因此在虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object
  • 由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作
  • <clinit>()方法对于类或接口来说并不是必须的,如果一个类中没有静态语句块,也就没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法
  • 接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成<clinit>()方法。但接口与类不同的是,接口的<clinit>()方法不需要先执行父接口的<clinit>()方法,只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()。
  • 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只有一个线程会执行<clinit>()方法,其他线程阻塞等待,如果<clinit>()方法耗时很长,可能会造成进程阻塞。

2.4 类加载器

“通过一个类的全限定名来获取描述此类的二进制字节流” 实现这个动作的代码模块称为 类加载器
在类层次划分、OSGI、热部署、代码加密大放异彩
对于任意一个类,都需要由它的类加载器和类本身一同确立在Java虚拟机中的唯一性。

2.4.1 双亲委派模型

启动类加载器(虚拟机) 负责 <JAVA_HOME>\lib目录
拓展类加载器 开发者可以直接使用<JAVA_HOME>\lib\ext目录
应用程序类加载器 开发者可以直接使用 负责用户类路径所指定的类库

拓展类加载器
启动类加载器
应用程序类加载器
自定义类加载器
自定义类加载器

工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个请求都是如此,因此所有请求首先都会传送到启动类加载器,只有当父类无法完成加载时,子加载器才会尝试自己加载。

2.4.2 破坏双亲委派模型

  • 第一次 被破坏 JDK1.2之前,因为该模型在1.2之后才被引入,为了向前兼容,添加了新的protected方法findClass()
  • 第二次 如果基础类要回调用户的代码 - JNDI服务,引入了线程上下文类加载器,默认是应用程序类加载器。这个加载器,父类加载器请求子类加载器完成类加载
  • 第三次 OSGI环境下,类加载器不再是双亲委派树状结构,而是复杂的网状结构
    • 以java*开头的类委派给父类加载器
    • 委派列表名单内的类委派给父类架子啊器
    • 将Import列表中的类委派给Export这个类的Bundle的类加载器
    • 查找当前Bundle的ClassPath,使用自己的类加载器
    • 查找类是否在自己的Fragment Bundle中,如果在,则委派给对应Bundle的类加载器
    • 查找Dynamic import列表的Bundle
    • 类查找失败
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值