GitHub-理解JVM系列:

https://github.com/kevinten10/Effective-Java


一、类文件结构

Java虚拟机不和包括Java在内的所有编程语言绑定,它只和”class文件”这种特定的二进制文件格式所关联

class文件中包含了Java虚拟机指令集和符号集以及若干其他辅助信息

  • Java:*.java => javac => ↘
  • jruby:.rb => jrubyc => → 字节码 .class => Java虚拟机
  • groovy:… => groovyc => ↗

1. class类文件结构

  • 8位字节为基础单位的二进制流
  • 各个数据项目,严格按照顺序紧密排列,无任何分隔符
  • 若需占用8个字节以上,按高位在前的方式分割成若干8字节(最高位在地址最低处)
  • 只有两种数据类型
    • 无符号数:u1,u2,u3,u4代表1,2,3,4个字节
    • 表:由多个无符号数或其他表构成的复合类型

2. 字节码指令

  • 1字节长度+参数:2^8 = 256种
  • 虚拟机面向操作数栈,而不是寄存器,故大多数不包括操作数
  • 放弃操作数长度对齐
    • 优点:无需补齐,高效
    • 缺点:需要从字节中重建出具体数据的结构
  • 操作码只有一个字节
do{
  1. PC+1
  2. 根据PC位置,从字节码流中取出操作码
  3. if(操作数存在){取出操作数}
  4. 执行操作码
}while(字节码流>0)

3. class文件格式

classfile{
    u4              magic               魔数
    u2              minor_version       次版本号
    u2              major_version       主版本号
①☆ u2              constant_pool_count
②☆ cp_info         constant_pool    
    u2              access_flags        访问标志:类/接口层次的访问信息
    u2              this_class 类索引
    u2              super_class 父类索引
    u2              interfaces_count    实现接口数
③  u2              interfaces  
    u2              fields_count        成员变量
④☆ field_info      fields 
    u2              methods_count       方法
⑤☆ method-info     methods 
    u2              arttributes_count 
⑥☆ attribute_info  attributes 
}

* info结尾为表结构    

①常量池计数值
②常量表

  • 类和接口的全限定名
  • 字段的名称和描述符–>未初始化(运行时初始化),无具体信息
  • 方法的名称和描述符–>未初始化,无真正内容信息

③实现的接口
④字段表集合/成员变量 –> 类变量/实例变量
⑤方法表集合:方法描述, 重载方法–>不同特征签名
⑥属性表集合:class文件,字段表,方法表都可以携带自己的属性表集合

Java虚拟机指令设计

  • 将输入的虚拟机代码在加载或执行时,翻译成另一虚拟机的指令集
  • JIT,翻译成CPU本地指令集

二、虚拟机类加载机制

1. 类加载

懒惰加载机制:

有且仅有五种情况 => 立即初始化

  • new,读取/设置静态域,调用静态方法
  • 反射加载(.class加载时不初始化)
  • 初始化一个类,对其父类先初始化
  • 执行的主类在VM启动时
  • 动态方法句柄?

除此之外,所有引用类的方式都不会触发初始化–>“被动引用”

  • .class
  • 调用父类静态域,子类不会初始化
  • 接口初始化时,不要求父类接口初始化,调用时才初始化

类加载过程解析:

1. 加载

  • 通过类的全限定名,获取类的二进制字节流(.class文件)
  • 将字节流代表的静态存储结构—>方法区的运行时数据区
  • 在内存中生成此class对象,作为方法区的访问入口

2. 连接(与加载交叉进行)

2.1 验证

  • 是否符合虚拟机要求
  • 保护虚拟机安全
  • 是否有编译错误
文件格式验证:是否符合class文件格式,魔数、版本号、常量池类型….
元数据验证:是否符合Java语言规范,验证方法区中的语法语义
字节码验证:逻辑验证(最复杂)
符号引用验证:解析时发生

=>若确保不需要验证,可使用-Xverify:none关闭验证

2.2 准备

  • 为类变量分配内存并设置初始值(非初始化)
  • 设置为零值(内存全部置0),引用类型逻辑上为null
  • static final域根据定义设置初始值,如“123”

2.3 解析

  • 将常量池中的符号引用替换为直接引用的过程
    • 符号引用:字面量,class格式中,与内存无关
    • 直接引用:直接指向目标的指针(内存中)
  • 类接口解析
  • 字段解析
  • 类方法解析
  • 接口方法解析

3. 初始化

  • 初始化static域
  • 父类先于子类
  • 多线程同时初始化一个类,线程安全,只有一个执行其余阻塞

2. 类加载器

  • Java虚拟机外部
  • 通过类的全限定名获取类的二进制字节流
  • 每一个类加载器,都有一个独立的类名称空间
  • 比较类是否“相等”,只有同一个类加载器时才有意义(类似于命名空间的作用)

1. 双亲委派模型

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

启动类加载器

  • 是虚拟机的一部分,由C++实现
  • 加载 JAVA_HOME/lib 目录中的类

扩展类加载器

  • 加载 JAVA_HOME/lib/exb 目录中的类
  • 由Java实现

应用程序类加载器

  • 加载用户类路径 CLASSPATH 上指定的类库
  • 系统类加载器,程序中默认的类加载器
  • Java实现

类加载过程

  • 如果一个类加载器收到类加载请求,首先不自己去加载
  • 将请求委派给父类处理,所有请求都传送到顶层的启动类加载器
  • 只有父类加载器反馈自己无法完成这个加载请求(搜索范围没有找到所需的类)
  • 才会由子类加载器尝试去加载
优点:在不同程序中,启动类加载的同一个类,不随程序变化

3. 虚拟机字节码执行引擎

  • 虚拟机最核心的组成部分之一

运行时栈帧结构:stack frame (在虚拟机栈区域,线程私有)

1. 局部变量表

  • 变量值存储空间,存放方法参数,方法内部变量
  • 容量以变量槽slot为最小单位
    • 32位
    • 64位:32+32
  • 通过索引定位使用局部变量表:从0至最大slot数
    • 若是实例方法,则索引0为对象实例的引用(this)
  • slot可重用,节省空间
2. 操作数栈
  • 最大深度在编译时写入
  • 字节码指令指向栈中 存入/提取 内容,如算术运算
  • 元素类型要与字节码指令序列严格匹配

3. 动态连接

  • 方法区常量池中的方法引用,在运行期间转为直接引用
    • 句柄法
    • 直接指针法

4. 方法返回地址

  • 异常时返回地址(正常退出可以使用PC作为返回
  • 保存在异常快照中,故抛出异常会有较大的开销

方法调用 :确定被调用方法的版本

①解析

所有方法调用中的目标方法在class文件里面都是常量池中的符号引用
静态解析:在类加载的解析阶段,将符号引用替换为直接引用

②分派

静态分派—–重载

(引用类型)Human man = new Man()(内存类型);
Human为静态类型(编译器可知),重载时,根据静态类型进行分派

动态分派—–重写

Man为实际类型(运行期确定)

  • 建立了实际类型的内存空间
  • 在操作数栈顶的第一个元素的实际类型
  • 根据实际类型进行方法调用
动态类型语言:类型检查在运行期(变量本身没有类型,变量的值才有类型)
静态类型语言:类型检查在编译器(生成符号引用,保存到class中)

字节码解释执行引擎

源码–>词法分析–>抽象语法树–>指令流–>解释器–>解释执行


4. Tomcat类加载器架构

目录

①/common:被Tomcat与所有web程序可见
②/server:被Tomcat可见,web不可见
③/shared:web可见,Tomcat不可见
④/webapp/WEB-INF:此web可见,其他web不可见

①②③默认合并为/lib目录

类加载模型(双亲委派模型)

在Tomcat中,common类加载器将会加载/lib目录下的jar包,被所有web应用公用,而每个web应用的类加载器都是相互隔离的,故而每个web应用都有独立的命名空间。