精华内容
下载资源
问答
  • 主要给大家介绍了关于java字节码框架ASM如何操作字节码的相关资料,文中通过示例代码介绍的很详细,有需要的朋友可以参考借鉴,下面来一起看看吧。
  • EVM字节码反编译器 解释器和反编译器,以及用于从字节码中以编程方式提取信息的其他几种工具。 用法 npm我evm 产品特点 将字节码转换为操作码 从字节码或TX数据中读取事件或函数之类的信息 从字节码中提取(如果有...
  • 市面上以及网络搜索中都基本很少有成体系的关于字节码编程的知识,这主要由于大部分开发人员其实很少接触这部分内容,包括;ASM、Javassist、Byte-buddy以及JavaAgent,没有很大的市场也就没有很多的资料。但大家...
  • jclasslib是一款免费开源的java字节码查看工具,该软件不但可以查看java字节码,同时还包含一个类库允许开发者读取,修改,写入Java Class文件与字节码。简单的说:用户可以通过jclasslib修改jar包下面的类,是一个...
  • 这篇文章我们以输出 "Hello, World" 来开始字节码之旅,如果之前没有怎么接触过字节码的话,这篇⽂章应该能够让你对字节码有⼀个最基本的认识。
  • Retrotranslator的网站位于http: //sourceforge.net/projects/retrotranslator,可以在这个网站下载相关的运行程序以及源码,该工具可以实现大多java15的功能转换为java14环境下运行。我测试了一个前端时间开发的一...
  • 轻松看懂Java字节码,对java字节码的详细分析,理解java字节码
  • java字节码文件反编译

    2018-01-17 11:12:14
    jd-gui java字节码反编译 class反编译 jd-gui java字节码反编译 class反编译
  • 字节码可视化工具

    2018-09-13 11:49:23
    字节码可视化工具,对于查看十六进制的字节码文件非常方便
  • 修改class文件需要用到的工具,里边有详细使用说明,修改具体操作请看 https://blog.csdn.net/qq_16457573/article/details/100030956
  • 1.将class文件直接拖拽进工作框,即可查看class文件. 2.支持目录结构查看,方便实用
  • 字节码编程,提供了字节码的基础知识和操作系统等知识,学习完成后可能对字节码有一个很好的认识,在java中很多的一些也能用到字节码的知识
  • Gradle Plugin 修改其class 字节码的Demo
  • 字节码指令详解

    万次阅读 2020-08-05 16:49:04
    字节码指令详解 指令简介 在计算机中,CPU指令就是指挥机器工作的指令,程序就是一系列按一定顺序排列的指令,执行程序的过程就是执行指令的过程,也就是计算机的工作过程。 通常一条CPU指令包括两方面的内容:操作...

    字节码指令详解

    指令简介

    在计算机中,CPU指令就是指挥机器工作的指令,程序就是一系列按一定顺序排列的指令,执行程序的过程就是执行指令的过程,也就是计算机的工作过程。

    通常一条CPU指令包括两方面的内容:操作码和操作数,操作码表示要完成的操作,操作数表示参与运算的数据及其所在的单元地址(这个单元地址可以是寄存器、内存等)。

    Java虚拟机中的字节码指令与CPU中指令类似,Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的操作码(opcode)以及跟随其后的零至多个代表此操作所需参数的操作数(operand)所构成。

    操作数的数量以及长度取决于操作码,如果一个操作数的长度超过了一个字节,那么它将大端排序存储,即高位在前的字节序。

    操作码只占一个字节,也就导致操作码个数不能超过256。

    class文件只会出现数字形式的操作码,但是为了便于人识别,操作码有他对应的助记符形式。

    对于基本数据类型,指令在设计的时候都用一个字母缩写来指代(boolean除外)。

    基本数据类型缩写
    byteb
    shorts
    inti
    longl
    floatf
    doubled
    charc
    refrencea
    boolean

    指令详解

    加载存储指令

    加载存储指令用来交换局部变量表和操作数栈中的数据,以及将常量加载到操作数栈。

    1. 将一个局部变量从局部变量表中加载到操作数栈栈顶:
    iload、iload_<n>、lload、lload_<n>、fload、fload_<n>、dload、dload_<n>、aload、aload_<n>
    

    其中n为局部变量表中的slot的序号,double和long占用两个slot。

    1. 将一个操作数栈的栈顶元素存储到局部变量表:
    istore、istore_<n>、lstore、lstore_<n>、fstore、fstore_<n>、dstore、dstore_<n>、astore、astore_<n>
    
    1. 将一个常量加载到操作数栈栈顶:
    bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>
    

    操作数为将要操作的数值或者常量池行号。

    1. 扩充局部变量表的访问索引的指令:wide。

    运算指令

    运算指令会取出操作数栈栈顶的两个元素进行某种特定的运算,然后将结果重新存入到操作数栈栈顶。

    运算指令分为两种:整型运算的指令和浮点型运算的指令。

    无论是哪种运算指令,都使用Java虚拟机的数据类型,由于没有直接支持byte、short、char和boolean类型的算术指令,使用操作int类型的指令代替。

    加法指令:iadd、ladd、fadd、dadd

    减法指令:isub、lsub、fsub、dsub

    乘法指令:imul、lmul、fmul、dmul

    除法指令:idiv、ldiv、fdiv、ddiv

    求余指令:irem、lrem、frem、drem

    取反指令:ineg、lneg、fneg、dneg

    位移指令:ishl、ishr、iushr、lshl、lshr、lushr

    按位或指令:ior、lor

    按位与指令:iand、land

    按位异或指令:ixor、lxor

    局部变量自增指令:iinc

    比较指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp

    类型转换指令

    类型转换指令可以将两种不同的数值类型进行相互转换。这些转换操作一般用于实现用户代码中的显式类型转换操作,或者用来解决字节码指令集不完备的问题。

    宽化指令:

    • int类型到long、float或者double类型
    • long类型到float、double类型
    • float类型到double类型

    处理窄化类型转换(Narrowing Numeric Conversions)时,必须显式地使用转换指令来完成,这些转换指令包括:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f。

    对象的创建与存取指令

    new:创建对象。

    getfield:获取实例对象的属性的值。

    putfield:设置实例对象的属性的值。

    getstatic:获取类的静态属性的值。

    putstatic:设置类的静态属性的值。

    数组的创建与存取指令

    newarray:创建元素为基本数据类型的数组。

    anewarray:创建数据类型为引用类型的数组。

    multianewarray:创建多维数组。

    把一个数组元素加载到操作数栈栈顶的指令:baload、caload、saload、iaload、laload、faload、daload、aaload

    将一个操作数栈栈顶的值存储到数组元素中的指令:bastore、castore、sastore、iastore、fastore、dastore、aastore

    取数组长度的指令:arraylength

    检查类实例类型的指令

    instanceof、checkcast。

    操作数栈管理指令

    pop:将操作数栈的栈顶元素出栈。

    pop2:将操作数栈的栈顶两个元素出栈。

    复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2

    将栈最顶端的两个数值互换:swap

    控制转移指令

    控制转移指令可以让Java虚拟机有条件或无条件地从指定的位置指令而不是控制转移指令的下一条指令继续执行程序。

    控制转移指令如下:

    • 条件分支:ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq 和 if_acmpne。
    • 复合条件分支:tableswitch、lookupswitch。
    • 无条件分支:goto、goto_w、jsr、jsr_w、ret。

    方法调用指令

    invokevirtual:指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是Java语言中最常见的方法分派方式。

    invokeinterface:指令用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。

    invokespecial:指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。

    invokestatic:指令用于调用类方法(static方法)。

    invokedynamic:指令用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法,前面4条调用指令的分派逻辑都固化在Java虚拟机内部,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。

    方法调用指令与数据类型无关。

    方法返回指令

    方法返回指令是根据方法的返回值类型区分的,包括ireturn(当返回值是boolean、byte、char、short和int类型时使用)、lreturn、freturn、dreturn 和areturn,另外还有一条return指令供声明为void的方法、实例初始化方法以及类和接口的类初始化方法使用。

    异常处理指令

    在Java程序中显式抛出异常的操作(throw语句)都由athrow指令来实现。

    同步指令

    monitorenter和monitorexit两条指令来支持synchronized关键字的语义。

    常见语法结构对应的字节码指令

    异常处理

    public class SynchronizedDemo {
    
        final Object lock = new Object();
    
        void doLock() {
            synchronized (lock) {
                System.out.println("lock");
            }
        }
    }
    

    对应的字节码如下:

    stack=2, locals=3, args_size=1
       0: aload_0
       1: getfield      #3                  // Field lock:Ljava/lang/Object;
       4: dup
       5: astore_1
       6: monitorenter
       7: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
      10: ldc           #5                  // String lock
      12: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      15: aload_1
      16: monitorexit
      17: goto          25
      20: astore_2
      21: aload_1
      22: monitorexit
      23: aload_2
      24: athrow
      25: return
    Exception table:
       from    to  target type
           7    17    20   any
          20    23    20   any
    

    在synchronized生成的字节码中,其中包含两条monitorexit指令,是为了保证所有的异常条件,都能够退出。

    可以看到,编译后的字节码,带有一个叫Exception table的异常表,里面的每一行数据,都是一个异常处理:

    • from:指定字节码索引的开始位置
    • to:指定字节码索引的结束位置
    • targe:t异常处理的起始位置
    • type:异常类型

    也就是说,只要在from和to之间发生了type异常,就会跳转到target所指定的位置。

    装箱和拆箱

    public class BoxDemo {
    
        public Integer cal() {
            Integer a = 1000;
            int b = a * 10;
            return b;
        }
    }
    

    对应的字节码如下:

     0: sipush        1000
     3: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
     6: astore_1
     7: aload_1
     8: invokevirtual #3                  // Method java/lang/Integer.intValue:()I
    11: bipush        10
    13: imul
    14: istore_2
    15: iload_2
    16: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
    19: areturn
    

    通过观察字节码,我们发现装箱和拆箱的本质:

    • 装箱:调用Xxx.valueOf()方法将基本数据类型包装成包装对象。
    • 拆箱:调用Xxx.xxxValue()方法将包装对象转为基本数据类型。

    数组

    package com.morris.jvm.bytecode;
    
    public class ArrayDemo {
        int getValue() {
            int[] arr = new int[]{1111, 2222, 3333, 4444};
            return arr[2];
        }
        int getLength(int[] arr) {
            return arr.length;
        }
    }
    
    

    getValue()方法对应的字节码如下:

    stack=4, locals=2, args_size=1
       0: iconst_4
       1: newarray       int
       3: dup
       4: iconst_0
       5: sipush        1111
       8: iastore
       9: dup
      10: iconst_1
      11: sipush        2222
      14: iastore
      15: dup
      16: iconst_2
      17: sipush        3333
      20: iastore
      21: dup
      22: iconst_3
      23: sipush        4444
      26: iastore
      27: astore_1
      28: aload_1
      29: iconst_2
      30: iaload
      31: ireturn
    LineNumberTable:
      line 5: 0
      line 6: 28
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0      32     0  this   Lcom/morris/jvm/bytecode/ArrayDemo;
         28       4     1   arr   [I
    

    可以看到,新建数组的代码,被编译成了newarray指令。

    数组的创建具体操作:

    1. iconst_0:常量0压入操作数栈栈顶。
    2. sipush:将一个常量1111压入操作数栈栈顶。
    3. iastore:将栈顶int型数值存入数组的0索引位置。

    数组元素的访问,是通过第28~30行字节码来实现的:

    1. aload_1:从局部变量表中第二个引用类型的局部变量压入操作数栈栈顶,这个引用就是生成的数组的引用。
    2. iconst_2:将int型2压入操作数栈栈顶。
    3. iaload:将int型数组指定索引的值压入操作数栈栈顶。

    getLength()方法对应的字节码如下:

    0: aload_1
    1: arraylength
    2: ireturn
    

    获取数组的长度,是由字节码指令arraylength来完成的。

    更多精彩内容关注本人公众号:架构师升级之路
    在这里插入图片描述

    展开全文
  • 字节码文件及字节码指令

    千次阅读 2019-05-21 11:24:14
    每个字节码文件开头的4个字节称为魔数,作用是确定这个字节码文件是否是一个能被虚拟机接受的字节码文件。在上图字节码文件中看到的开始的四个字节0xCAFEBABE代表的就是Java。 接着魔数后的第5个和第6个两个字节...

    我记得开始学习Java的第一堂课时,我的大学老师是这样说的,Java号称是“一次编写,到处运行”,为什么有底气这样说,是因为Java程序并不是直接运行在操作系统上的,它通过不同操作系统上的Java虚拟机实现了“到处运行”的美好愿景。而且我的老师当时还说过,不止Java程序可以在Java虚拟机上运行,其他的程序也同样可以在Java虚拟机上运行。Java虚拟机并不认识具体的某种编程语言,而是编程语言要通过编译器编译成虚拟机认识的格式内容。那么虚拟机认识的格式内容就是本文主要讲述的一个神奇的东东–字节码文件。

    本文主要内容是字节码文件的格式及字节码指令。

    一、字节码文件

    字节码是什么?

    一开始我以为字节码是个非常复杂的东西,不修炼几千年根本无法了解其中的奥秘。但是剥丝抽茧后,我发现其实本质上就是一个特定格式排列各个字段信息的二进制流文件,用于虚拟机识别执行。根据Java虚拟机规范,字节码文件使用类似于C语言结构体的伪结构来存储数据,这种伪结构只有两种数据类型,无符号数和表。

    下面首先来看一段为字节码而造的代码:

    package com.earl.se.basics;
    
    public class Demo5 implements Runnable {
    
        private int number = 1;
    
        @Override
        public void run() {
            System.out.println("Demo5 thread say hello byte code.");
        }
    
        public static void main(String[] args){
    
            System.out.println("Main thread say hello byte code.");
    
            new Thread(new Runnable() {
                @Override
                public void run() {
    
                }
            }).start();
        }
    }
    
    

    当我通过javac命令编译了Demo5.java后,会生成字节码文件Demo5.class,通过十六进制编辑器WinHex将其打开,可以看到是如下的格式:

    在这里插入图片描述

    哈哈,仅仅是这么几行代码,生成的字节码文件看起来是不是都很头大,毕竟人脑还是适合阅读人类的文字而非机器所喜欢的文字。但是没办法,既然选择要正面刚虚拟机的知识,那么了解其内部的结构是必不可少的环节。接下来先介绍一下字节码文件中各个部分分别代表的意义,概念扫盲之后,其实是可以通过咱们开发常用的IDE来进行查看字节码文件的,这样是不是就显得轻松多了,嘿嘿~

    字节码文件结构

    字节码文件有两种数据格式:

    • 无符号数属于基本数据类型,以u1,u2,u4,u8分别表示1个字节,2个字节,4个字节,8个字节的无符号数。
    • 表是由多个无符号数或其他表作为数据项构成的复合数据类型。所有的表都是以“_info”结尾。

    下表是按照字节码文件中各字段排列顺序的全部格式:

    名称类型数量
    魔数(magic)u41
    子版本号(minor_version)u21
    主版本号(major_version)u21
    常量池计数值(constant_pool_count)u21
    常量池(constant_pool)cp_infoconstant_pool_count-1
    访问标志(access_flag)u21
    类索引(this_class)u21
    父类索引(super_class)u21
    接口计数值(interfaces_count)u21
    接口索引集合(interfaces)u2interface_count
    字段表集合计数器(fields_count)u21
    字段表集合(fields)field_infofields_count
    方法表集合计数器(methods_count)u21
    方法表集合(methods)method_infomethods_count
    属性表集合计数器(attributes_count)u21
    属性表集合(attributes)attribute_infoattributes_count

    下面依次来说明各个类型是什么含义:

    每个字节码文件开头的4个字节称为魔数,作用是确定这个字节码文件是否是一个能被虚拟机接受的字节码文件。在上图字节码文件中看到的开始的四个字节0xCAFEBABE代表的就是Java。

    接着魔数后的第5个和第6个两个字节表示子版本号,接下来的第7个和第8个字节表示主版本号。

    接下来是常量池相关信息。由于常量池中常量的数量是不固定的,因此在常量池入口处设置了一个常量池计数值。常量池中主要存放的是字面量和符号引用。
    字面量即如java语言中的常量概念。
    符号引用则属于编译原理方面的概念,包括类和接口的全限定名,字段的名称和描述符,方法的名称和描述符。
    在字节码文件中并不存储各个方法,字段的最终内存布局信息,因此当虚拟机运行时,需要从常量池中获取到符号引用,在类创建或运行时在找到具体的内存地址。

    接下来的两个字节代表访问标志,用于识别类或接口层次的访问信息,包括这个字节码文件对应的是类还是接口,是否定义为public类型,是否定义为abstract类型,类的话是否是final声明等。

    接下来是类索引,父类索引和接口索引集合。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类全限定名,接口索引集合描述这个类实现了哪些接口(如果这个类是接口,那么就是继承了哪些接口)。接口计数值表示这个类或接口实现或继承的接口数量,如果没有实现或继承接口,这个值为0。

    字段表集合计数器与字段表集合用来描述接口或类中声明的变量。

    方法表集合计数器与方法表集合用来描述接口或类中声明的方法。

    属性表集合计数器与属性表集合用来描述Class文件等在某些场景的专有信息。

    通过IDE来查看字节码文件

    扫盲结束,接下来讲讲如何通过我们常用的IDE来查看字节码文件。这里我选用的是IDEA,首先要安装字节码查看的插件。File->Settings->Plugins,在Marketplace中搜索jclasslib,然后安装jclasslib Bytecode viewer,重启IDEA即可完成安装。

    接下来就可以使用jclasslib来查看字节码文件了,还是以Demo5.java为例,在IDEA中打开这个文件,然后点击View->Show Bytecode With jclasslib,这样就会看到如下图的展示:

    在这里插入图片描述

    这样看起来是不是就清楚很多了,右侧的窗口中将字节码文件中的各个字段都直观地展示出来,再无需我们手动去将十六进制转换为人类文字了。

    二、字节码指令

    字节码指令是一个字节长度的,代表着某种特定操作含义的数字操作码,由操作码及代表此操作所需操作参数构成。主要包含以下的一些指令操作:

    1. 加载和存储指令:用于将数据在栈帧中的局部变量表和操作数栈之间来回传输,主要包含以下的操作:
      • 将局部变量加载到操作栈,涉及指令有load指令
      • 将一个数值从操作数栈存储到局部变量表,涉及指令有store指令
      • 将一个常量加载到操作数栈,涉及指令有push指令,const指令
      • 扩充局部变量表的访问索引指令,wide指令
    2. 运算指令:用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入操作栈顶。运算主要分为两种,对整型数据进行运算和对浮点型数据进行运算,主要包含以下操作:
      • 加法指令:add
      • 减法指令:sub
      • 乘法指令:mul
      • 除法指令:div
      • 求余指令:rem
      • 取反指令:neg
      • 位移指令:shl,shr,ushr
      • 按位或指令:or
      • 按位与指令:and
      • 按位异或指令:xor
      • 局部变量自增指令:inc
      • 比较指令:cmpg,cmpl,cmp
    3. 类型转换指令:用于将两种不同的数值类型进行相互转换,这些转换操作主要用于代码中的显式类型转换。
      • 对于小类型转大类型的操作,虚拟机直接支持,无需转换指令。
      • 对于大类型转小类型的操作,涉及指令有i2b,i2c,i2s,l2i,f2i,f2l,d2i,d2l,d2f。大转小的操作可能会导致精度丢失。
    4. 对象创建与访问指令,主要涉及以下操作:
      • 创建类实例的指令:new
      • 创建数组的指令:newarray,anewarray,multianewarray
      • 访问类字段和实例字段指令:getfield,getstatic,putfield,putstatic
      • 加载数组元素到操作数栈指令:aload
      • 将操作数栈的值存储到数组元素指令:astore
      • 取数组长度:arraylength
      • 检查类实例类型指令:instanceof,checkcast
    展开全文
  • 字节码详解

    千次阅读 2020-10-08 18:16:21
    字节码详解 前言 万事开头难 字节码相关内容往深了挖其实东西很多,我就按照自己学习的一个心理历程去分享一下这块儿的内容,起个抛砖引玉的作用,很多地方没有特别深入的研究,有待大家补充。 什么是字节码 Java...

    字节码详解

    前言

    万事开头难

    字节码相关内容往深了挖其实东西很多,我就按照自己学习的一个心理历程去分享一下这块儿的内容,起个抛砖引玉的作用,很多地方没有特别深入的研究,有待大家补充。

    什么是字节码

    Java作为一款“一次编译,到处运行”的编程语言,跨平台靠的是JVM实现对不同操作系统API的支持,而一次编译指的就是class字节码;即我们编写好的.java文件,通过编译器编译成.class文件,JVM负责加载解释字节码文件,并生成系统可识别的代码执行(具体解析本次不做深入研究).

    Class文件

    字节码官方文档

    hello world

    从代码开始:

    package com.qty.first;
    
    public class ClassDemo {
    	
    	public static void main(String[] args) {
    		System.out.println("hello world!!");
    	}
    }
    

    直接在IDE下新建项目,写一个Hello World程序,用文本编辑器打开生成的ClassDemo.class文件,如下:
    在这里插入图片描述
    不可读的乱码,我们用16进制方式打开:
    在这里插入图片描述

    已经有点可读的样子,跟代码比起来,可读性确实不高,但这就是接下来的任务,分析这些16进制。

    class结构

    下面是官方文档给出的定义:

    ClassFile {
        u4             magic; //魔数
        u2             minor_version; //次版本号
        u2             major_version; //主版本号
        u2             constant_pool_count; //常量池数量+1
        cp_info        constant_pool[constant_pool_count-1]; //常量池
        u2             access_flags; // 访问标识
        u2             this_class; // 常量池的有效下标
        u2             super_class; // 常量池的有效下标
        u2             interfaces_count; // 接口数
        u2             interfaces[interfaces_count];// 下标从0开始,元素为常量池的有效下标
        u2             fields_count;
        field_info     fields[fields_count];
        u2             methods_count;
        method_info    methods[methods_count];
        u2             attributes_count;
        attribute_info attributes[attributes_count];
    }
    

    为什么是CafeBabe

    其他地方的16进制没那么显眼,唯独开头的4个字节开起来像是个单词CAFEBABE.

    为什么所有文件都要有一个魔数开头,其实就是让JVM有一个稳定快速的途径来确认这个文件是字节码文件。

    为什么一定是CafeBabe,源于Java与咖啡的不解之缘。像是zip文件的PK.

    Unsupported major.minor version 51.0

    这个报错大家应该都见过,出现这个报错的时候都知道是JDK版本不对,立马去IDE上修改JDK编译版本、运行版本,OK报错解决。不过为什么JDK不一致时会报错呢,JVM是怎么确定版本不一致的?

    从字节码文件说,CafeBabe继续往后看八个字节,分别是00000034,我本地环境使用的是JDK1.8

    class文件中看到的是16进制,把0034转为10进制的数字就是52。我用JDK1.7编译之后,如下:
    在这里插入图片描述
    主版本号对应的两个字节,根据我们本地编译版本不同也会不同。

    下面是JDK版本与版本号对应关系:

    jdk版本major.minor version
    1.145
    1.246
    1.347
    1.448
    549
    650
    751
    852

    类的访问标识

    访问标识类型表:

    Flag NameValueInterpretation
    ACC_PUBLIC0x0001Declared public; may be accessed from outside its package.
    ACC_FINAL0x0010Declared final; no subclasses allowed.
    ACC_SUPER0x0020Treat superclass methods specially when invoked by the invokespecial instruction.
    ACC_INTERFACE0x0200Is an interface, not a class.
    ACC_ABSTRACT0x0400Declared abstract; must not be instantiated.
    ACC_SYNTHETIC0x1000Declared synthetic; not present in the source code.这个关键字不是源码生成,而是编译器生成的
    ACC_ANNOTATION0x2000Declared as an annotation type.
    ACC_ENUM0x4000Declared as an enum type.

    类型同时存在时进行+操作,如public final的值就是0x0011.

    ACC_SYNTHETIC类型是编译器根据实际情况生成,比如内部类的private方法在外部类调用的时候,违反了private只能本类调用的原则,但IDE编译时并不会报错,因为在生成内部类的时候加上了ACC_SYNTHETIC类型修饰

    常量池

    常量池数量是实际常量个数+1,常量池下标从1开始,到n-1结束;cp_info结构根据不同类型的常量,拥有不同的字节数,通用结构为:

    cp_info {
        u1 tag;
        u1 info[];//根据tag不同,长度不同
    }
    

    即每个结构体第一个字节标识了当前常量的类型,类型表如下:

    Constant TypeValue
    CONSTANT_Class7
    CONSTANT_Fieldref9
    CONSTANT_Methodref10
    CONSTANT_InterfaceMethodref11
    CONSTANT_String8
    CONSTANT_Integer3
    CONSTANT_Float4
    CONSTANT_Long5
    CONSTANT_Double6
    CONSTANT_NameAndType12
    CONSTANT_Utf81
    CONSTANT_MethodHandle15
    CONSTANT_MethodType16
    CONSTANT_InvokeDynamic18

    不同常量对应后续字节数不同,如CONSTANT_ClassCONSTANT_Utf8_info

    CONSTANT_Class_info {
        u1 tag;
        u2 name_index;//name_index需要是常量池中有效下标
    }
    
    CONSTANT_Utf8_info {
        u1 tag;
        u2 length; //bytes的长度,即字节数
        u1 bytes[length];
    }
    

    PS: 为什么constant_pool_count的值是常量池的数量+1,从1开始到n-1结束?不从0开始的原因是什么?

    这个问题在这里提一下,因为常量池中很多常量需要引用其他常量,而有可能存在常量并不需要任何有效引用,所以常量池空置了下标0的位置作为备用

    还是拿Hello World为例,复制前面一段来讲:

    CA FE BA BE 00 00 00 33  00 22 07 00 02 01 00 17
    63 6F 6D 2F 71 74 79 2F  66 69 72 73 74 2F 43 6C
    61 73 73 44 65 6D 6F 07  00 04 01 00 10 6A 61 76
    61 2F 6C 61 6E 67 2F 4F  62 6A 65 63 74 01 00 06
    
    • CA FE BA BE是魔数,00 00 00 33为主次版本号
    • 00 22表示常量池数量+1,0X22 = 34即常量池长度为33
    • 再往后一个字节就是第一个常量的tag,07从常量类型表中可以看到类型是CONSTANT_Class_info,那么第一个常量就是CONSTANT_Class_info,name_index为:00 02,即是常量池中第二个常量
    • 继续往后取一个字节就是第二个常量的tag,01CONSTANT_Utf8_info,那么接下来的两个自己就是bytes数组的长度即后续的字节数,0X0017 = 23也就是第二个常量还需要在读取23个字节63 6F 6D 2F 71 74 79 2F 66 69 72 73 74 2F 43 6C 61 73 73 44 65 6D 6F,这个23个字节转成字符串就是com/qty/first/ClassDemo也就是我们的类名

    PS : CONSTANT_Utf8_info中字符可以参考UTF-8编码的规则

    下面贴上所有常量类型的结构,如果有兴趣可以详细去了解每个类型的结构及其含义:

    CONSTANT_Fieldref_info {
        u1 tag;
        u2 class_index;
        u2 name_and_type_index;
    }
    
    CONSTANT_Methodref_info {
        u1 tag;
        u2 class_index;
        u2 name_and_type_index;
    }
    
    CONSTANT_InterfaceMethodref_info {
        u1 tag;
        u2 class_index;
        u2 name_and_type_index;
    }
    CONSTANT_String_info {
        u1 tag;
        u2 string_index;
    }
    CONSTANT_Integer_info {
        u1 tag;
        u4 bytes;
    }
    
    CONSTANT_Float_info {
        u1 tag;
        u4 bytes;
    }
    CONSTANT_Long_info {
        u1 tag;
        u4 high_bytes;
        u4 low_bytes;
    }
    
    CONSTANT_Double_info {
        u1 tag;
        u4 high_bytes;
        u4 low_bytes;
    }
    CONSTANT_NameAndType_info {
        u1 tag;
        u2 name_index;
        u2 descriptor_index;
    }
    CONSTANT_MethodHandle_info {
        u1 tag;
        u1 reference_kind;
        u2 reference_index;
    }
    CONSTANT_MethodType_info {
        u1 tag;
        u2 descriptor_index;
    }
    CONSTANT_InvokeDynamic_info {
        u1 tag;
        u2 bootstrap_method_attr_index;
        u2 name_and_type_index;
    }
    

    Field-字段

    field结构如下:

    field_info {
        u2             access_flags; //访问标识
        u2             name_index;
        u2             descriptor_index;
        u2             attributes_count; //属性个数
        attribute_info attributes[attributes_count];
    }
    

    field访问标识类型如下:

    Flag NameValueInterpretation
    ACC_PUBLIC0x0001Declared public; may be accessed from outside its package.
    ACC_PRIVATE0x0002Declared private; usable only within the defining class.
    ACC_PROTECTED0x0004Declared protected; may be accessed within subclasses.
    ACC_STATIC0x0008Declared static.
    ACC_FINAL0x0010Declared final; never directly assigned to after object construction (JLS §17.5).
    ACC_VOLATILE0x0040Declared volatile; cannot be cached.
    ACC_TRANSIENT0x0080Declared transient; not written or read by a persistent object manager.
    ACC_SYNTHETIC0x1000Declared synthetic; not present in the source code.
    ACC_ENUM0x4000Declared as an element of an enum.

    关于attribute_info后面再讲。

    Methods-方法

    method_info的结构如下:

    method_info {
        u2             access_flags; //访问标识
        u2             name_index;
        u2             descriptor_index;
        u2             attributes_count;
        attribute_info attributes[attributes_count];
    }
    

    类、字段与方法的访问标识类型都不太相同,方法的访问标识如下:

    Flag NameValueInterpretation
    ACC_PUBLIC0x0001Declared public; may be accessed from outside its package.
    ACC_PRIVATE0x0002Declared private; accessible only within the defining class.
    ACC_PROTECTED0x0004Declared protected; may be accessed within subclasses.
    ACC_STATIC0x0008Declared static.
    ACC_FINAL0x0010Declared final; must not be overridden (§5.4.5).
    ACC_SYNCHRONIZED0x0020Declared synchronized; invocation is wrapped by a monitor use.
    ACC_BRIDGE0x0040A bridge method, generated by the compiler.
    ACC_VARARGS0x0080Declared with variable number of arguments.
    ACC_NATIVE0x0100Declared native; implemented in a language other than Java.
    ACC_ABSTRACT0x0400Declared abstract; no implementation is provided.
    ACC_STRICT0x0800Declared strictfp; floating-point mode is FP-strict.
    ACC_SYNTHETIC0x1000Declared synthetic; not present in the source code.

    ACC_BRIDGE也是由编译器生成的,比如泛型的子类重写父类方法, 就会有一个在子类生成一个新的方法用ACC_BRIDGE标识

    ACC_VARARGS可变参数的方法会出现这个标记

    ACC_STRICT strictfp标识的方法中,所有float和double表达式都严格遵守FP-strict的限制,符合IEEE-754规范.

    Descriptors-描述

    方法和字段都有自己的描述信息,方法的描述包括参数、返回值的类型,字段描述为字段的类型,下面是类型表:

    FieldType termTypeInterpretation
    Bbytesigned byte
    CcharUnicode character code point in the Basic Multilingual Plane, encoded with UTF-16
    Ddoubledouble-precision floating-point value
    Ffloatsingle-precision floating-point value
    Iintinteger
    Jlonglong integer
    L ClassName ;referencean instance of class ClassName
    Sshortsigned short
    Zbooleantrue or false
    [referenceone array dimension

    方法描述格式为:( {ParameterDescriptor} ) ReturnDescriptor

    例如:

    Object m(int i, double d, Thread t);
    

    描述信息就是:(IDLjava/lang/Thread;)Ljava/lang/Object;

    对象类型的后面需要用;分割,基础类型不需要

    attribute-属性

    attribute_info类型比较多,这里只把我们最关心的代码说下,即Code_attribute:

    Code_attribute {
        u2 attribute_name_index;
        u4 attribute_length;
        u2 max_stack;
        u2 max_locals;
        u4 code_length;
        u1 code[code_length];
        u2 exception_table_length;
        {   u2 start_pc;
            u2 end_pc;
            u2 handler_pc;
            u2 catch_type;
        } exception_table[exception_table_length];
        u2 attributes_count;
        attribute_info attributes[attributes_count];
    }
    

    只要不是native、abstact修饰的方法,必须含有Code_attribute属性

    Code_attribute中包含codeexceptionattribute_info等信息,这里主要说下code中的内容。

    code数组中的内容就是方法中编译后的代码:

             0: aload_0
             1: invokespecial #10                 // Method java/lang/Object."<init>":()V
             4: return
    

    这个就是我们上面那个类的无参构造函数编译后的效果,那这里面的aload_0invokespecialreturn学过JVM相关知识的话,大家已经很熟悉了.

    • aload_0就是变量0进栈
    • invokespecial调用实例的初始化方法,即构造方法
    • return 即方法结束,返回值为void

    那这些aload_0invokespecialreturn相关的指令是如何存储在code数组中的,或者说是以什么形式存在的?

    其实JVM有这样一个指令数组,code数组中的记录的就是指令数组的有效下标,下面是部分指令:

    JVM指令指令下标描述
    return0xB1当前方法返回void
    areturn0xB0从方法中返回一个对象的引用
    ireturn0xAC当前方法返回int
    iload_00x1A第一个int型局部变量进栈
    lload_00x1E第一个long型局部变量进栈
    istore_00x3B将栈顶int型数值存入第一个局部变量
    lstore_00x3F将栈顶long型数值存入第一个局部变量
    getstatic0xB2获取指定类的静态域,并将其值压入栈顶
    putstatic0xB3为指定的类的静态域赋值
    invokespecial0xB7调用超类构造方法、实例初始化方法、私有方法
    invokevirtual0xB6调用实例方法
    iadd0x60栈顶两int型数值相加,并且结果进栈
    iconst_00x03int型常量值0进栈
    ldc0x12将int、float或String型常量值从常量池中推送至栈顶

    详细指令列表可以查看官方文档

    关于attribute_info还有其他类型,有兴趣的可以查看Attribute,类型及其出现位置如下:

    AttributeLocation
    SourceFileClassFile
    InnerClassesClassFile
    EnclosingMethodClassFile
    SourceDebugExtensionClassFile
    BootstrapMethodsClassFile
    ConstantValuefield_info
    Codemethod_info
    Exceptionsmethod_info
    RuntimeVisibleParameterAnnotations, RuntimeInvisibleParameterAnnotationsmethod_info
    AnnotationDefaultmethod_info
    MethodParametersmethod_info
    SyntheticClassFile, field_info, method_info
    DeprecatedClassFile, field_info, method_info
    SignatureClassFile, field_info, method_info
    RuntimeVisibleAnnotations, RuntimeInvisibleAnnotationsClassFile, field_info, method_info
    LineNumberTableCode
    LocalVariableTableCode
    LocalVariableTypeTableCode
    StackMapTableCode
    RuntimeVisibleTypeAnnotations, RuntimeInvisibleTypeAnnotationsClassFile, field_info, method_info, Code

    javap

    熟悉16进制内容后,再来看看JDK提供的工具:

    javap -verbose ClassDemo.class
    

    可以参照反编译效果对比之前16进制文件的分析,输入如下:

    Classfile /D:/eclipse-workspace/class-demo/bin/com/qty/first/ClassDemo.class
      Last modified 2020-10-7; size 560 bytes
      MD5 checksum 9e627e92c2887591a4d9d1cfd11d1f89
      Compiled from "ClassDemo.java"
    public class com.qty.first.ClassDemo
      minor version: 0
      major version: 51
      flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
       #1 = Class              #2             // com/qty/first/ClassDemo
       #2 = Utf8               com/qty/first/ClassDemo
       #3 = Class              #4             // java/lang/Object
       #4 = Utf8               java/lang/Object
       #5 = Utf8               <init>
       #6 = Utf8               ()V
       #7 = Utf8               Code
       #8 = Methodref          #3.#9          // java/lang/Object."<init>":()V
       #9 = NameAndType        #5:#6          // "<init>":()V
      #10 = Utf8               LineNumberTable
      #11 = Utf8               LocalVariableTable
      #12 = Utf8               this
      #13 = Utf8               Lcom/qty/first/ClassDemo;
      #14 = Utf8               main
      #15 = Utf8               ([Ljava/lang/String;)V
      #16 = Fieldref           #17.#19        // java/lang/System.out:Ljava/io/PrintStream;
      #17 = Class              #18            // java/lang/System
      #18 = Utf8               java/lang/System
      #19 = NameAndType        #20:#21        // out:Ljava/io/PrintStream;
      #20 = Utf8               out
      #21 = Utf8               Ljava/io/PrintStream;
      #22 = String             #23            // hello world!!
      #23 = Utf8               hello world!!
      #24 = Methodref          #25.#27        // java/io/PrintStream.println:(Ljava/lang/String;)V
      #25 = Class              #26            // java/io/PrintStream
      #26 = Utf8               java/io/PrintStream
      #27 = NameAndType        #28:#29        // println:(Ljava/lang/String;)V
      #28 = Utf8               println
      #29 = Utf8               (Ljava/lang/String;)V
      #30 = Utf8               args
      #31 = Utf8               [Ljava/lang/String;
      #32 = Utf8               SourceFile
      #33 = Utf8               ClassDemo.java
    {
      public com.qty.first.ClassDemo();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: invokespecial #8                  // Method java/lang/Object."<init>":()V
             4: return
          LineNumberTable:
            line 3: 0
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       5     0  this   Lcom/qty/first/ClassDemo;
    
      public static void main(java.lang.String[]);
        descriptor: ([Ljava/lang/String;)V
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=2, locals=1, args_size=1
             0: getstatic     #16                 // Field java/lang/System.out:Ljava/io/PrintStream;
             3: ldc           #22                 // String hello world!!
             5: invokevirtual #24                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
             8: return
          LineNumberTable:
            line 6: 0
            line 7: 8
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       9     0  args   [Ljava/lang/String;
    }
    SourceFile: "ClassDemo.java"
    

    字节码技术应用

    字节码技术的应用场景包括但不限于AOP,动态生成代码,接下来讲一下字节码技术相关的第三方类库,第三方框架的讲解是为了帮助大家了解字节码技术的应用方向,文档并没有对框架机制进行详细分析,有兴趣的可以去了解相关框架实现原理和架构,也可以后续为大家奉上相关详细讲解。

    ASM

    ASM 是一个 Java 字节码操控框架,它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。

    说白了,ASM可以在不修改Java源码文件的情况下,直接对Class文件进行修改,改变或增强原有类功能。

    在熟悉了字节码原理的情况下,理解动态修改字节码技术会更加容易,接下来我们只针对ASM框架中几个主要类进行分析,并举个栗子帮助大家理解。

    主要类介绍

    ClassVisitor

    提供各种对字节码操作的方法,包括对属性、方法、注解等内容的修改:

    public abstract class ClassVisitor {
        /**
         * 	构造函数
         * @param api api的值必须等当前ASM版本号一直,否则报错
         */
        public ClassVisitor(final int api) {
            this(api, null);
        }
        
        /**
         * 对类的头部信息进行修改
         *
         * @param version 版本号,从Opcodes中获取
         * @param access 访问标识,多种类型叠加使用'+'
         * @param name 类名,带报名路径,使用'/'分割
         * @param signature 签名
         * @param superName 父类
         * @param interfaces 接口列表
         */
        public void visit(int version,int access,String name,String signature,String superName,String[] interfaces)
        {
            if (cv != null) {
                cv.visit(version, access, name, signature, superName, interfaces);
            }
        }
        
        /**
    	 * 对字段进行修改
    	 * 
    	 * @param access    访问标识
    	 * @param name      字段名称
    	 * @param desc      描述
    	 * @param signature 签名
    	 * @param value     字段值
    	 * @return
    	 */
    	public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
    		if (cv != null) {
    			return cv.visitField(access, name, desc, signature, value);
    		}
    		return null;
    	}
    	
    	/**
    	 * 对方法进行修改
    	 * 
    	 * @param access     访问标识
    	 * @param name       方法名称
    	 * @param desc       方法描述
    	 * @param signature  签名
    	 * @param exceptions 异常列表
    	 * @return
    	 */
    	public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
    		if (cv != null) {
    			return cv.visitMethod(access, name, desc, signature, exceptions);
    		}
    		return null;
    	}
    	
    	/**
    	* 终止编辑,对当前类的编辑结束时调用
    	*/
    	public void visitEnd() {
    		if (cv != null) {
    			cv.visitEnd();
    		}
    	}
    
    }
    
    ClassWriter

    主要功能就是记录所有字节码相关字段,并提供转换为字节数组的方法:

    //ClassWriter继承了ClassVisitor 即拥有了对class修改的功能
    public class ClassWriter extends ClassVisitor {
        //下面这些成员变量,是不是很眼熟了
        private int access;
        private int name;
        String thisName;
        private int signature;
        private int superName;
        private int interfaceCount;
        private int[] interfaces;
        private int sourceFile;
        private Attribute attrs;
        private int innerClassesCount;
        private ByteVector innerClasses;
    	FieldWriter firstField;
        MethodWriter firstMethod;
        
        //这个就是将缓存的字节码封装对象再进行转换,按照Class文件格式转成字节数组
        public byte[] toByteArray() {
        }
    }
    
    ClassReader
    //读取Class文件
    public class ClassReader {
        /**
         * 构造函数
         * @param b Class文件的字节数组
         */
    	public ClassReader(final byte[] b) {
    		this(b, 0, b.length);
    	}
        
        /**
         * 相当于将ClassReader中读取到的数据,转存到classVisitor中,后续通过使用ClassVisitor的API对原Class进行修改、增强
         * @param classVisitor
         * @param flags
         */
        public void accept(final ClassVisitor classVisitor, final int flags) {
            accept(classVisitor, new Attribute[0], flags);
        }
        
    }
    
    Opcodes
    public interface Opcodes {
        //这里面的内容就是前面讲到的JVM指令集合和各种访问标识等常量
        // access flags
        int ACC_PUBLIC = 0x0001; // class, field, method
        int ACC_PRIVATE = 0x0002; // class, field, method
        int ACC_PROTECTED = 0x0004; // class, field, method
        int ACC_STATIC = 0x0008; // field, method
        int ACC_FINAL = 0x0010; // class, field, method
        int ACC_SUPER = 0x0020; // class
        int ACC_SYNCHRONIZED = 0x0020; // method
        int ACC_VOLATILE = 0x0040; // field
        int ACC_BRIDGE = 0x0040; // method
        int ACC_VARARGS = 0x0080; // method
        int ACC_TRANSIENT = 0x0080; // field
        int ACC_NATIVE = 0x0100; // method
        int ACC_INTERFACE = 0x0200; // class
        int ACC_ABSTRACT = 0x0400; // class, method
        int ACC_STRICT = 0x0800; // method
        int ACC_SYNTHETIC = 0x1000; // class, field, method
        int ACC_ANNOTATION = 0x2000; // class
        int ACC_ENUM = 0x4000; // class(?) field inner
        
        int NOP = 0; // visitInsn
        int ACONST_NULL = 1; // -
        int ICONST_M1 = 2; // -
        int ICONST_0 = 3; // -
        int ICONST_1 = 4; // -
        int ICONST_2 = 5; // -
        int ICONST_3 = 6; // -
        int ICONST_4 = 7; // -
        int ICONST_5 = 8; // -
        int LCONST_0 = 9; // -
        int LCONST_1 = 10; // -
        int FCONST_0 = 11; // -
        int FCONST_1 = 12; // -
        int FCONST_2 = 13; // -
        int DCONST_0 = 14; // -
        int DCONST_1 = 15; // -
        int BIPUSH = 16; // visitIntInsn
        int SIPUSH = 17; // -
        int LDC = 18; // visitLdcInsn
       
    }
    

    以上这些类都只是截取其中一部分,旨在讲解思路。

    举个栗子

    废话不多说,直接献上代码:

    package com.qty.classloader;
    
    import java.io.File;
    import java.io.FileOutputStream;
    import java.lang.reflect.Method;
    
    import org.objectweb.asm.ClassWriter;
    import org.objectweb.asm.MethodVisitor;
    import org.objectweb.asm.Opcodes;
    
    public class AsmDemo {
    
    	public static void main(String[] args) throws Exception {
    		// 生成一个类只需要ClassWriter组件即可
    		ClassWriter cw = new ClassWriter(0);
    		// 通过visit方法确定类的头部信息
            //相当于 public class Custom 编译版本1.7
    		cw.visit(Opcodes.V1_7, Opcodes.ACC_PUBLIC, "com/qty/classloader/Custom", null, "java/lang/Object", null);
    		// 生成默认的构造方法
    		MethodVisitor mw = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
    
    		// 生成构造方法的字节码指令
            // aload_0 加载0位置的局部变量,即this
    		mw.visitVarInsn(Opcodes.ALOAD, 0);
            // 调用初始化函数
    		mw.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V");
    		mw.visitInsn(Opcodes.RETURN);
            //maxs编辑的是最大栈深度和最大局部变量个数
    		mw.visitMaxs(1, 1);
    
    		// 生成方法 public void doSomeThing(String value)
    		mw = cw.visitMethod(Opcodes.ACC_PUBLIC, "doSomeThing", "(Ljava/lang/String;)V", null, null);
    
    		// 生成方法中的字节码指令
            //相当于 System.out.println(value);
    		mw.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
    		mw.visitVarInsn(Opcodes.ALOAD, 1);
    		mw.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V");
    		mw.visitInsn(Opcodes.RETURN);
    		mw.visitMaxs(2, 2);
    
    		cw.visitEnd(); // 使cw类已经完成
    		// 将cw转换成字节数组写到文件里面去
    		byte[] data = cw.toByteArray();
            //这里需要输出到对应项目的classes的目录下
    		File file = new File("./target/classes/com/qty/classloader/Custom.class");
    		FileOutputStream fout = new FileOutputStream(file);
    		fout.write(data);
    		fout.close();
            //class生成了,试一下能不能正确运行
    		Class<?> exampleClass = Class.forName("com.qty.classloader.Custom");
    		Method method = exampleClass.getDeclaredMethod("doSomeThing", String.class);
    		Object o = exampleClass.newInstance();
    		method.invoke(o, "this is a test!");
    	}
    }
    
    

    以上代码在我本地跑通没有问题,且能够正确输出this is a test!.

    使用命令看一下反编译效果:

      Last modified 2020-10-7; size 320 bytes
      MD5 checksum eed71ac57da1174f4adf0910a9fa338a
    public class com.qty.classloader.Custom
      minor version: 0
      major version: 51
      flags: ACC_PUBLIC
    Constant pool:
       #1 = Utf8               com/qty/classloader/Custom
       #2 = Class              #1             // com/qty/classloader/Custom
       #3 = Utf8               java/lang/Object
       #4 = Class              #3             // java/lang/Object
       #5 = Utf8               <init>
       #6 = Utf8               ()V
       #7 = NameAndType        #5:#6          // "<init>":()V
       #8 = Methodref          #4.#7          // java/lang/Object."<init>":()V
       #9 = Utf8               doSomeThing
      #10 = Utf8               (Ljava/lang/String;)V
      #11 = Utf8               java/lang/System
      #12 = Class              #11            // java/lang/System
      #13 = Utf8               out
      #14 = Utf8               Ljava/io/PrintStream;
      #15 = NameAndType        #13:#14        // out:Ljava/io/PrintStream;
      #16 = Fieldref           #12.#15        // java/lang/System.out:Ljava/io/PrintStream;
      #17 = Utf8               java/io/PrintStream
      #18 = Class              #17            // java/io/PrintStream
      #19 = Utf8               println
      #20 = NameAndType        #19:#10        // println:(Ljava/lang/String;)V
      #21 = Methodref          #18.#20        // java/io/PrintStream.println:(Ljava/lang/String;)V
      #22 = Utf8               Code
    {
      public com.qty.classloader.Custom();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: invokespecial #8                  // Method java/lang/Object."<init>":()V
             4: return
    
      public void doSomeThing(java.lang.String);
        descriptor: (Ljava/lang/String;)V
        flags: ACC_PUBLIC
        Code:
          stack=2, locals=2, args_size=2
             0: getstatic     #16                 // Field java/lang/System.out:Ljava/io/PrintStream;
             3: aload_1
             4: invokevirtual #21                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
             7: return
    }
    

    ASM除了可以动态生成新的Class文件,还可以修改原有Class文件的功能或者在原Class文件新增方法字段等,这里不再举例子,有兴趣的可以自己研究一下。不过大家已经发现,使用ASM动态修改Class文件,难度还是有的,需要使用者对JVM指令、Class格式相当熟悉,

    除了ASM,还有其他第三方工具也提供了对字节码的动态修改,包括CGLib,Javassisit,AspectJ等,而这些框架相比于ASM,则是将JVM指令级别的编码封装起来,让使用者直接使用Java代码编辑,使用更加方便。

    想要详细了解ASM,可以参考ASM官方文档.

    IDEA插件 ASM byteCode Outline 可以直接看到代码的JVM操作指令.

    Javassisit

    Javassisit官方文档

    与ASM一样,Javassist也是一个处理Java字节码的类库。

    主要类介绍

    ClassPool

    主要负责加载或者生产class文件

    public class ClassPool {
        //新建一个class,classname为类的全限类名
        public CtClass makeClass(String classname) throws RuntimeException {
            return makeClass(classname, null);
        }
        //增加一个jar包或者目录供搜索class使用
        public ClassPath insertClassPath(String pathname) throws NotFoundException
        {
            return source.insertClassPath(pathname);
        }
        //从搜索目录中找到对应class并返回CtClass引用供后续功能使用
        public CtClass get(String classname) throws NotFoundException {
        }
        
    }
    
    CtClass

    一个CtClass对象对应一个Class字节码对象。

    public abstract class CtClass {
        //为class增加接口、字段、方法
    	public void addInterface(CtClass anInterface) {}
        public void addField(CtField f) throws CannotCompileException {}
        public void addMethod(CtMethod m) throws CannotCompileException {}
        
        //在指定目录生产class文件
        public void writeFile(String directoryName) throws CannotCompileException, IOException{}
        //生成class对象到当前JVM中,即加载当前修改的Class对象
        public Class<?> toClass() throws CannotCompileException {}
    }
    
    CtMethod

    对应class中的Method

    public final class CtMethod extends CtBehavior {
        //修改方法名
        public void setName(String newname) {}
        //修改方法体
        public void setBody(CtMethod src, ClassMap map) throws CannotCompileException{}
    }
    
    CtBehavior
    public abstract class CtBehavior extends CtMember {
        //设置方法体
        public void setBody(String src) throws CannotCompileException {}
        //在方法体前插入代码
       	public void insertBefore(String src) throws CannotCompileException {}
        //在方法体最后插入代码
        public void insertAfter(String src) throws CannotCompileException {}
    }
    

    再举个栗子

    public class SsisitDemo {
    	public static void main(String[] args) throws Exception {
    		ClassPool pool = ClassPool.getDefault();
    		CtClass ct = pool.makeClass("com.qty.GenerateClass");// 创建类
    		ct.setInterfaces(new CtClass[] { pool.makeInterface("java.lang.Cloneable") });// 让类实现Cloneable接口
    		try {
    			CtField f = new CtField(CtClass.intType, "id", ct);// 获得一个类型为int,名称为id的字段
    			f.setModifiers(AccessFlag.PUBLIC);// 将字段设置为public
    			ct.addField(f);// 将字段设置到类上
    			// 添加构造函数
    			CtConstructor constructor = CtNewConstructor.make("public GeneratedClass(int pId){this.id=pId;}", ct);
    			ct.addConstructor(constructor);
    			// 添加方法
    			CtMethod helloM = CtNewMethod.make("public void hello(String des){ System.out.println(des+this.id);}", ct);
    			ct.addMethod(helloM);
    			ct.writeFile("./target/classes");// 将生成的.class文件保存到磁盘
    
    			// 下面的代码为验证代码
    			Class<?> clazz = Class.forName("com.qty.GenerateClass");
    			Field[] fields = clazz.getFields();
    			System.out.println("属性名称:" + fields[0].getName() + "  属性类型:" + fields[0].getType());
    			Constructor<?> con = clazz.getConstructor(int.class);
    			Method me = clazz.getMethod("hello", String.class);
    			me.invoke(con.newInstance(12), "this is a test-- ");
    		} catch (CannotCompileException e) {
    			e.printStackTrace();
    		} catch (IOException e) {
    			e.printStackTrace();
    		}
    	}
    }
    

    输出如下:

    属性名称:id  属性类型:int
    this is a test-- 12
    

    使用javap -c查看:

    Compiled from "GenerateClass.java"
    public class com.qty.GenerateClass implements java.lang.Cloneable {
      public int id;
    
      public com.qty.GenerateClass(int);
        Code:
           0: aload_0
           1: invokespecial #15                 // Method java/lang/Object."<init>":()V
           4: aload_0
           5: iload_1
           6: putfield      #17                 // Field id:I
           9: return
    
      public void hello(java.lang.String);
        Code:
           0: getstatic     #26                 // Field java/lang/System.out:Ljava/io/PrintStream;
           3: new           #28                 // class java/lang/StringBuffer
           6: dup
           7: invokespecial #29                 // Method java/lang/StringBuffer."<init>":()V
          10: aload_1
          11: invokevirtual #33                 // Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
          14: aload_0
          15: getfield      #35                 // Field id:I
          18: invokevirtual #38                 // Method java/lang/StringBuffer.append:(I)Ljava/lang/StringBuffer;
          21: invokevirtual #42                 // Method java/lang/StringBuffer.toString:()Ljava/lang/String;
          24: invokevirtual #47                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
          27: return
    }
    

    Class加载

    上面讲到所有的内容都是Demo级别的例子,并没有从项目使用层面来分析这些技术如何使用。比如,我们修改的字节码何时加载到JVM?运行中的项目如果动态修改某个类的实现,怎么加载?

    ClassLoader

    ClassLoader双亲委托机制确保了每个Class只能被一个ClassLoader加载,每个ClassLoader关注自己的资源目录:

    • BootStrapClassLoader -> <JAVA_HOME>/lib或者-Xbootclasspath指定的路径
    • ExtClassLoader -> <JAVA_HOME>/lib/ext或者-Djava.ext.dir指定的路径
    • AppClassLoader -> 项目classPath目录,通常就是classes目录和moven引用的jar包

    上面的例子中,自动生成的Class文件都是直接放到项目classpath下,可以直接被AppClassLoader获取到,所以可以直接使用Class.forName获取到class对象。但之前的例子都是直接生成新的class文件,如果是修改已经加载好的class文件会是什么效果,我们接着看栗子:

    package com.qty.first;
    public class SsisitObj {
    	private String name;
    	public void sayMyName() {
    		System.out.println("My name is " + name);
    	}
    	public String getName() {
    		return name;
    	}
    	public void setName(String name) {
    		this.name = name;
    	}
    }
    

    正常设置name之后,调用sayMyName会输出自己的名字。现在要在项目运行中对这个class进行修改,使sayMyName除了打印出自己名字外,还要在打印之前输出开始结束标记。

    package com.qty.first;
    
    import javassist.ClassPool;
    import javassist.CtClass;
    import javassist.CtMethod;
    
    public class ClassDemo {
    
    	public static void main(String[] args) throws Exception {
    		SsisitObj obj = new SsisitObj();
    		obj.setName("Jack");
    		obj.sayMyName();
    		addCutPoint();
    		obj.sayMyName();
    	}
    	//对SsisitObj中方法进行修改
    	private static void addCutPoint() {
    		try {
    			ClassPool pool = ClassPool.getDefault();
    			pool.insertClassPath("target/classes/com/qty/first");
    			CtClass cc = pool.get("com.qty.first.SsisitObj");
                //定位到方法
    			CtMethod fMethod = cc.getDeclaredMethod("sayMyName");
                //覆盖发放内容
    			fMethod.setBody("{" 
                                + "System.out.println(\"Method start. \");"
    							+ "System.out.println(\"My name is \" + name);" 
                                + "System.out.println(\"Method end.  \");}");
                //生成class并加载
    			cc.toClass();
    		} catch (Exception e) {
    			e.printStackTrace();
    		}
    	}
    }
    
    

    上面这个例子一定会报错attempted duplicate class definition for name: "com/qty/first/SsisitObj"

    因为Classloader并没有卸载class的方法,所以一旦class被加载到JVM之后,就不可以再次被加载,那是不是有其他方案?

    上栗子:

    package com.qty.first;
    
    import java.io.File;
    
    import javassist.ClassPool;
    import javassist.CtClass;
    import javassist.CtMethod;
    
    public class ClassDemo {
    	private static String url = "./com/qty/first/SsisitObj.class";
    
    	public static void main(String[] args) throws Exception {
    		ISaySomething obj = loadFile().newInstance();
    		obj.setName("jack");
    		obj.sayMyName();
    		addCutPoint();
    		System.out.println("-----------我是分割线-----------------");
    		obj = loadFile().newInstance();
    		obj.setName("jack");
    		obj.sayMyName();
    	}
    
    	//代码只是示意,如果真实需求需要使用自定义classLoader加载,那么会缓存当前ClassLoader
        //当Class对象更改时再进行更换
    	private static Class<ISaySomething> loadFile() throws Exception {
    		MyClassLoader loader = new MyClassLoader();
    		File file = new File(url);
    		loader.addURLFile(file.toURI().toURL());
    		Class<ISaySomething> clazz = (Class<ISaySomething>) loader.createClass("com.qty.first.SsisitObj");
    		return clazz;
    	}
    
    	private static void addCutPoint() {
    		try {
    			ClassPool pool = ClassPool.getDefault();
    			pool.insertClassPath("target/classes/com/qty/first");
    			CtClass cc = pool.get("com.qty.first.SsisitObj");
    			CtMethod fMethod = cc.getDeclaredMethod("sayMyName");
    			fMethod.setBody("{" 
                                + "System.out.println(\"Method start. \");"
    							+ "System.out.println(\"My name is \" + name);" 
                            	+ "System.out.println(\"Method end.  \");}");
    			cc.writeFile("./");
    			url = "./com/qty/first/SsisitObj.class";
    		} catch (Exception e) {
    			e.printStackTrace();
    
    		}
    	}
    }
    
    
    package com.qty.first;
    
    import java.io.ByteArrayOutputStream;
    import java.io.File;
    import java.io.FileInputStream;
    import java.net.URL;
    import java.net.URLClassLoader;
    import java.net.URLConnection;
    
    public class MyClassLoader extends URLClassLoader {
    
    	public MyClassLoader() {
    		super(new URL[] {}, findParentClassLoader());
    	}
    
    	/**
    	 * 定位基于当前上下文的父类加载器
    	 * 
    	 * @return 返回可用的父类加载器.
    	 */
    	private static ClassLoader findParentClassLoader() {
    		ClassLoader parent = MyClassLoader.class.getClassLoader();
    		if (parent == null) {
    			parent = MyClassLoader.class.getClassLoader();
    		}
    		if (parent == null) {
    			parent = ClassLoader.getSystemClassLoader();
    		}
    		return parent;
    	}
    
    	private URLConnection cachedFile = null;
    
    	/**
    	 * 将指定的文件url添加到类加载器的classpath中去,并缓存jar connection,方便以后卸载jar
    	 * 一个可想类加载器的classpath中添加的文件url
    	 * 
    	 * @param
    	 */
    	public void addURLFile(URL file) {
    		try {
    			// 打开并缓存文件url连接
    			URLConnection uc = file.openConnection();
    			uc.setUseCaches(true);
    			cachedFile = uc;
    		} catch (Exception e) {
    			System.err.println("Failed to cache plugin JAR file: " + file.toExternalForm());
    		}
    		addURL(file);
    	}
    
    	/**
    	 * 绕过双亲委派逻辑,直接获取Class
    	 */
    	public Class<?> createClass(String name) throws Exception {
    		byte[] data;
    		data = readClassFile(name);
    		return defineClass(name, data, 0, data.length);
    	}
    
    	// 获取要加载 的class文件名
    	private String getFileName(String name) {
    		int index = name.lastIndexOf('.');
    		if (index == -1) {
    			return name + ".class";
    		} else {
    			return name.replace(".", "/")+".class";
    		}
    	}
    
    	/**
    	 * 读取Class文件
    	 */
    	private byte[] readClassFile(String name) throws Exception {
    		String fileName = getFileName(name);
    		File file = new File(fileName);
    		FileInputStream is = new FileInputStream(file);
    		ByteArrayOutputStream bos = new ByteArrayOutputStream();
    		int len = 0;
    		while ((len = is.read()) != -1) {
    			bos.write(len);
    		}
    		byte[] data = bos.toByteArray();
    		is.close();
    		bos.close();
    		return data;
    	}
    
    }
    

    输出:

    My name is jack
    -----------我是分割线-----------------
    Method start. 
    My name is jack
    Method end.  
    

    这个栗子只是示意,也就是说当使用自定义Classloader的时候,是可以通过更换Classloader来实现重新加载Class的需求。

    Instrument

    在 JDK 1.5 中,Java 引入了java.lang.Instrument包,该包提供了一些工具帮助开发人员在 Java 程序运行时,动态修改系统中的 Class 类型。其中,使用该软件包的一个关键组件就是 Java agent。

    相比classloader对未加载到JVM中的class进行修改,使用Instrument可以在运行时对已经加载的class文件重定义。

    最后的栗子:

    package com.qty.second;
    
    import java.lang.instrument.ClassDefinition;
    import java.lang.instrument.UnmodifiableClassException;
    
    import com.qty.MyAgent;
    
    import javassist.ClassPool;
    import javassist.CtClass;
    import javassist.CtMethod;
    
    public class ClassDemo {
    	public static void main(String[] args) throws ClassNotFoundException, UnmodifiableClassException {
    		SsisitObj obj = new SsisitObj();
    		obj.setName("Tom");
    		obj.sayMyName();
    		ClassDefinition definition = new ClassDefinition(obj.getClass(), getEditClass());
    		MyAgent.getIns().redefineClasses(definition);
    		obj = new SsisitObj();
    		obj.setName("Jack");
    		obj.sayMyName();
    	}
    	
    	private static byte[] getEditClass() {
    		try {
    			ClassPool pool = ClassPool.getDefault();
    			pool.insertClassPath("target/classes/com/qty/second");
    			CtClass cc = pool.get("com.qty.second.SsisitObj");
    			CtMethod fMethod = cc.getDeclaredMethod("sayMyName");
    			fMethod.setBody("{" + "System.out.println(\"Method start. \");"
    					+ "System.out.println(\"My name is \" + name);" + "System.out.println(\"Method end.  \");}");
    			return cc.toBytecode();
    		} catch (Exception e) {
    			e.printStackTrace();
    
    		}
    		return null;
    	}
    }
    
    

    结语

    本次分享的重点内容是字节码技术的入门介绍。在了解字节码结构等相关知识之后,通过举例的方式了解一下字节码技术相关应用方法,以及如何将字节码技术运用到实际项目中。

    本次分享就到此为止,谢谢支持。

    引申

    既然JVM运行时识别的只是.class文件,而文件格式我们也了解,那是不是只要我们能够正确生成.class文件就可以直接运行,甚至可以不用Java语言?

    答案大家肯定都知道了,当然可以。Kotlin,Scala,Groovy,Jython,JRuby…这些都是基于JVM的编程语言。

    那如果我们想自己实现一款基于JVM的开发语言,怎么搞?

    1. 定义语义,静态,动态?强类型,弱类型?
    2. 定义语法,关键字(if,else,break,return…)
    3. 定义代码编译器,如何将自己的代码编译成.class

    有兴趣的大佬,可以试试

    还可以继续引申,语义语法都定义好了,是不是可以实现编译器直接编译成.exe文件,或者linux下可以运行程序?

    待续

    • Class加载详细过程,如JVM如何将指令生成对应代码
    • 字节码技术相关框架详解,ASM,CGLib,Javassisit,AspectJ,JDK Proxy
    • ClassLoader详解
    • Java Agent
    展开全文
  • jvm字节码自动加载

    2018-02-07 09:47:44
    jvm字节码自动加载jvm字节码自动加载jvm字节码自动加载jvm字节码自动加载jvm字节码自动加载
  • Java字节码增强探秘

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

    1.字节码

    1.1 什么是字节码?

    Java之所以可以“一次编译,到处运行”,一是因为JVM针对各种操作系统、平台都进行了定制,二是因为无论在什么平台,都可以编译生成固定格式的字节码(.class文件)供JVM使用。因此,也可以看出字节码对于Java生态的重要性。之所以被称之为字节码,是因为字节码文件由十六进制值组成,而JVM以两个十六进制值为一组,即以字节为单位进行读取。在Java中一般是用javac命令编译源代码为字节码文件,一个.java文件从编译到运行的示例如图1所示。

    图1 Java运行示意图

    对于开发人员,了解字节码可以更准确、直观地理解Java语言中更深层次的东西,比如通过字节码,可以很直观地看到Volatile关键字如何在字节码上生效。另外,字节码增强技术在Spring AOP、各种ORM框架、热部署中的应用屡见不鲜,深入理解其原理对于我们来说大有裨益。除此之外,由于JVM规范的存在,只要最终可以生成符合规范的字节码就可以在JVM上运行,因此这就给了各种运行在JVM上的语言(如Scala、Groovy、Kotlin)一种契机,可以扩展Java所没有的特性或者实现各种语法糖。理解字节码后再学习这些语言,可以“逆流而上”,从字节码视角看它的设计思路,学习起来也“易如反掌”。

    本文重点着眼于字节码增强技术,从字节码开始逐层向上,由JVM字节码操作集合到Java中操作字节码的框架,再到我们熟悉的各类框架原理及应用,也都会一一进行介绍。

    1.2 字节码结构

    .java文件通过javac编译后将得到一个.class文件,比如编写一个简单的ByteCodeDemo类,如下图2的左侧部分:

    图2 示例代码(左侧)及对应的字节码(右侧)

    编译后生成ByteCodeDemo.class文件,打开后是一堆十六进制数,按字节为单位进行分割后展示如图2右侧部分所示。上文提及过,JVM对于字节码是有规范要求的,那么看似杂乱的十六进制符合什么结构呢?JVM规范要求每一个字节码文件都要由十部分按照固定的顺序组成,整体结构如图3所示。接下来我们将一一介绍这十部分:

    图3 JVM规定的字节码结构

    (1) 魔数(Magic Number)

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

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

    (2) 版本号

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

    (3) 常量池(Constant Pool)

    紧接着主版本号之后的字节为常量池入口。常量池中存储两类常量:字面量与符号引用。字面量为代码中声明为Final的常量值,符号引用如类和接口的全局限定名、字段的名称和描述符、方法的名称和描述符。常量池整体上分为两部分:常量池计数器以及常量池数据区,如下图4所示。

    图4 常量池的结构

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

    图5 前十个字节及含义

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

    图6 各类型的cp_info

    具体以CONSTANT_utf8_info为例,它的结构如下图7左侧所示。首先一个字节“tag”,它的值取自上图6中对应项的Tag,由于它的类型是utf8_info,所以值为“01”。接下来两个字节标识该字符串的长度Length,然后Length个字节为这个字符串具体的值。从图2中的字节码摘取一个cp_info结构,如下图7右侧所示。将它翻译过来后,其含义为:该常量类型为utf8字符串,长度为一字节,数据为“a”。

    图7 CONSTANT_utf8_info的结构(左)及示例(右)

    其他类型的cp_info结构在本文不再赘述,整体结构大同小异,都是先通过Tag来标识类型,然后后续n个字节来描述长度和(或)数据。先知其所以然,以后可以通过javap -verbose ByteCodeDemo命令,查看JVM反编译后的完整常量池,如下图8所示。可以看到反编译结果将每一个cp_info结构的类型和值都很明确地呈现了出来。

    图8 常量池反编译结果

    (4) 访问标志

    常量池结束之后的两个字节,描述该Class是类还是接口,以及是否被Public、Abstract、Final等修饰符修饰。JVM规范规定了如下图9的访问标志(Access_Flag)。需要注意的是,JVM并没有穷举所有的访问标志,而是使用按位或操作来进行描述的,比如某个类的修饰符为Public Final,则对应的访问修饰符的值为ACC_PUBLIC | ACC_FINAL,即0x0001 | 0x0010=0x0011。

    图9 访问标志

    (5) 当前类名

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

    (6) 父类名称

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

    (7) 接口信息

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

    (8) 字段表

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

    图10 字段表结构

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

    图11 字段表示例

    (9)方法表

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

    图12 方法表结构

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

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

    图13 反编译后的方法表

    (10)附加属性表

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

    1.3 字节码操作集合

    在上图13中,Code区的红色编号0~17,就是.java中的方法源代码编译后让JVM真正执行的操作码。为了帮助人们理解,反编译后看到的是十六进制操作码所对应的助记符,十六进制值操作码与助记符的对应关系,以及每一个操作码的用处可以查看Oracle官方文档进行了解,在需要用到时进行查阅即可。比如上图中第一个助记符为iconst_2,对应到图2中的字节码为0x05,用处是将int值2压入操作数栈中。以此类推,对0~17的助记符理解后,就是完整的add()方法的实现。

    1.4 操作数栈和字节码

    JVM的指令集是基于栈而不是寄存器,基于栈可以具备很好的跨平台性(因为寄存器指令集往往和硬件挂钩),但缺点在于,要完成同样的操作,基于栈的实现需要更多指令才能完成(因为栈只是一个FILO结构,需要频繁压栈出栈)。另外,由于栈是在内存实现的,而寄存器是在CPU的高速缓存区,相较而言,基于栈的速度要慢很多,这也是为了跨平台性而做出的牺牲。

    我们在上文所说的操作码或者操作集合,其实控制的就是这个JVM的操作数栈。为了更直观地感受操作码是如何控制操作数栈的,以及理解常量池、变量表的作用,将add()方法的对操作数栈的操作制作为GIF,如下图14所示,图中仅截取了常量池中被引用的部分,以指令iconst_2开始到ireturn结束,与图13中Code区0~17的指令一一对应:

    图14 控制操作数栈示意图

    1.5 查看字节码工具

    如果每次查看反编译后的字节码都使用javap命令的话,好非常繁琐。这里推荐一个Idea插件:jclasslib。使用效果如图15所示,代码编译后在菜单栏"View"中选择"Show Bytecode With jclasslib",可以很直观地看到当前字节码文件的类信息、常量池、方法区等信息。

    图15 jclasslib查看字节码

    2. 字节码增强

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

    图16 字节码增强技术

    2.1 ASM

    对于需要手动操纵字节码的需求,可以使用ASM,它可以直接生产 .class字节码文件,也可以在类被加载入JVM之前动态修改类行为(如下图17所示)。ASM的应用场景有AOP(Cglib就是基于ASM)、热部署、修改其他jar包中的类等。当然,涉及到如此底层的步骤,实现起来也比较麻烦。接下来,本文将介绍ASM的两种API,并用ASM来实现一个比较粗糙的AOP。但在此之前,为了让大家更快地理解ASM的处理流程,强烈建议读者先对访问者模式进行了解。简单来说,访问者模式主要用于修改或操作一些数据结构比较稳定的数据,而通过第一章,我们知道字节码文件的结构是由JVM固定的,所以很适合利用访问者模式对字节码文件进行修改。

    图17 ASM修改字节码

    2.1.1 ASM API

    2.1.1.1 核心API

    ASM Core API可以类比解析XML文件中的SAX方式,不需要把这个类的整个结构读取进来,就可以用流式的方法来处理字节码文件。好处是非常节约内存,但是编程难度较大。然而出于性能考虑,一般情况下编程都使用Core API。在Core API中有以下几个关键类:

    • ClassReader:用于读取已经编译好的.class文件。
    • ClassWriter:用于重新构建编译后的类,如修改类名、属性以及方法,也可以生成新的类的字节码文件。
    • 各种Visitor类:如上所述,CoreAPI根据字节码从上到下依次处理,对于字节码文件中不同的区域有不同的Visitor,比如用于访问方法的MethodVisitor、用于访问类变量的FieldVisitor、用于访问注解的AnnotationVisitor等。为了实现AOP,重点要使用的是MethodVisitor。

    2.1.1.2 树形API

    ASM Tree API可以类比解析XML文件中的DOM方式,把整个类的结构读取到内存中,缺点是消耗内存多,但是编程比较简单。TreeApi不同于CoreAPI,TreeAPI通过各种Node类来映射字节码的各个区域,类比DOM节点,就可以很好地理解这种编程方式。

    2.1.2 直接利用ASM实现AOP

    利用ASM的CoreAPI来增强类。这里不纠结于AOP的专业名词如切片、通知,只实现在方法调用前、后增加逻辑,通俗易懂且方便理解。首先定义需要被增强的Base类:其中只包含一个process()方法,方法内输出一行“process”。增强后,我们期望的是,方法执行前输出“start”,之后输出"end"。

    public class Base {
        public void process(){
            System.out.println("process");
        }
    }
    

    为了利用ASM实现AOP,需要定义两个类:一个是MyClassVisitor类,用于对字节码的visit以及修改;另一个是Generator类,在这个类中定义ClassReader和ClassWriter,其中的逻辑是,classReader读取字节码,然后交给MyClassVisitor类处理,处理完成后由ClassWriter写字节码并将旧的字节码替换掉。Generator类较简单,我们先看一下它的实现,如下所示,然后重点解释MyClassVisitor类。

    import org.objectweb.asm.ClassReader;
    import org.objectweb.asm.ClassVisitor;
    import org.objectweb.asm.ClassWriter;
    
    public class Generator {
        public static void main(String[] args) throws Exception {
    				//读取
            ClassReader classReader = new ClassReader("meituan/bytecode/asm/Base");
            ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
            //处理
            ClassVisitor classVisitor = new MyClassVisitor(classWriter);
            classReader.accept(classVisitor, ClassReader.SKIP_DEBUG);
            byte[] data = classWriter.toByteArray();
            //输出
            File f = new File("operation-server/target/classes/meituan/bytecode/asm/Base.class");
            FileOutputStream fout = new FileOutputStream(f);
            fout.write(data);
            fout.close();
            System.out.println("now generator cc success!!!!!");
        }
    }
    

    MyClassVisitor继承自ClassVisitor,用于对字节码的观察。它还包含一个内部类MyMethodVisitor,继承自MethodVisitor用于对类内方法的观察,它的整体代码如下:

    import org.objectweb.asm.ClassVisitor;
    import org.objectweb.asm.MethodVisitor;
    import org.objectweb.asm.Opcodes;
    
    public class MyClassVisitor extends ClassVisitor implements Opcodes {
        public MyClassVisitor(ClassVisitor cv) {
            super(ASM5, cv);
        }
        @Override
        public void visit(int version, int access, String name, String signature,
                          String superName, String[] interfaces) {
            cv.visit(version, access, name, signature, superName, interfaces);
        }
        @Override
        public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
            MethodVisitor mv = cv.visitMethod(access, name, desc, signature,
                    exceptions);
            //Base类中有两个方法:无参构造以及process方法,这里不增强构造方法
            if (!name.equals("<init>") && mv != null) {
                mv = new MyMethodVisitor(mv);
            }
            return mv;
        }
        class MyMethodVisitor extends MethodVisitor implements Opcodes {
            public MyMethodVisitor(MethodVisitor mv) {
                super(Opcodes.ASM5, mv);
            }
    
            @Override
            public void visitCode() {
                super.visitCode();
                mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                mv.visitLdcInsn("start");
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
            }
            @Override
            public void visitInsn(int opcode) {
                if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)
                        || opcode == Opcodes.ATHROW) {
                    //方法在返回之前,打印"end"
                    mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                    mv.visitLdcInsn("end");
                    mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
                }
                mv.visitInsn(opcode);
            }
        }
    }
    

    利用这个类就可以实现对字节码的修改。详细解读其中的代码,对字节码做修改的步骤是:

    • 首先通过MyClassVisitor类中的visitMethod方法,判断当前字节码读到哪一个方法了。跳过构造方法""后,将需要被增强的方法交给内部类MyMethodVisitor来进行处理。
    • 接下来,进入内部类MyMethodVisitor中的visitCode方法,它会在ASM开始访问某一个方法的Code区时被调用,重写visitCode方法,将AOP中的前置逻辑就放在这里。
    • MyMethodVisitor继续读取字节码指令,每当ASM访问到无参数指令时,都会调用MyMethodVisitor中的visitInsn方法。我们判断了当前指令是否为无参数的“return”指令,如果是就在它的前面添加一些指令,也就是将AOP的后置逻辑放在该方法中。
    • 综上,重写MyMethodVisitor中的两个方法,就可以实现AOP了,而重写方法时就需要用ASM的写法,手动写入或者修改字节码。通过调用methodVisitor的visitXXXXInsn()方法就可以实现字节码的插入,XXXX对应相应的操作码助记符类型,比如mv.visitLdcInsn(“end”)对应的操作码就是ldc “end”,即将字符串“end”压入栈。

    完成这两个visitor类后,运行Generator中的main方法完成对Base类的字节码增强,增强后的结果可以在编译后的target文件夹中找到Base.class文件进行查看,可以看到反编译后的代码已经改变了(如图18左侧所示)。然后写一个测试类MyTest,在其中new Base(),并调用base.process()方法,可以看到下图右侧所示的AOP实现效果:

    图18 ASM实现AOP的效果

    2.1.3 ASM工具

    利用ASM手写字节码时,需要利用一系列visitXXXXInsn()方法来写对应的助记符,所以需要先将每一行源代码转化为一个个的助记符,然后通过ASM的语法转换为visitXXXXInsn()这种写法。第一步将源码转化为助记符就已经够麻烦了,不熟悉字节码操作集合的话,需要我们将代码编译后再反编译,才能得到源代码对应的助记符。第二步利用ASM写字节码时,如何传参也很令人头疼。ASM社区也知道这两个问题,所以提供了工具ASM ByteCode Outline

    安装后,右键选择“Show Bytecode Outline”,在新标签页中选择“ASMified”这个tab,如图19所示,就可以看到这个类中的代码对应的ASM写法了。图中上下两个红框分别对应AOP中的前置逻辑于后置逻辑,将这两块直接复制到visitor中的visitMethod()以及visitInsn()方法中,就可以了。

    图19 ASM Bytecode Outline

    2.2 Javassist

    ASM是在指令层次上操作字节码的,阅读上文后,我们的直观感受是在指令层次上操作字节码的框架实现起来比较晦涩。故除此之外,我们再简单介绍另外一类框架:强调源代码层次操作字节码的框架Javassist。

    利用Javassist实现字节码增强时,可以无须关注字节码刻板的结构,其优点就在于编程简单。直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构或者动态生成类。其中最重要的是ClassPool、CtClass、CtMethod、CtField这四个类:

    • CtClass(compile-time class):编译时类信息,它是一个class文件在代码中的抽象表现形式,可以通过一个类的全限定名来获取一个CtClass对象,用来表示这个类文件。
    • ClassPool:从开发视角来看,ClassPool是一张保存CtClass信息的HashTable,key为类名,value为类名对应的CtClass对象。当我们需要对某个类进行修改时,就是通过pool.getCtClass(“className”)方法从pool中获取到相应的CtClass。
    • CtMethod、CtField:这两个比较好理解,对应的是类中的方法和属性。

    了解这四个类后,我们可以写一个小Demo来展示Javassist简单、快速的特点。我们依然是对Base中的process()方法做增强,在方法调用前后分别输出"start"和"end",实现代码如下。我们需要做的就是从pool中获取到相应的CtClass对象和其中的方法,然后执行method.insertBefore和insertAfter方法,参数为要插入的Java代码,再以字符串的形式传入即可,实现起来也极为简单。

    import com.meituan.mtrace.agent.javassist.*;
    
    public class JavassistTest {
        public static void main(String[] args) throws NotFoundException, CannotCompileException, IllegalAccessException, InstantiationException, IOException {
            ClassPool cp = ClassPool.getDefault();
            CtClass cc = cp.get("meituan.bytecode.javassist.Base");
            CtMethod m = cc.getDeclaredMethod("process");
            m.insertBefore("{ System.out.println(\"start\"); }");
            m.insertAfter("{ System.out.println(\"end\"); }");
            Class c = cc.toClass();
            cc.writeFile("/Users/zen/projects");
            Base h = (Base)c.newInstance();
            h.process();
        }
    }
    

    3. 运行时类的重载

    3.1 问题引出

    上一章重点介绍了两种不同类型的字节码操作框架,且都利用它们实现了较为粗糙的AOP。其实,为了方便大家理解字节码增强技术,在上文中我们避重就轻将ASM实现AOP的过程分为了两个main方法:第一个是利用MyClassVisitor对已编译好的class文件进行修改,第二个是new对象并调用。这期间并不涉及到JVM运行时对类的重加载,而是在第一个main方法中,通过ASM对已编译类的字节码进行替换,在第二个main方法中,直接使用已替换好的新类信息。另外在Javassist的实现中,我们也只加载了一次Base类,也不涉及到运行时重加载类。

    如果我们在一个JVM中,先加载了一个类,然后又对其进行字节码增强并重新加载会发生什么呢?模拟这种情况,只需要我们在上文中Javassist的Demo中main()方法的第一行添加Base b=new Base(),即在增强前就先让JVM加载Base类,然后在执行到c.toClass()方法时会抛出错误,如下图20所示。跟进c.toClass()方法中,我们会发现它是在最后调用了ClassLoader的native方法defineClass()时报错。也就是说,JVM是不允许在运行时动态重载一个类的。

    图20 运行时重复load类的错误信息

    显然,如果只能在类加载前对类进行强化,那字节码增强技术的使用场景就变得很窄了。我们期望的效果是:在一个持续运行并已经加载了所有类的JVM中,还能利用字节码增强技术对其中的类行为做替换并重新加载。为了模拟这种情况,我们将Base类做改写,在其中编写main方法,每五秒调用一次process()方法,在process()方法中输出一行“process”。

    我们的目的就是,在JVM运行中的时候,将process()方法做替换,在其前后分别打印“start”和“end”。也就是在运行中时,每五秒打印的内容由"process"变为打印"start process end"。那如何解决JVM不允许运行时重加载类信息的问题呢?为了达到这个目的,我们接下来一一来介绍需要借助的Java类库。

    import java.lang.management.ManagementFactory;
    
    public class Base {
        public static void main(String[] args) {
            String name = ManagementFactory.getRuntimeMXBean().getName();
            String s = name.split("@")[0];
            //打印当前Pid
            System.out.println("pid:"+s);
            while (true) {
                try {
                    Thread.sleep(5000L);
                } catch (Exception e) {
                    break;
                }
                process();
            }
        }
    
        public static void process() {
            System.out.println("process");
        }
    }
    

    3.2 Instrument

    instrument是JVM提供的一个可以修改已加载类的类库,专门为Java语言编写的插桩服务提供支持。它需要依赖JVMTI的Attach API机制实现,JVMTI这一部分,我们将在下一小节进行介绍。在JDK 1.6以前,instrument只能在JVM刚启动开始加载类时生效,而在JDK 1.6之后,instrument支持了在运行时对类定义的修改。要使用instrument的类修改功能,我们需要实现它提供的ClassFileTransformer接口,定义一个类文件转换器。接口中的transform()方法会在类文件被加载时调用,而在transform方法里,我们可以利用上文中的ASM或Javassist对传入的字节码进行改写或替换,生成新的字节码数组后返回。

    我们定义一个实现了ClassFileTransformer接口的类TestTransformer,依然在其中利用Javassist对Base类中的process()方法进行增强,在前后分别打印“start”和“end”,代码如下:

    import java.lang.instrument.ClassFileTransformer;
    
    public class TestTransformer implements ClassFileTransformer {
        @Override
        public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
            System.out.println("Transforming " + className);
            try {
                ClassPool cp = ClassPool.getDefault();
                CtClass cc = cp.get("meituan.bytecode.jvmti.Base");
                CtMethod m = cc.getDeclaredMethod("process");
                m.insertBefore("{ System.out.println(\"start\"); }");
                m.insertAfter("{ System.out.println(\"end\"); }");
                return cc.toBytecode();
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    }
    

    现在有了Transformer,那么它要如何注入到正在运行的JVM呢?还需要定义一个Agent,借助Agent的能力将Instrument注入到JVM中。我们将在下一小节介绍Agent,现在要介绍的是Agent中用到的另一个类Instrumentation。在JDK 1.6之后,Instrumentation可以做启动后的Instrument、本地代码(Native Code)的Instrument,以及动态改变Classpath等等。我们可以向Instrumentation中添加上文中定义的Transformer,并指定要被重加载的类,代码如下所示。这样,当Agent被Attach到一个JVM中时,就会执行类字节码替换并重载入JVM的操作。

    import java.lang.instrument.Instrumentation;
    
    public class TestAgent {
        public static void agentmain(String args, Instrumentation inst) {
            //指定我们自己定义的Transformer,在其中利用Javassist做字节码替换
            inst.addTransformer(new TestTransformer(), true);
            try {
                //重定义类并载入新的字节码
                inst.retransformClasses(Base.class);
                System.out.println("Agent Load Done.");
            } catch (Exception e) {
                System.out.println("agent load failed!");
            }
        }
    }
    

    3.3 JVMTI & Agent & Attach API

    上一小节中,我们给出了Agent类的代码,追根溯源需要先介绍JPDA(Java Platform Debugger Architecture)。如果JVM启动时开启了JPDA,那么类是允许被重新加载的。在这种情况下,已被加载的旧版本类信息可以被卸载,然后重新加载新版本的类。正如JDPA名称中的Debugger,JDPA其实是一套用于调试Java程序的标准,任何JDK都必须实现该标准。

    JPDA定义了一整套完整的体系,它将调试体系分为三部分,并规定了三者之间的通信接口。三部分由低到高分别是Java 虚拟机工具接口(JVMTI),Java 调试协议(JDWP)以及 Java 调试接口(JDI),三者之间的关系如下图所示:

    图21 JPDA

    现在回到正题,我们可以借助JVMTI的一部分能力,帮助动态重载类信息。JVM TI(JVM TOOL INTERFACE,JVM工具接口)是JVM提供的一套对JVM进行操作的工具接口。通过JVMTI,可以实现对JVM的多种操作,它通过接口注册各种事件勾子,在JVM事件触发时,同时触发预定义的勾子,以实现对各个JVM事件的响应,事件包括类文件加载、异常产生与捕获、线程启动和结束、进入和退出临界区、成员变量修改、GC开始和结束、方法调用进入和退出、临界区竞争与等待、VM启动与退出等等。

    而Agent就是JVMTI的一种实现,Agent有两种启动方式,一是随Java进程启动而启动,经常见到的java -agentlib就是这种方式;二是运行时载入,通过attach API,将模块(jar包)动态地Attach到指定进程id的Java进程内。

    Attach API 的作用是提供JVM进程间通信的能力,比如说我们为了让另外一个JVM进程把线上服务的线程Dump出来,会运行jstack或jmap的进程,并传递pid的参数,告诉它要对哪个进程进行线程Dump,这就是Attach API做的事情。在下面,我们将通过Attach API的loadAgent()方法,将打包好的Agent jar包动态Attach到目标JVM上。具体实现起来的步骤如下:

    • 定义Agent,并在其中实现AgentMain方法,如上一小节中定义的代码块7中的TestAgent类;
    • 然后将TestAgent类打成一个包含MANIFEST.MF的jar包,其中MANIFEST.MF文件中将Agent-Class属性指定为TestAgent的全限定名,如下图所示;

    图22 Manifest.mf

    • 最后利用Attach API,将我们打包好的jar包Attach到指定的JVM pid上,代码如下:
    import com.sun.tools.attach.VirtualMachine;
    
    public class Attacher {
        public static void main(String[] args) throws AttachNotSupportedException, IOException, AgentLoadException, AgentInitializationException {
            // 传入目标 JVM pid
            VirtualMachine vm = VirtualMachine.attach("39333"); 
            vm.loadAgent("/Users/zen/operation_server_jar/operation-server.jar");
        }
    }
    
    • 由于在MANIFEST.MF中指定了Agent-Class,所以在Attach后,目标JVM在运行时会走到TestAgent类中定义的agentmain()方法,而在这个方法中,我们利用Instrumentation,将指定类的字节码通过定义的类转化器TestTransformer做了Base类的字节码替换(通过javassist),并完成了类的重新加载。由此,我们达成了“在JVM运行时,改变类的字节码并重新载入类信息”的目的。

    以下为运行时重新载入类的效果:先运行Base中的main()方法,启动一个JVM,可以在控制台看到每隔五秒输出一次"process"。接着执行Attacher中的main()方法,并将上一个JVM的pid传入。此时回到上一个main()方法的控制台,可以看到现在每隔五秒输出"process"前后会分别输出"start"和"end",也就是说完成了运行时的字节码增强,并重新载入了这个类。

    图23 运行时重载入类的效果

    3.4 使用场景

    至此,字节码增强技术的可使用范围就不再局限于JVM加载类前了。通过上述几个类库,我们可以在运行时对JVM中的类进行修改并重载了。通过这种手段,可以做的事情就变得很多了:

    • 热部署:不部署服务而对线上服务做修改,可以做打点、增加日志等操作。
    • Mock:测试时候对某些服务做Mock。
    • 性能诊断工具:比如bTrace就是利用Instrument,实现无侵入地跟踪一个正在运行的JVM,监控到类和方法级别的状态信息。

    4. 总结

    字节码增强技术相当于是一把打开运行时JVM的钥匙,利用它可以动态地对运行中的程序做修改,也可以跟踪JVM运行中程序的状态。此外,我们平时使用的动态代理、AOP也与字节码增强密切相关,它们实质上还是利用各种手段生成符合规范的字节码文件。综上所述,掌握字节码增强后可以高效地定位并快速修复一些棘手的问题(如线上性能问题、方法出现不可控的出入参需要紧急加日志等问题),也可以在开发中减少冗余代码,大大提高开发效率。

    5. 参考文献

    作者简介

    泽恩,美团点评研发工程师。

    团队信息

    美团到店住宿业务研发团队负责美团酒店核心业务系统建设,致力于通过技术践行“帮大家住得更好”的使命。美团酒店屡次刷新行业记录,最近12个月酒店预订间夜量达到3个亿,单日入住间夜量峰值突破280万。团队的愿景是:建设打造旅游住宿行业一流的技术架构,从质量、安全、效率、性能多角度保障系统高速发展。
    美团到店事业群住宿业务研发团队现诚聘后台开发工程师/技术专家,欢迎有兴趣的同学加入。

    展开全文
  • 搭建字节码框架 /** * 字节码增强获取新的字节码 */ private byte[] getBytes(String className) throws IOException { ClassReader cr = new ClassReader(className); ClassWriter cw = new ClassWriter(cr, Class...
  • Java字节码

    千次阅读 2019-01-14 13:23:40
    Java最黑科技的玩法就是字节码编程,也就是动态修改或是动态生成 Java 字节码。使用字节码可以玩出很多高级的玩法,最高级的还是在 Java 程序运行时进行字节码修改和代码注入。听起来是不是一些很黑客,也很黑科技的...
  • 深入理解JVM-字节码

    千次阅读 2020-03-07 21:11:00
    Java字节码结构 Access_Flag访问标志 Fileds 字段表 Methods 方法表: 方法的属性结构 Code结构 其他结构 附加属性表 字节码补充注意事项 栈帧 字节码解释执行 Java字节码结构 Class字节码中有两...
  • 文章目录前言从AOP说起静态代理动态代理JavaProxyCGLIB字节码增强实现AOPASMJavaAssist运行时类加载Instrumentation接口JavaAgentPremainClass随JVM进程启动AgentClass以Attach方法注入Agent总结 前言 在上篇文章...
  • 可变字节码

    2014-05-15 00:44:09
    可变字节码,一种压缩算法,是大规模数据存储的一个不错的选择
  • ASM操作字节码,动态生成Java类class文件,模拟Spring的AOP实现原理。
  • 文章目录基本概念什么是字节码Javap命令查看字节码文件字节码文件解读static、final、volatile在字节码中的体现 基本概念 static:static修饰的变量被所有类实例共享,静态变量在其所在类被加载时进行初始化 final:...
  • 1、javap查看字节码内容上文介绍了字节码的结构,本文主要通过一个简单的例子说明class字节码的每一个字段。package com.zcm.test; import java.io.Serializable; public class SourceTest implements ...
  • 从一个class文件深入理解Java字节码结构

    万次阅读 多人点赞 2018-05-15 10:01:56
    我们都知道,Java程序最终是转换成class文件执行在虚拟机上的,那么class文件是个怎样的结构,虚拟机又是如何处理去执行class文件里面的内容呢,这篇文章带你深入理解Java字节码中的结构。 1.Demo源码 首先,...
  • 前一章讲述了java字节码文件的生成以及字节码文件中各个字段代表的含义,在本章节将讲述字节码是什么运行的 JVM的一些基础概念 要理解java字节码的运行情况,首先要了解有关JVM的一些知识,这些是java字节码运行的...
  • Python字节码(.pyc)介绍

    千次阅读 2018-05-11 16:49:56
    (转自IT派 微信公众号)一、了解Python字节码是什么,Python如何使用它来执行代码,以及了解它可以帮我们干什么。如果你曾经编写亦或只是使用Python语言,那么你可能已经习惯了看Python源码文件; 源码的文件名以.py...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 844,477
精华内容 337,790
关键字:

字节码