1

安装插件

在setting-Plugins 搜索jclass安装下图中的插件。
image

查看字节码

1、首先我们写一个简单的java代码来观察其字节码:

public class TestExep {
 public static void main(String[] args) {
    new TestExep().f();
 }
 public int f(){
     int a = 1+3;
     a++;
     return a;
 }
}

2、运行后,点击源程序-》点击view->Show Bytecode With jclasslib
image

3、 我们现在看到上图右边一块就是jclasslib解析出来的字节码:
主要有下面几块内容:
image
1) General Information:

类的一些基本信息,主要信息如下:
    Major Version = 52 表示编译此类文件的jdk版本1.8
    Constant pool count = 27 是常量池大小
    Access Flag = 0x0021 [public] 表示类的访问修饰符
    This class : 本类
    Super class : 父类
    Interfaces count = 0 实现接口数0(类中没有实现任何接口)
    Fields count = 0 属性数(没有定义任何属性)
    Methods count = 3 这是方法数(类中只写了两个方法,为什么有3个方法呢,因为还有一个是类的默认构造方法<init>)
    

2) Constant pool: 常量池

我们看一下Oracle官方文档对常量池的描述:
`constant_pool[]

The constant_pool is a table of structures (§4.4) representing various string constants, class and interface names, field names, and other constants that are referred to within the ClassFile structure and its substructures. The format of each constant_pool table entry is indicated by its first "tag" byte.

The constant_pool table is indexed from 1 to constant_pool_count - 1.
常量池是一个结构体表,这些结构体表示字符串常量、类及接口名、属性名和类文件结构及子结构引用的其他常量,每个constant_pool表项的格式由其第一个“tag”字节表示。常量池表项索引从1到constant_pool_count-1(第0号不用)

从这段描述我们可以知道常量池不只放我们在程序中定义的常量,还有编译后的类相关信息。

3) Intefaces 接口
4) Fields 属性
5) Methods 方法:

方法里面就是编译后的字节码,
 Code: 编译后的字节码
 Code下面有两个东西:
 LineNumberTable : 行号表,是用来记录每行代码的行号,方便代码报错的时候打印出行号,为什么需要行号表呢? 因为源文件一行代码被jvm编译后的生成的字节码可能有多行,索引需要用行号表记录编码后的代码在源文件的行号。
LocalVariableTable: 局部变量表,存放方法运行时的局部变量,这是我们这次解读代码比较重要的东西。

方法f的字节码

我们点击方法f的Code,可以看到如下的字节码:

0 iconst_4
1 istore_1
2 iinc 1 by 1
5 iload_1
6 ireturn

现在看到这一坨代码还不知道是什么意思,我们需要先了解一下Java运行时的内存区及这些代码指令的含义。

java运行时数据区

1、 我们知道java运行的内存被分为两个部分一个是堆区,一个是栈区。
栈区是线程私有的,线程私有的部分包括3个东西:
PC: 程序计数器
Native Method stack: 本地方法栈
JVM stack: jvm栈,每次调用Java方法的对应一个栈帧
2、 当我们的程序开始运行的时候,对应一个线程,当调用f方法的时候,会在这个线程的jvm栈里面生成一个栈帧记录方法运行时的数据。
那么运行时包含哪些数据呢:

1.  Local Variable Table 局部变量表
2.  Operand Stack 操作数栈
3.  Dynamic Linking 
4.  return address a() -> b(),方法a调用了方法b, b方法的返回值放在什么地方

其中,局部变量表和操作数栈是两个比较重要的东西,帮忙我们理解jvm指令。

指令集

jvm的指令集是基于栈,意思是任何jvm指令执行的操作数都是在操作数栈弹出来的,计算后的结果也压到里面。
所以我们看到的指令全是没有操作数的,因为默认从栈里取操作数。
如果计算的结果需要在后面使用,那么需要把结果存放到局部变量表中。
例如:
iload这条指令的jvm官方文档描述(https://docs.oracle.com/javase/specs/jvms/se15/html/jvms-6.html#jvms-6.5.iload
)

Description
The _index_ is an unsigned byte that must be an index into the local variable array of the current frame ([§2.6](https://docs.oracle.com/javase/specs/jvms/se15/html/jvms-2.html#jvms-2.6 "2.6. Frames")). The local variable at _index_ must contain an `int`. The _value_ of the local variable at _index_ is pushed onto the operand stack.
index是无符号byte类型,是当前栈帧局部变量表里的索引,index 将当前栈帧局部变量表的的index位置变量的值压到操作数栈里。

方法f代码解读

首先我们来看一下,当方法f被调用是栈帧的情况:
首先调用main有个Main方法的栈帧,然后调用f方法,又有一个f方法的栈帧。
image

下面我们看一下方法f的字节码

0 iconst_4
1 istore_1
2 iinc 1 by 1
5 iload_1
6 ireturn

首先我们需要一个局部变量表,一个操作数栈:

1、 iconst_4: 表示把整数常量4压入操作数栈(但是我们的程序里面没有常量4,只有1+3 编译器直接把结果计算出来,这样程序运行的时候就不需要计算了),此时操作数栈栈顶元素是4
这条指令执行后局部变量表(LVT)和操作数栈(Oprand)的情况:
image

2、istore_1: 把操作数栈顶元素出栈并存入局部变量表索引1位置,此时a=4, LVT和Oprand情况:
image

3、iinc 1 by 1: 把局部变量表索引1位置的数+1,即a++,此时a=5,LVT和Oprand情况:
image

4、iload_1: 将局部变量表索引1位置的数压入操作数栈,此时操作数栈有一个值,LVT和Oprand情况如下:
image

5、ireturn: 将操作数栈顶元素返回给main方法, 方法返回后,整个方法栈帧都会被删除。

总结

以上展示了一个简单的java程序的字节码阅读方法,关键是理解java的字节码指令基于栈编程模型,这样就好理解了,jvm指令在jvm规范中都可以查到,只要理解了这个,碰到不认识的指令,直接去jvm规范查询,我们就可以轻松看懂字节码。
我们看字节码可以看到jvm编译class文件过程中做的一些优化,比如常量运算直接在编译阶段就做了,执行阶段不需要再处理。
看字节码可以加深我们对Java的理解。
更重要的是jvm跟java无关,很多语言都可以在jvm上执行,比如scala, jypthon, kotlin等等,只要一门语言写的程序可以按jvm规范编译成.class文件就可以跑在java虚拟机上。


杜若
70 声望3 粉丝