一文看懂Java字节码

一文看懂Java字节码

Android小彩虹2021-07-11 5:07:45140A+A-

前言

随着Java语言的不断的发展,Java的应用场景慢慢被扩大,各种优雅解决问题的技术也不断衍生,如AOP技术,清晰理解Java运行原理就显得很有必要,本篇文章重点讲解Java字节码相关知识。

字节码基础

Java文件通过编译器生成的是class字节码文件,字节码文件也有文件自己的格式,这里不详细展开,直接通过Java自己带的工具查看一下。 首先我们的测试类文件如下:

public class Person {

	public String name;
	public int age;

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public int getAge() {
		return age;
	}

	public void setAge(int age) {
		this.age = age;
	}
}

定义了一个Person类,里面有name和age的属性,编译后生成Person.class文件,直接使用Java工具dump这个class文件,dump命令如下:

javap -v -p Person.class

dump生成的内容如下:

public class com.sec.resourceparse.Person
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref #5.#27 // java/lang/Object."<init>":()V
   #2 = Fieldref #4.#28 // com/sec/resourceparse/Person.name:Ljava/lang/String;
   #3 = Fieldref #4.#29 // com/sec/resourceparse/Person.age:I
   #4 = Class #30 // com/sec/resourceparse/Person
   #5 = Class #31 // java/lang/Object
   #6 = Utf8 name
   #7 = Utf8 Ljava/lang/String;
   #8 = Utf8 age
   #9 = Utf8 I
  #10 = Utf8 <init>
  #11 = Utf8 ()V
  #12 = Utf8 Code
  #13 = Utf8 LineNumberTable
  #14 = Utf8 LocalVariableTable
  #15 = Utf8 this
  #16 = Utf8 Lcom/sec/resourceparse/Person;
  #17 = Utf8 getName
  #18 = Utf8 ()Ljava/lang/String;
  #19 = Utf8 setName
  #20 = Utf8 (Ljava/lang/String;)V
  #21 = Utf8 getAge
  #22 = Utf8 ()I
  #23 = Utf8 setAge
  #24 = Utf8 (I)V
  #25 = Utf8 SourceFile
  #26 = Utf8 Person.java
  #27 = NameAndType #10:#11 // "<init>":()V
  #28 = NameAndType #6:#7 // name:Ljava/lang/String;
  #29 = NameAndType #8:#9 // age:I
  #30 = Utf8 com/sec/resourceparse/Person
  #31 = Utf8 java/lang/Object
{
  public java.lang.String name;
    descriptor: Ljava/lang/String;
    flags: ACC_PUBLIC

  public int age;
    descriptor: I
    flags: ACC_PUBLIC

  public com.sec.resourceparse.Person();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1 // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/sec/resourceparse/Person;

  public java.lang.String getName();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #2 // Field name:Ljava/lang/String;
         4: areturn
      LineNumberTable:
        line 9: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/sec/resourceparse/Person;

  public void setName(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: putfield      #2 // Field name:Ljava/lang/String;
         5: return
      LineNumberTable:
        line 13: 0
        line 14: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       6     0  this   Lcom/sec/resourceparse/Person;
            0       6     1  name   Ljava/lang/String;

这里截取了部分内容,先简单看一下,首先是类信息的介绍:

public class com.sec.resourceparse.Person
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER

类名,编译的JDK版本,以及访问修饰符
然后字符串池:

Constant pool:
   #1 = Methodref #5.#27 // java/lang/Object."<init>":()V
   #2 = Fieldref #4.#28 // com/sec/resourceparse/Person.name:Ljava/lang/String;
   #3 = Fieldref #4.#29 // com/sec/resourceparse/Person.age:I
   #4 = Class #30 // com/sec/resourceparse/Person
   #5 = Class #31 // java/lang/Object
   #6 = Utf8 name
   #7 = Utf8 Ljava/lang/String;

这里包含整个类里面的字符串,包含声明的类信息,属性等
最后是方法的信息:

 public java.lang.String getName();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #2 // Field name:Ljava/lang/String;
         4: areturn
      LineNumberTable:
        line 9: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/sec/resourceparse/Person;

这里主要是方法名,访问修饰符,以及操作栈执行流程信息
看完整个类的class文件,下面介绍字节码相关的基础知识。

访问修饰符

上述字节码中类,属性以及方法中均有flag信息,这个就是修饰符,在字节码中类访问修饰符及对应值如下所示:

标志符名称 标志符值 释义
ACC_PUBLIC 0x0001 Public 类型
ACC_FINAL 0x0010 Final类型
ACC_SUPER 0x0020 是否允许使用invokespecial字节码指令的新语义
ACC_INTERFACE 0x0200 接口修饰符
ACC_ABSTRACT 0x0400 abstract修饰符
ACC_SYNTHETIC 0x1000 标志这个类并非由用户代码生成
ACC_ANNOTATION 0x2000 注解修饰符
**ACC_ENUM 0x400 枚举修饰符

上面介绍的是类的访问修饰符,那么属性以及方法的也是类似的,只是相对而言比较简单,这里就不继续展开了。

类型对照表

JAVA中有基本类型,数组,以及对象,字节码中对类型的表示有所区别,对照表如下所示:

类型 字节码表示 释义
byte B 字节
boolean Z bool
char C 字符
short S 短整型
int I 整型
float F 浮点数
long J 长整型
double D 浮点数
void V 空返回值
Ljava/lang/Object; 对象类型
数组 [] [

其中类是以L开头,中间是类路径,最后以;结尾,上面的数组是单个数组,要结合其他类型一起使用,如int[]的字节码是[I,int[][]的字节码是[[I.

方法解析

上面已经介绍了访问修饰符以及JAVA字节码中类型对照,下面讲解一下方法的解析,拿上面的方法举例,如下所示:

public java.lang.String getName();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #2 // Field name:Ljava/lang/String;
         4: areturn
      LineNumberTable:
        line 9: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/sec/resourceparse/Person;
  • descriptor:方法描述,描述的是方法参数以及返回值类型,其格式为: (参数类型)返回值类型,这里表示方法为无参且返回值为String
  • flags: 为方法的访问修饰符,这里表示为Public
  • Code:具体方法栈的描述
  • stack:栈分配最大深度
  • locals:方法内局部变量个数
  • args_size:方法参数数量
  • LineNumberTable:方法行数信息(不关注,没有细看)
  • LocalVariableTable:局部变量对照表

这里简单解释一下,类方法最少有一个参数,这个参数就是类对象本身,相当于this关键字,而且下标是0。

字节码指令

上面已经介绍了字节码相关的基础知识,但是没有详细说明字节码指令相关内容,本节就重点介绍字节码指令内容,字节码指令主要分为如下几类:

  • 存储与加载类指令
    加载参数到操作栈,或者将操作栈中的数据存到局部变量中,主要包括load系列指令,store和push等指令
  • 对象操作指令
    对象指令主要包括new生成对象,从对象中获取属性等操作,如getField和putField以及getStatic和putStatic等
  • 栈管理指令 pop和dup等压栈和推出栈指令
  • 运算指令
    运算指令主要是对数据进行加减乘除等指令,这里也只会在操作栈中执行
  • 控制跳转指令
    ifelse等条件判断指令,还有goto等
  • 方法调用和返回指令
    主要包括invoke系列指令和return系列指令,其中invoke是执行方法的指令,return是返回系列指令

操作栈流程

上面已经基本介绍完字节码所有的内容了,这里实战讲解方法操作流程。先记住下面这个点:
JAVA方法执行都是基于栈进行的,方法调用指令调用后都会出栈,如果方法有返回值,则将返回值压栈

先分析一个简单的:

public java.lang.String getName();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #2 // Field name:Ljava/lang/String;
         4: areturn
      LineNumberTable:
        line 9: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/sec/resourceparse/Person;

1.aload_0:这里是将第0个参数,压栈,参数的类型是对象(前面分析过是this)

2.getfield:从当前栈顶对象获取name的属性,并且将其压入栈中

3.areturn:当前栈顶是一个String类型的值,所以返回的使用要使用areturn

再介绍一个稍微复杂一点的列子:

public class Manager {

    public static void main(String [] args) {
        String resPath = "/Users/Desktop/resources.arsc";
        FileInputStream ins = null;
        ByteArrayOutputStream ous = null;
        try {
            ins = new FileInputStream(new File(resPath));
            ous = new ByteArrayOutputStream();
            int length = -1;
            byte data[] = new byte[4 * 1024];
            while ((length = ins.read(data)) != -1) {
                ous.write(data, 0, length);
            }
            byte[] resData = ous.toByteArray();
            ParseUtils.parseRes(resData);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

对应的字节码如下所示:

 public com.sec.resourceparse.Manager();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1 // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/sec/resourceparse/Manager;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=5, locals=7, args_size=1
         0: ldc           #2 // String /Users/Desktop/resources.arsc
         2: astore_1
         3: aconst_null
         4: astore_2
         5: aconst_null
         6: astore_3
         7: new           #3 // class java/io/FileInputStream
        10: dup
        11: new           #4 // class java/io/File
        14: dup
        15: aload_1
        16: invokespecial #5 // Method java/io/File."<init>":(Ljava/lang/String;)V
        19: invokespecial #6 // Method java/io/FileInputStream."<init>":(Ljava/io/File;)V
        22: astore_2
        23: new           #7 // class java/io/ByteArrayOutputStream
        26: dup
        27: invokespecial #8 // Method java/io/ByteArrayOutputStream."<init>":()V
        30: astore_3
        31: iconst_m1
        32: istore        4
        34: sipush        4096
        37: newarray       byte
        39: astore        5
        41: aload_2
        42: aload         5
        44: invokevirtual #9 // Method java/io/FileInputStream.read:([B)I
        47: dup
        48: istore        4
        50: iconst_m1
        51: if_icmpeq     66
        54: aload_3
        55: aload         5
        57: iconst_0
        58: iload         4
        60: invokevirtual #10 // Method java/io/ByteArrayOutputStream.write:([BII)V
        63: goto          41
        66: aload_3
        67: invokevirtual #11 // Method java/io/ByteArrayOutputStream.toByteArray:()[B
        70: astore        6
        72: aload         6
        74: invokestatic  #12 // Method com/sec/resourceparse/ParseUtils.parseRes:([B)V
        77: goto          87
        80: astore        4
        82: aload         4
        84: invokevirtual #14 // Method java/lang/Exception.printStackTrace:()V
        87: return

这个Manager中只声明了一个static的main方法,但是字节码中有一个init的方法,其实就是默认的无参构造方法,先看一下这个方法的字节码:

public com.sec.resourceparse.Manager();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1 // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/sec/resourceparse/Manager;

1.aload_0:将this对象压入栈中
2.invokespecial:调用栈顶对象的特殊方法init方法,由于init的返回值类型为V,所以调用后栈顶就为空
3.return:由于栈顶没有值,所以直接执行return指令就可以了

再重点看下另外一个方法:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=5, locals=7, args_size=1
         0: ldc           #2 // String /Users/Desktop/resources.arsc
         2: astore_1
         3: aconst_null
         4: astore_2
         5: aconst_null
         6: astore_3
         7: new           #3 // class java/io/FileInputStream
        10: dup
        11: new           #4 // class java/io/File
        14: dup
        15: aload_1
        16: invokespecial #5 // Method java/io/File."<init>":(Ljava/lang/String;)V
        19: invokespecial #6 // Method java/io/FileInputStream."<init>":(Ljava/io/File;)V
        22: astore_2
        23: new           #7 // class java/io/ByteArrayOutputStream
        26: dup
        27: invokespecial #8 // Method java/io/ByteArrayOutputStream."<init>":()V
        30: astore_3
        31: iconst_m1
        32: istore        4
        34: sipush        4096
        37: newarray       byte
        39: astore        5
        41: aload_2
        42: aload         5
        44: invokevirtual #9 // Method java/io/FileInputStream.read:([B)I
        47: dup
        48: istore        4
        50: iconst_m1
        51: if_icmpeq     66
        54: aload_3
        55: aload         5
        57: iconst_0
        58: iload         4
        60: invokevirtual #10 // Method java/io/ByteArrayOutputStream.write:([BII)V
        63: goto          41
        66: aload_3
        67: invokevirtual #11 // Method java/io/ByteArrayOutputStream.toByteArray:()[B
        70: astore        6
        72: aload         6
        74: invokestatic  #12 // Method com/sec/resourceparse/ParseUtils.parseRes:([B)V
        77: goto          87
        80: astore        4
        82: aload         4
        84: invokevirtual #14 // Method java/lang/Exception.printStackTrace:()V
        87: return

方法解释:

  1. ([Ljava/lang/String;)V:参数是一个String的一维数组,无返回值
  2. flags:访问修饰符为static 和 public的

堆栈操作解释:
0:压栈一个String类型的对象,值为:"/Users/Desktop/resources.arsc"

2-6:

  1. 弹出栈顶元素并且存在局部变量1中
  2. 将null压入栈中
  3. 弹出栈顶元素null,并且存在局部变量2中
  4. 将null压入栈中
  5. 弹出栈顶元素null,并且存在局部变量3中

上面操作结束后,方法栈和局部变量如下所示:

7-30:

  1. new一个java/io/FileInputStream对象,并且压入栈中
  2. dup:将上面产生的对象再压入栈中,当前栈有2个FileInputStream对象了
  3. new一个java/io/File对象,并且压入栈中
  4. dup:将上面产生的File对象再压入栈中,当前栈有2个File对象了
  5. aload_1:将局部变量1压入栈中,也就是String值压入栈中
  6. invokespecial:调用File的init方法,参数是String,无返回值
    说明: 5-6就是将创建出来的File对象,调用其构造方法的过程,这里应该弄清楚为什么创建对象后要压2次栈了
  7. invokespecial:调用FileInputStream对象的方法,参数是File,无返回值
  8. astore_2:将栈顶元素存到局部变量2中
  9. new一个java/io/ByteArrayOutputStream对象,并且压入栈中
  10. dup:将上面产生的对象再压入栈中,当前栈中有2个ByteArrayOutputStream对象
  11. invokespecial:调用ByteArrayOutputStream的init方法
  12. 将栈顶元素存到局部变量3中

31-42

  1. iconst_m1:将-1压入栈中
  2. istore 4:将栈顶弹出,存到局部变量4中
  3. sipush 4096:将4096 int类型的数压入栈顶
  4. newarray :取出栈的数,创建数组,并压入栈中
  5. astore 5:弹出栈的元素并且存入局部变量5中
  6. aload_2:将局部变量2压入栈中
  7. aload 5:将局部变量5压入栈中
  8. invokevirtual:执行FileInputStream的read方法,参数为byte数组,返回值为int
  9. dup:复制栈顶元素,并且压入栈中
  10. istore 4:弹出栈顶元素并且存到局部变量4中
  11. iconst_m1:将-1再压入栈中
  12. if_icmpeq 66:比较栈顶2个int数是否相等,相等就直接跳到66行,负责执行下面的逻辑

上面逻辑基本就这样分析,这个方法比较长,就不继续向下分析,都是一样的步骤

操作栈流程的关键: 所有的操作都伴随着压栈和出栈的逻辑,如方法调用,使用到的在栈中的类和参数会被出栈,如果方法有返回值,则将返回值压栈。

总结

字节码知识还是比较重要的,理解字节码知识能清晰的理解JVM运行机制,同时为后面AOP直接操作字节码打下基础。

参考:
segmentfault.com/a/119000000… my.oschina.net/ta8210/blog…

点击这里复制本文地址 以上内容由权冠洲的博客整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!

支持Ctrl+Enter提交

联系我们| 本站介绍| 留言建议 | 交换友链 | 域名展示
本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除

权冠洲的博客 © All Rights Reserved.  Copyright quanguanzhou.top All Rights Reserved
苏公网安备 32030302000848号   苏ICP备20033101号-1

联系我们