深入理解JVM——读书笔记(虚拟机字节码执行引擎)

类文件结构

Class文件的结构

  • class文件是以一组8个字节为基础单位的二进制流,中间没有添加任何分隔符,从数据结构上看,class文件是一个类C++结构体的数据结构。

  • class文件只有两种数据类型:无符号整数,表。表可以嵌套表与无符号整数,无符号整数由u1,u2,u4,u8来表示。*_info表示表类数据类型。

  • 整个Class文件可以被视作下表,按照下表进行严格排列

  • magic

    • 魔数,用来标识文件类型。类似于扩展名。Class文件的魔数是0xCAFEBABE
  • minor_version

    • 次版本号,一般为0,预留给大版本之间的“功能预览”小版本
  • major_version

    • 主版本号,从45开始,从JDK1.1之后每个Java版本,主版本号+1。JDK1.1用的是45.0~45.3
    • Java虚拟机只能运行比它版本号低的Class文件,JDK8的主版本号是52
  • constant_pool

    • 容量(大小)为constant_pool_count-1

    • 常量池存放两大类常量:字面量、符号引用

      • 字面量:文本字符串、被声明为final的常量值等
      • 符号引用
        • 被模块导出或者开放的包(Package)
        • 类和接口的全限定名
        • 字段的名称和描述符(描述符是特定规则的字符串,用来描述数据类型、参数列表、返回值)
        • 方法的名称和描述符
        • 方法句柄和方法类型
        • 动态调用点和动态常量
      • 常量池中的每一个常量都是一个表,现在共有17种不同结构的常量类型。但是它们的起始字段都是u1的标志位,代表属于哪种常量(类似Class文件开头的魔数)

  • access_flags

    • 访问标志,识别类或者接口层次的信息

  • this_class、super_class、interfaces

    • 类索引、父索引、接口索引集合
  • fields

    • 类级别变量和实例级别变量(就是类属性和对象属性),但是不包括局部变量
    • image-20200812150635252
    • fields修饰了变量的类型、并发可见性、作用域、可否被序列化、字段名称等
  • methods

    • 与fields类似,这里只特别指出方法体里面的代码被存储在自带属性表的code属性中
  • attributes

    • Class文件、字段表、方法表都可以携带自己的属性表集合,以描述某些场景专有的信息

    • Code属性

      • Java程序方法体里面的代码经过Javac编译器处理之后,最终变为字节码指令存储在Code属性
      • code_length的实际上限不超过2个字节长,所以太长的jsp可能会编译失败
      • Javac把对this关键字的访问转变成一个普通方法参数的访问
    • ConstantValue属性

      • 通知虚拟机自动为静态变量赋值

字节码指令

  • 操作码长度为一个字节,所以最多不超过256条操作指令

  • 大部分的指令都包含数据类型,比如:iconst,lconst,fconst,但是由于操作指令容量有限,所以很显然

    一些指令对应了多种数据类型,比如byte,short就会被自动符号扩展为intboolean和char零位扩展为int

  • 具体的指令集在这里省略,具体可以参见《Java虚拟机规范》

公有实现与私有化

  • 将输入的Java虚拟机代码在加载时或执行时翻译成另一种虚拟机的指令集
  • 将输入的Java虚拟机代码在加载时或执行时翻译成宿主机处理程序的本地指令集(即时编译器代码生成技术)

虚拟机类加载机制

  • 类加载

    • 描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载机制
  • 类的生命周期

    • 加载

      • 通过全限定类名来获得二进制字节流
      • 将字节流转化为方法区的运行时数据结构
      • 在内存中生成一个代表这个类的Java.lang.Class对象,作为方法区这个类的各种数据的访问入口
    • 验证

      • 确保Class文件的字节流包含的信息符合《Java虚拟机规范》,进行字节码级别的校验
    • 准备

      • 为类中定义的变量(静态变量)分配内存并设置类变量初始值的阶段。
    • 解析

      • 将常量池内的符号引用替换成直接引用的过程(也就是把类外的引用,从符号级别变到地址级别)
        • 这一步可以理解成C++的“链接"
    • 初始化

      • 通过程序编码制定的主观计划去初始化类变量和其他资源
      • 类构造器<clinit>()
        • <clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作静态语句块(static{}块)中的语句合并产生的
        • Java虚拟机会保证在子类的<clinit>()方法执行前,父类的<clinit>()方法已经执行完毕。因此在Java虚拟机中第一个被执行的<clinit>()方法的类型肯定是java.lang.Object
        • 接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成<clinit>()方法;
        • 执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法,因为只有当父接口中定义的变量被使用时,父接口才会被初始化
  • 类加载器

    • 类加载器实现了类加载阶段中的“整个加载阶段“,自定义类加载器可以灵活处理了“如何得到二进制流”这一步

    • 通过继承抽象类ClassLoader并实现loadClass方法来自定义类加载器

      • 本质是解析某种数据源,得到byte[],通过内置函数defineClass(),完成从byte[]到class对象的转化。
    • 都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间

    • 双亲委派模型

      • 在Java虚拟机的角度来看,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现[插图],是虚拟机自身的一部分;另外一种就是其他所有的类加载器,这些类加载器都由Java语言实现,并且全都继承自抽象类java.lang.ClassLoader
    • 双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此

    • 上下文加载器实现”向下加载“

虚拟机字节码执行引擎

  • 运行时栈帧结构
    • JVM以方法作为最基本的执行单元
      • 每一个方法从调用开始至执行结束的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程
      • 每一个栈帧都包括了局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息
      • 一个栈帧需要分配多少内存,并不会受到程序运行期变量数据的影响,而仅仅取决于程序源码和具体的虚拟机实现的栈内存布局形式(编译期确定栈帧大小)
      • 执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作
    • 局部变量表
      • 一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量
    • 操作数栈
      • 当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈和入栈操作。
      • 简单来说,运算过程中产生的中间数据、结果都存放在操作数栈上
    • 动态连接
      • 每个栈帧都包含一个指向运行时常量池[插图]中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接
    • 方法返回地址
  • 方法调用
    • 方法调用并不等同于方法中的代码被执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还未涉及方法内部的具体运行过程
    • 分派
      • 静态分派(编译期决定)
        • 所有依赖静态类型来决定方法执行版本的分派动作,都称为静态分派。最典型应用表现就是方法重载
      • 动态分派(运行期决定)
        • 重写
  • 动态类型语言
    • 运行时确定类型,并且忽视了运行时确定的类型与变量静态类型不一致时的”冲突“
    • “变量无类型而变量值才有类型”
  • 基于栈的字节码解释执行引擎
posted @ 2020-08-14 13:56  Aackkom  阅读(181)  评论(0编辑  收藏  举报