jvm--字节码浅析

jvm–字节码浅析

  • 本文主要 以java11为例 说明,如有错误,欢迎指正
  • java字节码一般是以.class为后缀的文件,但以.class为后缀的可不一定就是字节码文件,真正区分是否是字节码文件,还是得看前四个字节是否是16进制的cafebabe,如下
  • 在这里插入图片描述
  • 那么这个文件的结构是怎样的,我们怎么才能解析出相关信息呢
  • 有过二进制协议制定的小伙伴可能就不会太陌生,在制定协议时经常会用前2个字节表示什么,第n个字节表示什么等等,没错,这个字节码文件其实就是二进制协议,只不过里面的类型较多,解析和理解还是挺繁琐的

字节码文件结构

  • 官方文档:Chapter 4. The class File Format

  • 在这里插入图片描述

  • 上图就是字节码文件的结构,是从官方截取而来的,u来表示一个无符号字节,u1表示1个字节,u2表示2个字节,u4表示4个字节)

  • 这个结构总体看起来还是很清晰的,解析的时候也只要跟随这个总体结构一步一步深入下去即可

  • 下面我们就来解释下这些结构(下文中所说的位置和索引其实是同个意思)

占用的字节长度表示说明
u4magic魔术字,固定为16进制的ca fe ba be,表示class文件
u2minor_version副版本号,现在一般都为0
u2major_version主版本号,我们熟悉的java8对应的就是十进制的52
u2constant_pool_count常量池计数器,注意实际的个数是常量池计数器-1,即如果常量池计数器为10,那么实际常量个数为9,而且下标是从1开始的;下标为0有其他用处,比如匿名内部类的就是下标为0
cp_infoconstant_pool
[constant_pool_count-1]
常量表,其所占用字节长度根据常量池计数器和每个常量的定义决定,比如常量池计数器为5,那么我们可以知道有4个常量,假定每个常量依次占用的字节为2,2,3,4的话,那么这里的字节长度n=2+2+3+4=11
u2access_flags类的访问标识,例如ACC_PUBLIC,即我们在声明类时的public class Xxx{}中的public,注意此不同的访问标识是可以叠加的,比如同时拥有ACC_PUBLIC和ACC_FINAL
u2this_class当前类在常量池的索引(注意常量池索引是从1开始的),比如为1的话表示常量池列表的第一个常量
u2super_class父类在常量池的索引
u2interfaces_count实现的接口的计数器,没有实现任何接口的话则为0
u2* interfaces_countinterfaces
[interfaces_count]
此数组存放每个接口在常量表中的位置,而位置只占用两个字节长度而已,因此总的长度就是2*interfaces_count
另外,这里的顺序跟源码中声明的接口顺序必须保持一致
u2fields_count字段计数器
field_infofields
[fields_count]
字段表,由于每个字段占用长度不同,因此此部分占用长度要根据实际的字段计算
https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-4.html#jvms-4.5
u2methods_count方法计数器
method_infomethods
[methods_count]
方法表,由于每个方法占用长度不同,因此此部分占用长度要根据实际的方法计算
https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-4.html#jvms-4.6
u2attributes_count属性计数器
attribute_infoattributes
[attributes_count]
属性表,由于每个属性占用长度不同,因此此部分占用长度要根据实际的属性计算
https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-4.html#jvms-4.7

常量表

  • 为啥要有常量表(也叫常量池)呢,我们知道字节码文件可以被反编译成源码,既然可以被反编译,这就说明字节码中记录了整个源码的信息,比如方法名,字段名等
  • 而一个类中可能有多个方法同名,多个相同的调用,因此为了节省空间,我们只需要对这些信息建立一个表,然后在需要用到的地方用索引来替代,这样就可以大大节省空间了,而这张表就是我们所说的常量池,它其实就是一个数组,只不过索引是从1开始的,0表示其他意义
  • 比如某个字符串被放在了索引1的位置,那么在需要使用这个字符串的地方用索引1表示即可
  • 由于常量池记录了整个源码的信息,因此这部分内容在整个字节码中所占的比重是比较大的
  • 那么源码的每个信息是怎么存储的呢
  • jvm首先规定了每种信息的统一存储结构,即tag+info
  • tag用一个字节存储,用来表示信息的类别,共有17种,info则表示每种信息自己相对应的存储结构,这部分内容由于过多,想了解详情的建议去官方查阅:https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-4.html#jvms-4.4
  • 在这里插入图片描述
  • 举例说明,如果想存储类的信息,从官网查阅类的存储结构如下
CONSTANT_Class_info {
    u1 tag;//对于类来说,固定为0x07
    u2 name_index;//类信息在常量表中的位置
}
  • 也就是存储类信息只需要3个字节,第一个固定为0x07,后两个字节则表示此类信息在常量表的位置(也叫索引,表达的是用一个意思,下同)

接口表

CONSTANT_Class_info {
    u1 tag;//对于接口来说,固定为0x07
    u2 name_index;//接口信息在常量表中的位置
}
  • 用于表示接口时tag固定为7,name_index则表示接口名在常量表的位置
    在这里插入图片描述
  • 这里需要注意的是,接口表里只记录了接口在常量表中的位置,而不是接口的信息,因此接口表的字节长度为 2*接口数
  • 同时接口顺序与源码中声明的顺序是一致的

字段表

field_info {
    u2             access_flags; //访问标识,如ACC_PUBLIC
    u2             name_index; //字段名在常量表中的位置
    u2             descriptor_index;//字段类型,例如int则为I,也是指在常量表中的位置
    u2             attributes_count; //此字段携带的属性个数
    attribute_info attributes[attributes_count];
}
  • 前两个字节用于表示字段的访问权限,即我们源码中声明的private,public等,字段对应的访问标识有如下
  • 在这里插入图片描述
  • descriptor_index是指描述符的位置,这个描述符其实是放在常量表中,因此这里其实指的就是常量表的位置
  • 什么是描述符呢
  • 对于字节码来说就是我们在源码中看到的基本数据类型如int,double以及数组,类对象等的另一种表现形式,如下
  • 在这里插入图片描述
  • 至于属性,字段可以有哪些属性呢
  • 举个例子,比如public final int a=123;,由于用final修饰,那么这个123就是常量了,这就可以算是字段a的一个属性
  • 查阅得知属性的一般结构如下
attribute_info {
    u2 attribute_name_index; //属性字符串在常量表中的位置
    u4 attribute_length;//接下来属性所占用的字节长度,可以为0
    u1 info[attribute_length];//此部分根据具体的属性决定,其所占用的字节长度就是attribute_length
}
  • 而我们本例中的123表示"ConstantValue",是属性多种存储结构中的一种类型,如下
ConstantValue_attribute {
    u2 attribute_name_index;//即"ConstantValue"字符串在常量表中的位置
    u4 attribute_length;//对于常量值固定为2
    u2 constantvalue_index;//常量值123在常量表中的位置
}

方法表

method_info {
    u2             access_flags;//方法的访问标识,如ACC_PUBLIC
    u2             name_index;//方法名在常量表中位置,值得注意的是编译器可能会帮我们添加<init>,<clinit>方法
    u2             descriptor_index;//方法名描述符,如(II)V
    u2             attributes_count;//方法的属性个数
    attribute_info attributes[attributes_count];//属性表
}
  • 方法的访问标识可以有如下

  • 在这里插入图片描述

  • 那么方法有什么属性呢,方法抛出的异常可以算一个,方法的参数名也是,方法里的指令也是一个属性(即方法体)

  • 这里就来讲讲方法里最重要的方法体,在jvm中用Code来指代

属性–Code

  • 在说明Code的结构之前,我们得先知道Code究竟是哪块?
  • 在我们用命令javap -verbose xxx.class得到可读的字节码时,下面红框的部分就是Code了,那么左边的数字0,1,4表示什么意思呢,我们知道Code也是编译成二进制一个一个字节存储起来的,那么这个数字就是Code所占用的字节数组里的位置(也叫偏移量),通常都用pc表示
  • 在这里插入图片描述
Code_attribute {
    u2 attribute_name_index; //"Code"字符串在常量表中的位置
    u4 attribute_length;//接下来属性所占用的长度,即不包含attribute_length和attribute_name_index的长度,也就是不包含这6个字节;举个例子,整个Code_attribute的长度为n,那么attribute_length的值就是n-6
    u2 max_stack; //此方法操作数栈的最大深度
    u2 max_locals;//最多的局部变量个数
    u4 code_length;//指令所占用的字节长度,注意不是指指令的个数
    u1 code[code_length];//所有指令就存储在这里了
    u2 exception_table_length;//异常表的个数,每个异常都占用8个字节
    {   u2 start_pc; //在指令字节数组中的起始位置,比如指令总共占用了10个字节,如果这里start_pc是4的话,则表示第5个字节就是异常的开始位置;start_pc和end_pc就描述了这个trycatch所作用的范围
        u2 end_pc;//在指令字节数组中的结束位置
        u2 handler_pc;//产生异常后的处理程序在指令字节数组的位置
        u2 catch_type;//异常类型,在常量表中的位置
    } exception_table[exception_table_length];
    u2 attributes_count;//方法的属性个数,方法的属性比如代码行数,局部变量都属于方法的属性
    attribute_info attributes[attributes_count];//每个属性的占用空间
}
  • 代码指令的构成是“操作码+操作数(可选)”,操作码用一个字节表示,一个字节最多表示256个,也就是现在操作码的上限就是256个(多了我也不知道jvm后面会怎么兼容),而操作数是可选的,大部分情况下也就是两个字节来存储的常量表的索引

属性–LineNumberTable

  • 这里以方法的代码行号表属性来讲解一下,这个属性为LineNumberTable
LineNumberTable_attribute {
    u2 attribute_name_index; //"LineNumberTable"字符串在常量表中的位置
    u4 attribute_length;//接下来属性所占用的长度,即LineNumberTable_attribute所占用字节长度-6
    u2 line_number_table_length;//行数属性个数
    {   u2 start_pc;//在指令字节数组的位置
        u2 line_number;	//start_pc所在位置代表的指令所对应的源码的行号
    } line_number_table[line_number_table_length];
}

属性–LocalVariableTable

  • 为了加深印象,这里再以局部变量表为例讲解方法的属性
LocalVariableTable_attribute {
    u2 attribute_name_index;//"LocalVariableTable"字符串在常量表中的位置
    u4 attribute_length;//接下来属性所占用的长度,即LocalVariableTable所占用字节长度-6
    u2 local_variable_table_length;//局部变量个数
    {   u2 start_pc;//此变量的作用范围在指令字节数组中的起始位置
        u2 length;//start_pc+length就指明了此局部变量的作用范围
        u2 name_index;//变量名在常量表中的位置
        u2 descriptor_index;//描述符在常量表中的位置
        u2 index;//在局部变量表中的位置(槽位),我们知道局部变量表中的槽位是可以复用的,即多个局部变量可能都放置在同一个槽位
    } local_variable_table[local_variable_table_length];
}

属性表

  • https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-4.html#jvms-4.7
  • 在字段和方法结构中我们都可以看到有属性这个东西,那么对于jvm来说,什么才是属性呢
  • 我们之所以会对属性感到困惑,可能就是我们把属性和字段搞混了
  • 对于jvm来说,字段就是在源码中声明的字段,比如int a=1;中的a就是字段
  • 那么什么是属性呢,对于类来说,这个类的源文件名就是它的一个属性;字段如果是个常量值,那么常量值也是字段的一个属性;等等
  • 这些属性也需要像数组一样存储起来,这就是所谓的属性表
  • 属性的一般存储结构如下:
attribute_info {
    u2 attribute_name_index;//属性字符串在常量表中的位置
    u4 attribute_length;//接下来属性所占用的字节长度,可以为0 
    u1 info[attribute_length]; //此部分根据具体的属性决定,其所占用的字节长度就是attribute_length
}

属性–Deprecated

  • 以过时声明属性为例
Deprecated_attribute {
    u2 attribute_name_index;// "Deprecated"字符串在常量表的位置
    u4 attribute_length; //对于Deprecated来说,固定为0,因此也就没有了一般存储结构中的info信息
}

属性–SourceFile

  • 再以源文件属性为例
SourceFile_attribute {
    u2 attribute_name_index;//"SourceFile"字符串在常量表中的位置
    u4 attribute_length;//对于SourceFile来说,固定为2
    u2 sourcefile_index;//源文件名在常量表中的位置
}

问题

一个类究竟可以有多大

  • 从字节码结构中用于存储常量池计数器的字节个数我们可以知道,两个字节能表示的最大常量个数为65535-1,这也就是最大极限了
  • 不过可以放心,一般类都不会有这么大,如果真的有,那也不是jvm的问题,反而你应该自我反省怎么写出了一个如此庞大臃肿的类
  • 而且从jvm规范中我们可以知道用来存储个数的一般都是2个字节,那么其实这也就限制了类的大小,比如最多只能有两个字节长度的字段,最多只能有两个字节长度的方法等等

字段和属性的区别

  • 在字节码定义中,字段是源码中的声明的Field,而属性则是用来记录字段,方法和类的其他一些附加信息,如final修饰的字段的常量就是字段的一个属性,方法的方法体中的指令也是一个属性,类的源文件信息也是一个属性,等等

常量池索引为何从1开始

  • 常量池索引0用来表示不指向任何常量,例如匿名内部类是没有类名称信息的,此时常量池索引就是0

参考

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
为什么要学JVM1、一切JAVA代码都运行在JVM之上,只有深入理解虚拟机才能写出更强大的代码,解决更深层次的问题。2、JVM是迈向高级工程师、架构师的必备技能,也是高薪、高职位的不二选择。3、同时,JVM又是各大软件公司笔试、面试的重中之重,据统计,头部的30家互利网公司,均将JVM作为笔试面试的内容之一。4、JVM内容庞大、并且复杂难学,通过视频学习是最快速的学习手段。课程介绍本课程包含11个大章节,总计102课时,无论是笔试、面试,还是日常工作,可以让您游刃有余。第1章 基础入门,从JVM是什么开始讲起,理解JDK、JRE、JVM的关系,java的编译流程和执行流程,让您轻松入门。第2章 字节码文件,深入剖析字节码文件的全部组成结构,以及javap和jbe可视化反解析工具的使用。第3章 类的加载、解释、编译,本章节带你深入理解类加载器的分类、范围、双亲委托策略,自己手写类加载器,理解字节码解释器、即时编译器、混合模式、热点代码检测、分层编译等核心知识。第4章 内存模型,本章节涵盖JVM内存模型的全部内容,程序计数器、虚拟机栈、本地方法栈、方法区、永久代、元空间等全部内容。第5章 对象模型,本章节带你深入理解对象的创建过程、内存分配的方法、让你不再稀里糊涂。第6章 GC基础,本章节是垃圾回收的入门章节,带你了解GC回收的标准是什么,什么是可达性分析、安全点、安全区,四种引用类型的使用和区别等等。第7章 GC算法与收集器,本章节是垃圾回收的重点,掌握各种垃圾回收算法,分代收集策略,7种垃圾回收器的原理和使用,垃圾回收器的组合及分代收集等。第8章 GC日志详解,各种垃圾回收器的日志都是不同的,怎么样读懂各种垃圾回收日志就是本章节的内容。第9章 性能监控与故障排除,本章节实战学习jcmd、jmx、jconsul、jvisualvm、JMC、jps、jstatd、jmap、jstack、jinfo、jprofile、jhat总计12种性能监控和故障排查工具的使用。第10章 阿里巴巴Arthas在线诊断工具,这是一个特别小惊喜,教您怎样使用当前最火热的arthas调优工具,在线诊断各种JVM问题。第11章 故障排除,本章会使用实际案例讲解单点故障、高并发和垃圾回收导致的CPU过高的问题,怎样排查和解决它们。课程资料课程附带配套项目源码2个159页高清PDF理论篇课件1份89页高清PDF实战篇课件1份Unsafe源码PDF课件1份class_stats字段说明PDF文件1份jcmd Thread.print解析说明文件1份JProfiler内存工具说明文件1份字节码可视化解析工具1份GC日志可视化工具1份命令行工具cmder 1份学习方法理论篇部分推荐每天学习2课时,可以在公交地铁上用手机进行学习。实战篇部分推荐对照视频,使用配套源码,一边练习一遍学习。课程内容较多,不要一次性学太多,而是要循序渐进,坚持学习。      
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值