前不久《深刻理解Java虚拟机》第三版发布了,赶忙买来看了看新版的内容,这本书更新了不少新版本虚拟机的内容,还对之前的部份内容进行了重构,仍是值得去看的。本着复习和巩固的态度,我决定来编译一个简单的类文件来分析Java的字节码内容,来帮助理解和巩固Java字节码知识,但愿也对阅读本文的你有所帮助。java
说明:本次采用的环境是OpenJdk12python
首先咱们须要写个简单的小程序,1+1的程序,学习就要从最简单的1+1开始,代码以下:小程序
package top.luozhou.test;
/**
* @description:
* @author: luozhou
* @create: 2019-12-25 21:28
**/
public class TestJava {
public static void main(String[] args) {
int a=1+1;
System.out.println(a);
}
}
复制代码
写好java类文件后,首先执行命令javac TestJava.java
编译类文件,生成TestJava.class
。 而后执行反编译命令javap -verbose TestJava
,字节码结果显示以下:bash
Compiled from "TestJava.java"
public class top.luozhou.test.TestJava
minor version: 0
major version: 56
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#14 // java/lang/Object."<init>":()V
#2 = Fieldref #15.#16 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Methodref #17.#18 // java/io/PrintStream.println:(I)V
#4 = Class #19 // top/luozhou/test/TestJava
#5 = Class #20 // java/lang/Object
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 main
#11 = Utf8 ([Ljava/lang/String;)V
#12 = Utf8 SourceFile
#13 = Utf8 TestJava.java
#14 = NameAndType #6:#7 // "<init>":()V
#15 = Class #21 // java/lang/System
#16 = NameAndType #22:#23 // out:Ljava/io/PrintStream;
#17 = Class #24 // java/io/PrintStream
#18 = NameAndType #25:#26 // println:(I)V
#19 = Utf8 top/luozhou/test/TestJava
#20 = Utf8 java/lang/Object
#21 = Utf8 java/lang/System
#22 = Utf8 out
#23 = Utf8 Ljava/io/PrintStream;
#24 = Utf8 java/io/PrintStream
#25 = Utf8 println
#26 = Utf8 (I)V
{
public top.luozhou.test.TestJava();
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 8: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: iconst_2
1: istore_1
2: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
5: iload_1
6: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
9: return
LineNumberTable:
line 10: 0
line 11: 2
line 12: 9
}
复制代码
1.基础信息学习
上述结果删除了部分不影响解析的冗余信息,接下来咱们便来解析字节码的结果。优化
minor version: 0 次版本号,为0表示未使用
major version: 56 主版本号,56表示jdk12,表示只能运行在jdk12版本以及以后的虚拟机中
复制代码
flags: ACC_PUBLIC, ACC_SUPER
复制代码
ACC_PUBLIC
:这就是一个是不是public类型的访问标志。ui
ACC_SUPER
: 这个falg是为了解决经过 invokespecial
指令调用 super 方法的问题。能够将它理解成 Java 1.0.2 的一个缺陷补丁,只有经过这样它才能正确找到 super 类方法。从 Java 1.0.2 开始,编译器始终会在字节码中生成 ACC_SUPER 访问标识。感兴趣的同窗能够点击这里来了解更多。this
2.常量池编码
接下来,咱们将要分析常量池,你也能够对照上面总体的字节码来理解。spa
#1 = Methodref #5.#14 // java/lang/Object."<init>":()V
复制代码
这是一个方法引用,这里的#5
表示索引值,而后咱们能够发现索引值为5的字节码以下
#5 = Class #20 // java/lang/Object
复制代码
它表示这是一个Object
类,同理#14
指向的是一个"<init>":()V
表示引用的是初始化方法。
#2 = Fieldref #15.#16 // java/lang/System.out:Ljava/io/PrintStream;
复制代码
上面这段表示是一个字段引用,一样引用了#15
和#16
,实际上引用的就是java/lang/System
类中的PrintStream
对象。其余的常量池分析思路是同样的,鉴于篇幅我就不一一说明了,只列下其中的几个关键类型和信息。
NameAndType
:这个表示是名称和类型的常量表,能够指向方法名称或者字段的索引,在上面的字节码中都是表示的实际的方法。
Utf8
:咱们常用的是字符编码,可是这个不是只有字符编码的意思,它表示一种字符编码是Utf8
的字符串。它是虚拟机中最经常使用的表结构,你能够理解为它能够描述方法,字段,类等信息。 好比:
#4 = Class #19
#19 = Utf8 top/luozhou/test/TestJava
复制代码
这里表示#4
这个索引下是一个类,而后指向的类是#19
,#19
是一个Utf8
表,最终存放的是top/luozhou/test/TestJava
,那么这样一链接起来就能够知道#4
位置引用的类是top/luozhou/test/TestJava
了。
3.构造方法信息
接下来,咱们分析下构造方法的字节码,咱们知道,一个类初始化的时候最早执行它的构造方法,若是你没有写构造方法,系统会默认给你添加一个无参的构造方法。
public top.luozhou.test.TestJava();
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 8: 0
复制代码
descriptor: ()V
:表示这是一个没有返回值的方法。
flags: ACC_PUBLIC
:是公共方法。
stack=1, locals=1, args_size=1
:表示栈中的数量为1,局部变量表中的变量为1,调用参数也为1。
这里为何都是1呢?这不是默认的构造方法吗?哪来的参数?其实Java语言有一个潜规则:在任何实例方法里面均可以经过this
来访问到此方法所属的对象。而这种机制的实现就是经过Java编译器在编译的时候做为入参传入到方法中了,熟悉python
语言的同窗确定会知道,在python
中定义一个方法总会传入一个self
的参数,这也是传入此实例的引用到方法内部,Java只是把这种机制后推到编译阶段完成而已。因此,这里的1都是指this
这个参数而已。
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 8: 0
复制代码
通过上面这个分析对于这个构造方法表达的意思也就很清晰了。
aload_0
:表示把局部变量表中的第一个变量加载到栈中,也就是this
。
invokespecial
:直接调用初始化方法。
return
:调用完毕方法结束。
LineNumberTable:
这是一个行数的表,用来记录字节码的偏移量和代码行数的映射关系。line 8: 0
表示,源码中第8行对应的就是偏移量0
的字节码,由于是默认的构造方法,因此这里并没有法直观体现出来。
另外这里会执行Object
的构造方法是由于,Object
是全部类的父类,子类的构造要先构造父类的构造方法。
4.main方法信息
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: iconst_2
1: istore_1
2: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
5: iload_1
6: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
9: return
LineNumberTable:
line 10: 0
line 11: 2
line 12: 9
复制代码
有了以前构造方法的分析,咱们接下来分析main
方法也会熟悉不少,重复的我就略过了,这里重点分析code
部分。
stack=2, locals=2, args_size=1
:这里的栈和局部变量表为2,参数仍是为1。这是为何呢?由于main
方法中声明了一个变量a
,因此局部变量表要加一个,栈也是,因此他们是2。那为何args_size
仍是1呢?你不是说默认会把this
传入的吗?应该是2啊。注意:以前说的是在任何实例方法中,而这个main方法是一个静态方法,静态方法直接能够经过类+方法名访问,并不须要实例对象,因此这里就不必传入了。
0: iconst_2
:将int
类型2推送到栈顶。
1: istore_1
:将栈顶int
类型数值存入第二个本地变量。
2: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
:获取PrintStream
类。
5: iload_1
: 把第二个int
型本地变量推送到栈顶。
6: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
:调用println
方法。这里的println
方法就会把栈顶的元素做为本身的入参来执行,最终也会输出2。
9: return
:调用完毕结束方法。
这里的LineNumberTable
是有源码的,咱们能够对照下我前面描述是否正确:
line 10: 0
: 第10行表示0: iconst_2
字节码,这里咱们发现编译器直接给咱们计算好了把2推送到栈顶了。
line 11: 2
:第11行源码对应的是2: getstatic
获取输出的静态类PrintStream
。
line 12: 9
:12行源码对应的是return
,表示方法结束。
这里我也画了一个动态图片来演示main
方法执行的过程,但愿可以帮助你理解:
这篇文章我从1+1的的源码编译开始,分析了生成后的Java字节码,包括类的基本信息,常量池,方法调用过程等,经过这些分析,咱们对Java字节码有了比较基本的了解,也知道了Java编译器会把优化手段经过编译好的字节码体现出来,好比咱们的1+1=2,字节码字节赋值一个2给变量,而不是进行加法运算,从而优化了咱们的代码,提搞了执行效率。