乐趣区

关于后端:深入浅出JVM四之类文件结构

深入浅出 JVM(四)之类文件构造

Java 文件编译成字节码文件后,通过类加载机制到 Java 虚拟机中,Java 虚拟机可能执行所有符合要求的字节码,因而无论什么语言,只有可能编译成符合要求的字节码文件就可能被 Java 虚拟机执行

Java 虚拟机和字节码是语言、平台无关性的基石

本篇文章将深入浅出的解析字节码文件

无关性的基石

已经: 源代码 -> 通过编译 -> 本地机器码

Java: 源代码 -> 通过编译 -> 字节码 -> 解释器 -> 本地机器码

字节码: 与操作系统和机器指令集无关的, 平台中立的程序编译后的存储格局

字节码是无关性的基石

平台无关性的基石:

  1. 所有平台都对立反对字节码
  2. 不同的 Java 虚拟机都能够执行平台无关的字节码

因而实现了 一次编译, 到处运行

语言无关性的基石:

  1. Java 虚拟机
  2. 字节码

Java 虚拟机不是只能够执行 Java 源代码编译而成的字节码, 只有符合要求 (平安 …) 的字节码, 它都能够执行

因而 Kotlin… 等语言能够运行在 Java 虚拟机上

Class 类文件构造

文件格式存取数据的类型

  1. 无符号数 : u1,u2,u4,u8 代表 1,2,4,8 个字节的无符号数(能够示意数字,UTF- 8 的字符串, 索引援用 ….)
  2. 表: 由 n 个无符号数或 n 个表组成 (命名以_info 结尾)

初识 Class 文件格式

编写 Java 源代码

 public class Test {
     private int m;
     private final int CONSTANT=111;
 ​
     public int inc() throws Exception {
         int x;
         try {
             x = 1;
             return x;
         }catch (Exception e){
             x = 2;
             return  x;
         }finally{x = 3;}
     }
 }

应用可视化工具 classpy 查看反编译的后果

每个汇合前都有一个计数器来统计汇合中元素的数量

Class 文件格式的形容

数据类型 名称 数量 对应图中名字 作用
u4 magic 1 魔数 确定这个文件是否是一个能被虚拟机承受的 Class 文件
u2 minor_version 1 次版本号 虚拟机必须拒绝执行超过其版本号的 Class 文件
u2 major_version 1 主版本号 虚拟机必须拒绝执行超过其版本号的 Class 文件
u2 constant_pool_count 1 常量池容量计数器 统计常量数量
cp_info constant_pool constant_pool_count – 1 常量池 寄存常量
u2 access_flags 1 拜访标记 辨认类 (类, 接口) 的访问信息
u2 this_class 1 类索引 确定类的全限定名
u2 super_class 1 父类索引 确定父类的全限定名
u2 interfaces_count 1 接口计数器 统计该类实现接口数量
u2 interfaces interfaces_count 接口索引汇合 形容该类实现了的接口
u2 fields_count 1 字段表汇合计数器 统计类的字段数量
field_info fields fields_count 字段表汇合 形容类申明的字段(类变量, 实例变量)
u2 methods_count 1 办法表汇合计数器 统计类的办法数量
method_info methods methods_count 办法表汇合 形容类申明的办法
u2 attribute_count 1 属性表汇合计数器 统计属性数量
attribute_info attributes attributes_count 属性表汇合 形容属性

魔数与主次版本号

  • 魔数: 确定这个文件是否为一个能被虚拟机承受的无效 Class 文件
  • 主次版本号: 虚拟机拒绝执行超过其版本号的 Class 文件

    • 不同版本的 Java 前端编译器编译生成对应的 Class 文件主次版本号不同
    • 反对高版本 JVM 执行低版本前端编译器生成的 Class 文件(向下兼容)
    • 回绝低版本 JVM 执行高版本前端编译器生成的 Clsss 文件

常量池

常量池蕴含两大常量: 字面量和符号援用

符号援用与间接援用

  • 符号援用

    • 应用一组符号形容援用(为了定位到指标援用)
    • 与虚拟机内存布局无关
    • 还是符号援用时指标援用不肯定被加载到内存
  • 间接援用

    • 间接执行指标的指针, 绝对偏移量或间接定位指标援用的句柄
    • 与虚拟机内存布局相干
    • 解析间接援用时指标援用曾经被加载到内存中

字面量与符号援用

  • 字面量

    • 文本字符串
    • 被 final 申明的常量
  • 符号援用

    • 全限定名
    • 办法或字段的简略名称和描述符

图中的常量有咱们代码中相熟的常量也有很多没有显示呈现在代码中的常量

拜访标记

用于辨认类或接口的访问信息

是否是一个接口, 枚举, 模块, 注解 …

是否被 final(public,abstract…)润饰

ACC_PUBLIC: 被 public 润饰

ACC_SUPER: 容许应用 invokespecial 字节码指令

类索引, 父类索引与接口索引汇合

类索引

用于确定本类的全限定名

类索引指向常量池中示意该类的符号援用

父类索引

用于确定父类的全限定名

父类索引指向常量池中示意该类父类的符号援用

除了 Object 外, 所有类的父类索引都不为 0

接口索引汇合

形容这个类实现了哪些接口

咱们的例子中没有实现接口, 就没有(接口索引汇合计数器为 0)

总结

Class 文件由 类索引, 父类索引, 接口索引汇合 来确定该类的继承关系

字段表汇合

形容类申明的字段

字段包含类变量和成员变量(实例变量), 不包含局部变量

简略名称和描述符

  • 简略名称

    • 字段: 没有形容字段类型的名称
    • 办法: 没有形容参数列表和返回类型的名称
  • 描述符

    • 字段: 形容字段的类型
    • 办法: 形容参数列表和返回值
    • 描述符字符含意(long,boolean, 对象类型是 J,Z,L 其余都是首字母大写)

      标识字符 含意
      B byte
      C char
      D double
      F float
      I int
      J long
      S short
      Z boolean
      V void
      L 对象类型, 如 Ljava/lang/Object
    • 描述符形容 n 维数组

      • 在后面先写 n 个[ 再写标识字符

        比方 java.lang.Integer[] => [Ljava.lang.Integer

    • 描述符形容办法

      • 参数列表依照从左到右的程序写在 ()
      • 返回类型写到最初

        比方 String method(long[],int,String[]) => ([JIL[java.lang.String)Ljava.lang.String

因而 Class 文件中字段描述符指向常量池中的 #07 I 符号援用(的索引)

留神

  1. 字段表汇合不会列出父类或父接口中申明的字段
  2. 只用 简略名称 来确定字段, 所以不能有重名字段
  3. 用 简略名称 和 描述符 确定办法, 所以办法能够重名(重载)

    • 字节码文件 规定 简略名称 + 描述符雷同才是同一个办法
    • 然而 Java 语法 规定 重载 = 简略名称雷同 + 描述符的参数列表不同 + 描述符的返回类型不能不同

办法表汇合

形容类申明的办法

与字段表汇合相似

留神

办法表汇合中不会列出父类办法信息(不重写的状况)

属性表汇合

属性比拟多, 这里只阐明咱们例子中呈现的, 其余的会总结

用于形容某些场景专有信息

刚刚在字段, 办法表汇合中都能够看到属性表汇合, 阐明属性表汇合是能够被携带的

怎么没看到 Java 源代码中的代码呢?

实际上它属于属性表汇合中的 Code 属性

Code 属性

Java 源代码中办法体中的代码通过编译后编程字节码指令存储在 Code 属性内

其中的异样表汇合代表 编译器为这段代码生成的多条异样记录, 对应着可能呈现的代码执行门路

(程序在 try 中不抛出异样会怎么执行, 抛出异样又会怎么执行 ….)

Exceptions 属性

列举出办法中可能抛出的查看异样(Checked Exception), 也就是办法申明 throws 关键字前面的列举异样

LineNumberTable 属性

形容 Java 源码行号与字节码指令行号 (字节码偏移量) 对应关系

SourceFile 属性

记录生成此 Class 文件的源码名称

StackMapTable 属性

虚拟机类加载验证阶段的字节码验证时, 不须要再测验了, 只须要查看 StackMapTable 属性中的记录是否非法

编译阶段将一系列的验证类型后果记录在 StackMapTable 属性中

ConstantValue

在类加载的筹备阶段, 为动态变量 (常量) 赋值

只有类变量才有这个属性

实例变量的赋值: 在实例结构器 <init>

类变量的赋值: 在类结构器 <clinit> 或 带有 ConstantValue 属性在类加载的筹备阶段

如果类变量被 final 润饰(此时该变量是一个常量), 且该变量数据类型是根本类型或字符串, 就会生成 ConstantValue 属性, 该属性指向常量池中要赋值的常量, 在类加载的筹备阶段, 间接把在常量池中 ConstantValue 指向的常量赋值给该变量

![image-20201107191419341]()

总结所有属性
属性名 作用
Code 办法体内的代码通过编译后变为字节码指令存储在 Code 属性中
Exceptions 列举出办法可能抛出的查看异样(Checked Exception)
LineNumberTable Java 源码行号与字节码偏移量 (字节码行号) 对应关系
LocalVariableTable Java 源码定义的局部变量与栈帧中局部变量表中的变量对应关系 ( 局部变量名称, 描述符, 局部变量槽地位, 局部变量作用范畴等)
LocalVariableTypeTable LocalVariableTable 类似, 只是把 LocalVariableTable 的描述符换成了字段的特色签名(实现对泛型的形容)
SourceFile 记录生成这个 Class 文件的源码文件名称
SourceDebugExtension 用于存储额定的代码调式信息
ConstantValue 在类加载的筹备阶段, 为动态变量 (常量) 赋值
InnerClasses 记录外部类与宿主类之间的关系
Deprecated 用于示意某个字段, 办法或类已弃用 (能够用注解 @deprecated 示意)
Synthetic 用于示意某字段或办法不是由 Java 源代码生成的, 而是由编译器自行添加的
StackMapTable 虚拟机类加载验证阶段的字节码验证时, 不须要再测验了, 只须要查看 StackMapTable 属性中的记录是否非法
Signature 记录泛型签名信息
BootstrapMethods 保留动静调用 (invokeeddynamic) 指令援用的疏导办法限定符
MethodParameters 记录办法的各个形参名称与信息

javap 解析 Class 文件

对于 javac

javac xx.java 编译 Java 源文件, 不会生成对应的局部变量表

javac -g xx.java 编译 Java 源文件, 生成对应的局部变量表

idea 中编译 Java 源文件应用的是javac -g

对于 javap

罕用

javap -v 基本上能够反汇编出 Class 文件中的很多信息(常量池, 字段汇合, 办法汇合 …)

然而它不会显示公有字段或办法的信息, 所以能够应用javap -v -p

详解 javap -v -p

 public class JavapTest {
     private int a = 1;
     float b = 2.1F;
     protected double c = 3.5;
     public  int d = 10;
 ​
     private void test(int i){
         i+=1;
         System.out.println(i);
     }
 ​
     public void test1(){
         String s = "test1";
         System.out.println(s);
     }
 }

最初(不要白嫖,一键三连求求拉 \~)

本篇文章笔记以及案例被支出 gitee-StudyJava、github-StudyJava 感兴趣的同学能够 stat 下继续关注喔 \~

有什么问题能够在评论区交换,如果感觉菜菜写的不错,能够点赞、关注、珍藏反对一下 \~

关注菜菜,分享更多干货,公众号:菜菜的后端私房菜

本文由博客一文多发平台 OpenWrite 公布!

退出移动版