学习完JVM的分区以及.class文件的加载机制,下面来学习下通过工具查看栈是如何操作字节码的。
抛出几个问题
1、怎么查看字节码?
2、字节码是什么样子的?
3、对象初始化后,字节码是如何被栈执行的?
让我一起动手来实践下,详细的分析一个java文件产生的字节码,并从栈帧层面看一下字节码的具体执行过程
工具介绍
javap
javap是JDK自带的反解析工具,它的作用是将.class文件解析成可读的文件格式。使用javap -p -v XXX.class -p参数:打印一些私有的字段和方法;-v参数:尽量多的打印一些信息
在stack Overflow上有个有意思的问题:在某个类中增加一行注释之后,为什么两次生成的.class文件,它们的md5是不一样的?
因为在javac命令中可以指定一些额外的内容输出到字节码。
常用的有
javac -g:lines 强制生成LineNumberTable
javac -g:vars 强制生成LocalVariableTable
javac -g 生成所有的debug信息
为了观察字节码,以上命令要熟练掌握
jclasslib
jclasslib是一个图形化的工具,能够直接查看字节码的内容。jclasslib下载地址
类加载和对象创建
首先,写一个简单的Java程序A.java。
class B{
private int a = 1234;
static long C = 1111;
public long test(long num) {
long ret = this.a + num + C;
return ret;
}
}
public class A {
private B b = new B();
public static void main(String[] args) {
A a = new A();
long num = 4321;
long ret = a.b.test(num);
System.out.println("ret:"+ret);
}
}
类的初始化发生在类加载的过程中,那么对象的初始化呢?
对象的初始化常用的new 方法,还有其他的方法
Class 的newInstance
Constructor类的newInstance方法
反序列化
使用Object的clone方法
其中,后两个方法没有用到构造函数。
当虚拟机遇到一条new指令时,首先会检查指令的参数能否在常量池中定位一个符号引用。然后检查这个符号引用的类字节码是否加载、解析和初始化。如果没有,将执行对应的类加载。
在上面的代码中,执行过程中,在调用private B b = new B()时,会触发B类的类加载。
通过命令查看类B的test方法的字节码

介绍下三个比较重要的值
1>stack 值是4 。表明test方法的最大操作数栈深度为4。JVM运行时,会根据这个值,来分配栈帧中操作栈的深度。
2>locals变量存储了局部变量的存储空间。它的单位是Slot槽,可以被重用。其中存放的内容包括:this、方法参数、异常处理器的参数和方法体中定义的局部变量。
3>args_size指的是方法的参数个数,每个方法都有一个隐藏的参数this,所有数值是2。
字节码执行过程
main线程会拥有两个主要的运行时区域:Java虚拟机栈和程序计数器。其中,虚拟机栈中的每一项内容叫作栈帧,栈帧中包含四项内容:局部变量表、操作数栈、动态链接和完成出栈。

我们的字节码指令,就是靠操作这些数据结构运行的。下面看下具体的字节码指令

1)0:aload_0把第1个引用型局部变量推到操作数栈,这里的意思是把this装载到操作数栈中
2)1:getfield #2 将栈顶的指定的对象的第二个实例域(Field)的值,压入栈顶。#2指的是我们的成员变量a
3)i2l 将栈顶int类型的数据转换成Long类型,这里就涉及我们的隐式类型转换了
4)lload_1 将第一个局部变量入栈。也就是我们的num。这里的l标示long,同样用于局部变量装载。
5)ladd 将栈顶两个long型数值出栈后相加,并将结果入栈。
6)getstatic #3 根据偏移获取静态属性的值,并把这个值push到操作数栈上。
7)ladd 再次执行ladd。
8)lstore_3 将栈顶long型数值存入到第4个局部变量。
9)lload_3 正好与上面相反。上面是变量存入,现在要做的就是把这个变量ret,压入虚拟栈中。
10) lreturn 从当前方法返回long。
至此,函数完成了相加动作,执行成功。JVM为我们提供了非常丰富的字节码指令。详细的字节码指令列表,可以参考下面链接 https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html
注意点
注意上面的第8步,首先把变量存放到了变量报表,然后又拿出这个值,把它入栈。为什么多此一举的操作呢?因为在于我们定义了ret变量,JVM不知道执行顺序,只好默认的顺序执行。
假如不生成变量 直接返回相加的值,这样就会很好理解了。由于栈的操作复杂度是O(1),对于我们的程序性能几乎没有影响,不必尽量少的定义成员变量,因此平常编码,还是要以可读性为首要任务。
小结
通过这次学习,掌握了基础的字节码指令对程序计数器、局部变量表、操作数栈等内容的影响,初步了解了Java字节码的文件格式。
希望可以建立一个运行时的全局动态图,在看到相关的opcode时,能够举一反三的思考背后对这些数据结构的操作。这样理解字节码指令,才能做到事半功倍。
网友评论