zoukankan      html  css  js  c++  java
  • 字节码

      一、字节码指令

      二、字节码

      三、操作数栈和字节码

      四、字节码增强

    一、字节码指令

    Java 一个类

    public static void main(String []args){
             int i = 0;
             int j = 1;
             int k = i + j;
        }

    运行后产生 class 文件,使用 cmd 窗口,执行 javap -c 类名.class ,查看字节码

     参考:Java 字节码指令

     参考:字节码执行过程

    二、字节码

    2.1 什么是字节码

    Java之所以可以“一次编译,到处运行”,一是因为JVM针对各种操作系统、平台都进行了定制,二是因为无论在什么平台,都可以编译生成固定格式的字节码(.class文件)供JVM使用。因此,

    也可以看出字节码对于Java生态的重要性。之所以被称之为字节码,是因为字节码文件由十六进制值组成,而JVM以两个十六进制值为一组,即以字节为单位进行读取。在Java中一般是用javac

    令编译源代码为字节码文件,一个.java文件从编译到运行的示例如图1所示

     对于开发人员,了解字节码可以更准确、直观地理解Java语言中更深层次的东西,比如通过字节码,可以很直观地看到Volatile关键字如何在字节码上生效。另外,字节码增强技术在Spring AOP、

    各种ORM框架、热部署中的应用屡见不鲜,深入理解其原理对于我们来说大有裨益。除此之外,由于JVM规范的存在,只要最终可以生成符合规范的字节码就可以在JVM上运行,因此这就给了各

    种运行在JVM上的语言(如Scala、Groovy、Kotlin)一种契机,可以扩展Java所没有的特性或者实现各种语法糖。

    2.2 字节码结构

    .java 文件通过 javac 编译后生成一个 .class 文件,比如编写一个简单的

    编译后生成的文件,打开后是一堆十六进制数,按字节为单位进行分割后展示如图2右侧部分所示。上文提及过,JVM对于字节码是有规范要求的,那么看似杂乱的十六进制符合什么结构呢?

    JVM规范要求每一个字节码文件都要由十部分按照固定的顺序组成,整体结构如图3所示。接下来我们将一一介绍这十部分:

     (1) 魔数 Magcic Number 

    所有的.class文件的前四个字节都是魔数,魔数的固定值为:0xCAFEBABE。魔数放在文件开头,JVM可以根据文件的开头来判断这个文件是否可能是一个.class文件,如果是,才会继续进行之后的操作。

    有趣的是,魔数的固定值是Java之父James Gosling制定的,为CafeBabe(咖啡宝贝),而Java的图标为一杯咖啡。

    (2) 版本号 Minor Version / Major Version

    版本号为魔数之后的4个字节,前两个字节表示次版本号(Minor Version),后两个字节表示主版本号(Major Version)。上图2中版本号为“00 00 00 33”,次版本号转化为十进制为0,主版本号转化为十进制为51,在Oracle官网中查询序号51对应的主版本号为1.7,所以编译该文件的Java版本号为1.7.0

     (3)常量池 Constant Pool

    紧接着主版本号之后的字节为常量池入口。常量池中存储两类常量:字面量与符号引用。字面量为代码中声明为Final的常量值,符号引用如类和接口的全局限定名、字段的名称和描述符、方法的名称和描述符。

    常量池整体上分为两部分:常量池计数器以及常量池数据区,如下图4所示

    •  常量池计数器 (constant_pool_count):由于常量的数量不固定,所以需要先放置两个字节来表示常量池容量计数值。图2中示例代码的字节码前10个字节如下图5所示,将十六进制的24转化为十进制值为36,排除掉下标“0”,

                         也就是说,这个类文件中共有35个常量

             

     常量池计数器容量是从1开始的,不是0,制定Class 文件规范时,将第0项空出来是由特殊考虑的,这样做是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达 ”不引用任何一个常量池项目“的意思,这种情况下就可以把

     索引0来表示。此外 jvm 层面设计而不是 Java 层面,预留 0 也是为了兼容其他语言。

    •  常量池数据区(constant_pool_count-1) 个cp_info结构组成,一个cp_info 结构对应一个常量。在字节码中共有14种类型的 cp_info,每种类型的结构都是固定的

     具体以CONSTANT_utf8_info为例,它的结构如下图7左侧所示。首先一个字节“tag”,它的值取自上图6中对应项的Tag,由于它的类型是utf8_info,所以值为“01”。接下来两个字节标识该字符串的长度Length,

    然后Length个字节为这个字符串具体的值。从图2中的字节码摘取一个cp_info结构,如下图7右侧所示。将它翻译过来后,其含义为:该常量类型为utf8字符串,长度为一字节,数据为“a”。

    十六进制61 转换为10进制为:97 ,查阅如下 ASCII  ,参考:ASCII

     其他类型的cp_info结构在本文不再赘述,整体结构大同小异,都是先通过Tag来标识类型,然后后续n个字节来描述长度和(或)数据。先知其所以然,以后可以通过javap -verbose ByteCodeDemo命令,

     查看JVM反编译后的完整常量池,如下图8所示。可以看到反编译结果将每一个cp_info结构的类型和值都很明确地呈现了出来。

     (4) 访问标志

    常量池结束之后的两个字节,描述该Class是类还是接口,以及是否被Public、Abstract、Final等修饰符修饰。JVM规范规定了如下图9的访问标志(Access_Flag)。需要注意的是,JVM并没有穷举所有的访问标志,

    而是使用按位或操作来进行描述的,比如某个类的修饰符为Public Final,则对应的访问修饰符的值为ACC_PUBLIC | ACC_FINAL,即0x0001 | 0x0010=0x0011。

     (5)当前类索引

    访问标志后的两个字节,描述的是当前类的全限定名。这两个字节保存的值为常量池中的索引值,根据索引值就能在常量池中找到这个类的全限定名。

    (6)父类索引

    当前类名后的两个字节,描述父类的全限定名,同上,保存的也是常量池中的索引值。

    (7)接口索引

    父类名称后为两字节的接口计数器,描述了该类或父类实现的接口数量。紧接着的n个字节是所有接口名称的字符串常量的索引值

    (8)字段表

    字段表用于描述类和接口中声明的变量,包含类级别的变量以及实例变量,但是不包含方法内部声明的局部变量。字段表也分为两部分,第一部分为两个字节,描述字段个数;第二部分是每个字段的详细信息fields_info。字段表结构如下图所示:

     以图2中字节码的字段表为例,如下图11所示。其中字段的访问标志查图9,0002对应为Private。通过索引下标在图8中常量池分别得到字段名为“a”,描述符为“I”(代表int)。综上,就可以唯一确定出一个类中声明的变量private int a。

    (9)方法表

    字段表结束后为方法表,方法表也是由两部分组成,第一部分为两个字节描述方法的个数;第二部分为每个方法的详细信息。方法的详细信息较为复杂,包括方法的访问标志、方法名、方法的描述符以及方法的属性,如下图所示:

     方法的权限修饰符依然可以通过图9的值查询得到,方法名和方法的描述符都是常量池中的索引值,可以通过索引值在常量池中找到。而“方法的属性”这一部分较为复杂,直接借助javap -verbose将其反编译为人可以读懂的信息进行解读,

     如图13所示。可以看到属性中包括以下三个部分:

    • “Code区”:源代码对应的JVM指令操作码,在进行字节码增强时重点操作的就是“Code区”这一部分。
    • “LineNumberTable”:行号表,将Code区的操作码和源代码中的行号对应,Debug时会起到作用(源代码走一行,需要走多少个JVM指令操作码)。
    • “LocalVariableTable”:本地变量表,包含This和局部变量,之所以可以在每一个方法内部都可以调用This,是因为JVM将This作为每一个方法的第一个参数隐式进行传入。当然,这是针对非Static方法而言。

     (10)附加属性表

    字节码的最后一部分,该项存放了在该文件中类或接口所定义属性的基本信息。

    三、操作数栈和字节码

    JVM的指令集是基于栈而不是寄存器,基于栈可以具备很好的跨平台性(因为寄存器指令集往往和硬件挂钩),但缺点在于,要完成同样的操作,基于栈的实现需要更多指令才能完成(因为栈只是一个FILO结构,需要频繁压栈出栈)。

    另外,由于栈是在内存实现的,而寄存器是在CPU的高速缓存区,相较而言,基于栈的速度要慢很多,这也是为了跨平台性而做出的牺牲。

    我们在上文所说的操作码或者操作集合,其实控制的就是这个JVM的操作数栈。为了更直观地感受操作码是如何控制操作数栈的,以及理解常量池、变量表的作用,将add()方法的对操作数栈的操作制作为GIF,如下图14所示,

    图中仅截取了常量池中被引用的部分,以指令iconst_2开始到ireturn结束,与图13中Code区0~17的指令一一对应:

    四、字节码增强

    在上文中,着重介绍了字节码的结构,这为我们了解字节码增强技术的实现打下了基础。字节码增强技术就是一类对现有字节码进行修改或者动态生成全新字节码文件的技术。接下来,我们将从最直接操纵字节码的实现方式开始深入进行剖析。

     4.1 ASM

    对于需要手动操纵字节码的需求,可以使用ASM,它可以直接生产 .class字节码文件,也可以在类被加载入JVM之前动态修改类行为(如下图17所示)。ASM的应用场景有AOP(Cglib就是基于ASM)、

    热部署、修改其他jar包中的类等。当然,涉及到如此底层的步骤,实现起来也比较麻烦。接下来,本文将介绍ASM的两种API,并用ASM来实现一个比较粗糙的AOP。但在此之前,为了让大家更快地理

    解ASM的处理流程,强烈建议读者先对访问者模式进行了解。简单来说,访问者模式主要用于修改或操作一些数据结构比较稳定的数据,而通过第一章,我们知道字节码文件的结构是由JVM固定的,所

    以很适合利用访问者模式对字节码文件进行修改。

    参考:https://segmentfault.com/a/1190000020345321?utm_source=tag-newest

  • 相关阅读:
    springmvc中@PathVariable和@RequestParam的区别
    Spring MVC 学习总结(一)——MVC概要与环境配置
    web中session与序列化的问题
    EL表达式
    JSTL自定义标签
    [c++][语言语法]stringstream iostream ifstream
    [C++][语言语法]标准C++中的string类的用法总结
    [数据库]数据库查询语句
    [c++][语言语法]函数模板和模板函数 及参数类型的运行时判断
    机器学习算法汇总
  • 原文地址:https://www.cnblogs.com/baizhuang/p/11930480.html
Copyright © 2011-2022 走看看