教你用Java字节码做点有趣的事

6,686 阅读22分钟

0.写在前面

为什么会写这篇文章呢?主要是之前调研过日志脱敏相关的一些,具体可以参考LOG4j脱敏插件如何编写 里面描述了日志脱敏插件编写方法:

  • 直接在toString中修改代码,这种方法很麻烦,效率低,需要修改每一个要脱敏的类,或者写个idea插件自动修改toString(),这样不好的地方在于所有编译器都需要开个插件,不够通用。
  • 在编译时期修改抽象语法树修改toString()方法,就像类似Lombok一样,这个之前调研过,开发难度较大,可能后会更新如何去写。
  • 在加载的时候通过实现Instrumentation接口 asm库,修改class文件的字节码,但是有个比较麻烦的地方在于需要给jvm加上启动参数 -javaagent:agentjarpath,这个已经实现了,但是实现后发现的确不够通用。

期中二三两个已经实现了,开发这个的确比较有趣,自己的知识面也得到了扩展,后续会通过写4-5篇的文章,一步一步的带大家如何去实现这些有趣的工具,学会了之后,通过大家丰富的想象力相信能实现更多有意思的东西。

0.1字节码能干什么

例如我这篇文章要介绍的通过修改字节码去实现日志脱敏,其实就是修改toString的字节码: 可以看看怎么用:

@Desensitized
public class StreamDemo1 {


    private User user;
    @DesFiled(MobileDesFilter.class)
    private String name;
    private String idCard;
    @DesFiled(AddressDesFilter.class)
    private List<String> mm;
    public static void main(String[] args) throws IOException {
        StreamDemo1 streamDemo1 = new StreamDemo1();
        streamDemo1.setUser(new User());
        streamDemo1.setName("18428368642");
        streamDemo1.setIdCard("22321321321");
        streamDemo1.setMm(Arrays.asList("北京是朝阳区打撒所大所大","北京是朝阳区打撒所大所大"));
        System.out.println(streamDemo1);
    }
    
    @Override
    public String toString() {
        return "StreamDemo1{" +
                "user=" + user +
                ", name='" + name + '\'' +
                ", idCard='" + idCard + '\'' +
                ", mm=" + mm +
                '}';
    }
}

这个类很普通对吧,和其他的实体类,唯一的区别是多了一个注解: @DesFiled(MobileDesFilter.class),有了这个注解我们执行这个main方法:他会输出:

StreamDemo1{user=bean.User@22d8cfe0, name='184****4777', idCard='22321321321', mm=[北京是朝阳区打*****, 北京是朝阳区打*****]}

可以看见我们明明输入的是不带号的手机号,为什么输出缺带号了呢,这就是操纵字节码的神奇。当然大家也可以自己扩展思维,你可以用他来做aop切面,当然cglib做切面的确也是操纵的字节码,你也可以用它来做你想让它做的事

0.2语法树

另一方面我也调研了lombok的实现,对此我发现修改抽象语法树,似乎更加有趣,你可以想象,你平时是否重复的给每个方法打印入参出参,耗时耗力?你平时是否在为缺少关键的日志而感到想骂人?你平时是否害怕用写AOP用反射打日志会影响性能?为了解决这个问题做了一个意思的工具slothLog,github地址:slothlog github https://github.com/lzggsimida123/slothlog.git (当然也求各位大佬们给点star,O(∩_∩)O哈哈~)。

@LogInfo
public class DemoService {
    public String hello(String name, int age){
        System.out.println(name + age + "hello");
        return name+age;
    }
    public static void main(String[] args) {
        DemoService demoService = new DemoService();
        demoService.hello("java", 100);
    }
}

通过上面会输出以下信息,将方法的出参,入参都进行输出,脱离了调试时缺少日志的苦恼

[INFO ] 2018-07-20 20:02:42,219 DemoService.main invoke start  args: {} 
[INFO ] 2018-07-20 20:02:42,220 DemoService.hello invoke start  name: java ,age: 100 
java100hello
[INFO ] 2018-07-20 20:02:42,221 DemoService.hello invoke end  name: java ,age: 100 , result: java100

后续我会一步一步的教大家如何去完成一个类似Lombok的修改语法树的框架,做更多有趣的事。

0.3关于本篇

如果你不喜欢上面这些东西,也别着急,字节码是java的基础,我觉得是所有Java程序员需要必备的,当然你也有必要了解一下。 本篇是系列的第一篇,这篇主要讲的主要是字节码是什么,通过对这篇的了解,也是后续章节的基础。

1.什么是字节码?

1.1机器码

机器码(machine code)顾名思义也就是,机器能识别的代码,也叫原生码。机器码是CPU可直接解读的指令。机器码与硬件等有关,不同的CPU架构支持的硬件码也不相同。机器码是和我们的底层硬件直接打交道,现在学的人也是逐渐的变少了,如果对这个感兴趣的同学可以去学习一下汇编,汇编的指令会被翻译成机器码。

1.2字节码

字节码(Byte-code)是一种包含执行程序、由一序列 op 代码/数据对组成的二进制文件。字节码是程序的中间表示形式:介于人类可读的源码和机器码之间。它经常被看作是包含一个执行程序的二进制文件,更像一个对象模型。字节码被这样叫是因为通常每个操作码 是一字节长,所以字节码的程度是根据一字节来的。字节码也是由,一组操作码组成,而操作码实际上是对栈的操作,可以移走参数和地址空间,也可以放入结果。JAVA通过JIT(即时编译)可以将字节码转换为机器码。

字节码的实现方式是通过编译器和虚拟机器。编译器将源码编译成字节码,特定平台上的虚拟机器将字节码转译为可以直接执行的指令。在java中一般是用Javac编译源文件变成字节码,也就是我们的class文件。

从网络上找到了两张图片,下面是java源码编译器生成字节码过程:

java虚拟机执行引擎过程,这里会分为两个阶段:

  • 普通的代码(非热)都是走的字节码解释器

  • 热代码:多次调用的方法,多次执行的循环体,会被JIT优化成机器码。

2.字节码执行

2.1JVM桢栈结构:

方法调用在JVM中转换成的是字节码执行,字节码指令执行的数据结构就是栈帧(stack frame)。也就是在虚拟机栈中的栈元素。虚拟机会为每个方法分配一个栈帧,因为虚拟机栈是LIFO(后进先出)的,所以当前线程正在活动的栈帧,也就是栈顶的栈帧,JVM规范中称之为“CurrentFrame”,这个当前栈帧对应的方法就是“CurrentMethod”。字节码的执行操作,指的就是对当前栈帧数据结构进行的操作。

JVM的运行时数据区的结构如下图:。

我们这里主要讨论栈帧的数据结构:有四个部分,局部变量区,操作数栈,动态链接,方法的返回地址。

2.1.1局部变量表:

局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序被编译成Class文件时,就在Code属性中locals变量:

如下面代码反编译后就能看见locals=5。

局部变量的容量以变量槽(Slot)为最小单位,32位虚拟机中一个Slot可以存放一个32位以内的数据类型(boolean、byte、char、short、int、float、reference(引用)和returnAddress八种)。

同时Slot对对象的引用会影响GC,(要是被引用,不会被回收)。

系统不会为局部变量赋予初始值,也就是说不存在类变量那样的准备阶段。

虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程的,如果是实例方法(非static),那么局部变量表的第0位索引的Slot默认是用于传递方法所属对象实例的引用,在方法中通过this访问。

我们上面的代码中是4个Int的solt加一个this 的solt所以就等于5。

2.1.2操作数栈

Java虚拟机的解释执行引擎被称为"基于栈的执行引擎",其中所指的栈就是指-操作数栈。

操作数栈同局部变量表一样,也是编译期间就能决定了其存储空间(最大的单位长度),通过 Code属性存储在类或接口的字节流中。操作数栈也是个LIFO栈。 它不是通过索引来访问,而是通过标准的栈操作—压栈和出栈—来访问的。比如,如果某个指令把一个值压入到操作数栈中,稍后另一个指令就可以弹出这个值来使用。

虚拟机在操作数栈中存储数据的方式和在局部变量区中是一样的:如int、long、float、double、reference和returnType的存储。对于byte、short以及char类型的值在压入到操作数栈之前,也会被转换为int。

2.1.3动态链接

动态链接就是将符号引用所表示的方法,转换成方法的直接引用。加载阶段或第一次使用时转化为直接引用的(将变量的访问转化为访问这些变量的存储结构所在的运行时内存位置)就叫做静态解析。JVM的动态链接还支持运行期转化为直接引用。也可以叫做Late Binding,晚期绑定。动态链接是java灵活OO的基础结构。

注:

符号引用就是字符串,这个字符串包含足够的信息,以供实际使用时可以找到相应的位置。你比如说某个方法的符号引用,如:“java/io/PrintStream.println:(Ljava/lang/String;)V”。里面有类的信息,方法名,方法参数等信息。

当第一次运行时,要根据字符串的内容,到该类的方法表中搜索这个方法。运行一次之后,符号引用会被替换为直接引用,下次就不用搜索了。直接引用就是偏移量,通过偏移量虚拟机可以直接在该类的内存区域中找到方法字节码的起始位置。重写就是动态链接,重载就是静态解析。

2.1.4方法返回地址

**方法正常退出,JVM执行引擎会恢复上层方法局部变量表操作数栈并把返回值压入调用者的栈帧的操作数栈,PC计数器的值就会调整到方法调用指令后面的一条指令。**这样使得当前的栈帧能够和调用者连接起来,并且让调用者的栈帧的操作数栈继续往下执行。   方法的异常调用完成,如果异常没有被捕获住,或者遇到athrow字节码指令显示抛出,那么就没有返回值给调用者。

2.2字节码指令集

2.2.1加载和存储指令

加载和存储指令用于将数据从栈帧的局部变量表和操作数栈之间来回传输。

1)将一个局部变量加载到操作数栈的指令包括:iload,iload_,lload、lload、float、 fload_、dload、dload_,aload、aload。

2)将一个数值从操作数栈存储到局部变量表的指令:istore,istore_,lstore,lstore_,fstore,fstore_,dstore,dstore_,astore,astore_

3)将常量加载到操作数栈的指令:bipush,sipush,ldc,ldc_w,ldc2_w,aconst_null,iconst_ml,iconst_,lconst_,fconst_,dconst_

4)局部变量表的访问索引指令:wide

2.2.2运算指令

算术指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶。

1)加法指令:iadd,ladd,fadd,dadd

2)减法指令:isub,lsub,fsub,dsub

3)乘法指令:imul,lmul,fmul,dmul

4)除法指令:idiv,ldiv,fdiv,ddiv

5)求余指令:irem,lrem,frem,drem

6)取反指令:ineg,leng,fneg,dneg

7)位移指令:ishl,ishr,iushr,lshl,lshr,lushr

8)按位或指令:ior,lor

9)按位与指令:iand,land

10)按位异或指令:ixor,lxor

11)局部变量自增指令:iinc

12)比较指令:dcmpg,dcmpl,fcmpg,fcmpl,lcmp

Java虚拟机没有明确规定整型数据溢出的情况,但规定了处理整型数据时,只有除法和求余指令出现除数为0时会导致虚拟机抛出异常。

Java虚拟机要求在浮点数运算的时候,所有结果否必须舍入到适当的精度,如果有两种可表示的形式与该值一样,会优先选择最低有效位为零的。称之为最接近数舍入模式。

浮点数向整数转换的时候,Java虚拟机使用IEEE 754标准中的向零舍入模式,这种模式舍入的结果会导致数字被截断,所有小数部分的有效字节会被丢掉。

2.2.3类型转换指令

类型转换指令将两种Java虚拟机数值类型相互转换,这些操作一般用于实现用户代码的显式类型转换操作。JVM直接就支持宽化类型转换(小范围类型向大范围类型转换):

1.int类型到long,float,double类型

2.long类型到float,double类型

3.float到double类型

但在处理窄化类型转换时,必须显式使用转换指令来完成,这些指令包括:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和 d2f。将int 或 long 窄化为整型T的时候,仅仅简单的把除了低位的N个字节以外的内容丢弃,N是T的长度。这有可能导致转换结果与输入值有不同的正负号。

在将一个浮点值窄化为整数类型T(仅限于 int 和 long 类型),将遵循以下转换规则:

1)如果浮点值是NaN , 那转换结果就是int 或 long 类型的0

2)如果浮点值不是无穷大,浮点值使用IEEE 754 的向零舍入模式取整,获得整数v, 如果v在T表示范围之内,那就是v

3)否则,根据v的符号, 转换为T 所能表示的最大或者最小正数

2.2.4对象创建和访问指令

虽然类实例和数组都是对象,Java虚拟机对类实例和数组的创建与操作使用了不同的字节码指令。

1)创建实例的指令:new

2)创建数组的指令:newarray,anewarray,multianewarray

3)访问字段指令:getfield,putfield,getstatic,putstatic

4)把数组元素加载到操作数栈指令:baload,caload,saload,iaload,laload,faload,daload,aaload

5)将操作数栈的数值存储到数组元素中执行:bastore,castore,castore,sastore,iastore,fastore,dastore,aastore

6)取数组长度指令:arraylength JVM支持方法级同步和方法内部一段指令序列同步,这两种都是通过moniter实现的。

7)检查实例类型指令:instanceof,checkcast

2.2.5操作数栈管理指令

如同操作一个普通数据结构中的堆栈那样,Java虚拟机提供了一些用于直接操作操作舒展的指令,包括:

1)将操作数栈的栈顶一个或两个元素出栈:pop、pop2

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

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

2.2.6控制转移指令

让JVM有条件或无条件从指定指令而不是控制转移指令的下一条指令继续执行程序。控制转移指令包括:

1)条件分支:ifeq,iflt,ifle,ifne,ifgt,ifge,ifnull,ifnotnull,if_cmpeq,if_icmpne,if_icmlt,if_icmpgt等

2)复合条件分支:tableswitch,lookupswitch

3)无条件分支:goto,goto_w,jsr,jsr_w,ret

JVM中有专门的指令集处理int和reference类型的条件分支比较操作,为了可以无明显标示一个实体值是否是null,有专门的指令检测null 值。boolean类型和byte类型,char类型和short类型的条件分支比较操作,都使用int类型的比较指令完成,而 long,float,double条件分支比较操作,由相应类型的比较运算指令,运算指令会返回一个整型值到操作数栈中,随后再执行int类型的条件比较操作完成整个分支跳转。各种类型的比较都最终会转化为int类型的比较操作。

2.2.7方法调用和返回指令

invokevirtual指令:调用对象的实例方法,根据对象的实际类型进行分派(虚拟机分派)。

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

invokespecial:调用需要特殊处理的实例方法,包括实例初始化方法,私有方法和父类方法

invokestatic:调用类方法(static)

方法返回指令是根据返回值的类型区分的,包括ireturn(返回值是boolean,byte,char,short和 int),lreturn(long),freturn,drturn(double)和areturn(引用地址),另外一个return供void方法,实例初始化方法,类和接口的类初始化i方法使用。

2.2.8异常处理指令

在Java程序中显式抛出异常的操作(throw语句)都有athrow 指令来实现,除了用throw 语句显示抛出异常情况外,Java虚拟机规范还规定了许多运行时异常会在其他Java虚拟机指令检测到异常状况时自动抛出。在Java虚拟机中,处理异常不是由字节码指令来实现的,而是采用异常表来完成的。

2.2.9同步指令

方法级的同步是隐式的,无需通过字节码指令来控制,它实现在方法调用和返回操作中。虚拟机从方法常量池中的方法标结构中的 ACC_SYNCHRONIZED标志区分是否是同步方法。方法调用时,调用指令会检查该标志是否被设置,若设置,执行线程持有moniter,然后执行方法,最后完成方法时释放moniter。同步一段指令集序列,通常由synchronized块标示,JVM指令集中有monitorenter和monitorexit来支持synchronized语义。

大多数的指令有前缀和(或)后缀来表明其操作数的类型。

3.字节码实例分析

这一节将给大家分析如何一步一步的分析字节码。

3.1源代码

有如下简单代码,下面代码是一个简单的demo,有一个常量,有一个类成员变量,同时方法有三个,一个构造方法,一个get(),一个静态main方法,用来输出信息。

package java8;

public class ByteCodeDemo {
    private static final String name = "xiaoming";
    
    private int age;

    public ByteCodeDemo(int age) {
        this.age = age;
    }

    public int getAge() {
        return age;
    }

    public static void main(String[] args) {
        ByteCodeDemo byteCodeDeomo = new ByteCodeDemo(12);
        System.out.println("name:" + name + "age:" + byteCodeDeomo.getAge());
    }
}

3.2.反编译

用命令行找到我们这段代码所在的路径,输入如下命令:

javac ByteCodeDemo.java

javap -p -v ByteCodeDemo

有关Javap命令可以用help或者参考javap命令,我们这里用的-p,-v输出所有类和成员信息,以及附加信息(文件路径,文件大小,常量池等等)

3.3.得到如下信息

Classfile /Users/lizhao/Documents/RPC/test/src/main/java/java8/ByteCodeDemo.class
  Last modified 2018-5-8; size 861 bytes
  MD5 checksum d225c0249912bec4b11c41a0a52e6418
  Compiled from "ByteCodeDemo.java"
public class java8.ByteCodeDemo
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #14.#31        // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#32         // java8/ByteCodeDemo.age:I
   #3 = Class              #33            // java8/ByteCodeDemo
   #4 = Methodref          #3.#34         // java8/ByteCodeDemo."<init>":(I)V
   #5 = Fieldref           #35.#36        // java/lang/System.out:Ljava/io/PrintStream;
   #6 = Class              #37            // java/lang/StringBuilder
   #7 = Methodref          #6.#31         // java/lang/StringBuilder."<init>":()V
   #8 = String             #38            // name:xiaomingage:
   #9 = Methodref          #6.#39         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #10 = Methodref          #3.#40         // java8/ByteCodeDemo.getAge:()I
  #11 = Methodref          #6.#41         // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
  #12 = Methodref          #6.#42         // java/lang/StringBuilder.toString:()Ljava/lang/String;
  #13 = Methodref          #43.#44        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #14 = Class              #45            // java/lang/Object
  #15 = Utf8               name
  #16 = Utf8               Ljava/lang/String;
  #17 = Utf8               ConstantValue
  #18 = String             #46            // xiaoming
  #19 = Utf8               age
  #20 = Utf8               I
  #21 = Utf8               <init>
  ....省略部分
  #58 = Utf8               (Ljava/lang/String;)V
{
  private static final java.lang.String name;
    descriptor: Ljava/lang/String;
    flags: ACC_PRIVATE, ACC_STATIC, ACC_FINAL
    ConstantValue: String xiaoming

  private int age;
    descriptor: I
    flags: ACC_PRIVATE

  public java8.ByteCodeDemo(int);
    descriptor: (I)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: iload_1
         6: putfield      #2                  // Field age:I
         9: return
      LineNumberTable:
        line 18: 0
        line 19: 4
        line 20: 9

  public int getAge();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field age:I
         4: ireturn
      LineNumberTable:
        line 23: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=2, args_size=1
         0: new           #3                  // class java8/ByteCodeDemo
         3: dup
         4: bipush        12
         6: invokespecial #4                  // Method "<init>":(I)V
         9: astore_1
        10: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
        13: new           #6                  // class java/lang/StringBuilder
        16: dup
        17: invokespecial #7                  // Method java/lang/StringBuilder."<init>":()V
        20: ldc           #8                  // String name:xiaomingage:
        22: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        25: aload_1
        26: invokevirtual #10                 // Method getAge:()I
        29: invokevirtual #11                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
        32: invokevirtual #12                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        35: invokevirtual #13                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        38: return
      LineNumberTable:
        line 27: 0
        line 28: 10
        line 29: 38
}
SourceFile: "ByteCodeDemo.java"

如果你是第一次用javap,那你一定会觉得这个是啥又臭又长,别着急下面我会一句一句给你翻译,这里你需要对照上面的字节码指令,一步一步的带你翻译。

3.4.附加信息

Classfile /Users/lizhao/Documents/RPC/test/src/main/java/java8/ByteCodeDemo.class //输出了我们的class文件的完整路径
  Last modified 2018-5-8; size 861 bytes //以及class文件修改时间以及大小
  MD5 checksum d225c0249912bec4b11c41a0a52e6418 //md5校验和
  Compiled from "ByteCodeDemo.java" //从哪个文件编译而来
public class java8.ByteCodeDemo 
  minor version: 0
  major version: 52 //java主版本  major_version.minor_version 组成我们的版本号52.0
  flags: ACC_PUBLIC, ACC_SUPER //public,ACC_SUPER用于兼容早期编译器,新编译器都设置该标记,以在使用 invokespecial指令时对子类方法做特定处理。
Constant pool:
   #1 = Methodref          #14.#31        // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#32         // java8/ByteCodeDemo.age:I
   #3 = Class              #33            // java8/ByteCodeDemo
   .........

部分信息在后面已经注释解释, 我们主要来说一下我们的Constant pool,常量池:

在Java字节码中,有一个常量池,用来存放不同类型的常量。由于Java设计的目的之一就是字节码需要经网络传输的,因而字节码需要比较紧凑,以减少网络传输的流量和时间。常量池的存在则可以让一些相同类型的值通过索引(引用)的方式从常量池中找到,而不是在不同地方有不同拷贝,缩减了字节码的大小。

tag中表示的数据类型,有如下11种,:

  • CONSTANT_Class_info

  • CONSTANT_Integer_info

  • CONSTANT_Long\info

  • CONSTANT_Float_info

  • CONSTANT_Double_info

  • CONSTANT_String_info

  • CONSTANT_Fieldref_info

  • CONSTANT_Methodref_info

  • CONSTANT_InterfaceMethodref_info

  • CONSTANT_NameAndType_info

  • CONSTANT_Utf8_info

注:在Java字节码中,所有boolean、byte、char、short类型都是用int类型存放,因而在常量池中没有和它们对应的项。 有关常量池的介绍可以参照这里:

http://www.blogjava.net/DLevin/archive/2011/09/05/358033.html

3.5.main方法分析

这里把main方法单独复制了出来,每一句话都进行了解释。

在看下面之前,可以自己尝试一下是否能将main方法字节码看懂

 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V //方法描述,入参是String,返回是void
    flags: ACC_PUBLIC, ACC_STATIC 
    Code:
      stack=3, locals=2, args_size=1 //栈深最大3,局部变量2,args_size入参是1(如果是实体方法会把this也算入参)
         0: new           #3                  // class java8/ByteCodeDemo new指令创建对象,这里引用了常量池的class 所以这里一共占了三行 2个字节是class 
         //一个字节是new,所以下个行号是 0+3 = 3 并把当前申请的空间地址放到栈顶
         3: dup 															//将栈顶cpoy一份再次放入栈顶,也就是我们上面的空间地址
         4: bipush        12									//取常量12放入栈空间
         6: invokespecial #4                  // Method "<init>":(I)V //执行初始化方法这个时候会用到4的栈顶,和3的栈顶,弹出
         9: astore_1													//将栈顶放入局部变量,也就是0的空间地址,这个时候栈是空的
        10: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream; //获取这个方法地址到栈顶
        13: new           #6                  // class java/lang/StringBuilder 把新开辟的空间地址放到栈顶
        16: dup																//复制一份
        17: invokespecial #7                  // Method java/lang/StringBuilder."<init>":()V //弹出栈顶
        20: ldc           #8                  // String name:xiaomingage://取常量到栈顶
        22: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;//弹出栈顶两个元素,压入StringBuilder的引用
        25: aload_1														// 把局部变量,也就是我们刚才的空间地址压入
        26: invokevirtual #10                 // Method getAge:()I //弹出栈顶,获取年龄,把年龄压入栈顶
        29: invokevirtual #11                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;//弹出栈顶两个元素,压入StringBuilder
        32: invokevirtual #12                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;//弹出栈顶两个元素,压入toString
        35: invokevirtual #13                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V//弹出栈顶两个元素,此时栈空
        38: return //返回
      LineNumberTable: //字节码偏移量到源代码行号之间的联系
        line 29: 0 
        line 30: 10
        line 31: 38
}

思考:这里看懂了之后,大家可以自己尝试下自己写个稍微复杂的字节码,然后进行理解,加深一下映像。

最后

下一篇字节码之ASM,下一篇将会给大家详细讲解如何通过asm去操作字节码,以及如何去实现我们上面的功能,喜欢这一系列可以关注公众号,不丢失文章

如果大家觉得这篇文章对你有帮助,或者想提前获取后续章节文章,或者你有什么疑问想提供1v1免费vip服务,都可以关注我的公众号,关注即可免费领取上百G最新java学习资料视频,以及最新面试资料,你的关注和转发是对我最大的支持,O(∩_∩)O: