Java 字节码指令是 JVM 体系中很是难啃的一块硬骨头,我估计有些读者会有这样的疑惑,“Java 字节码难学吗?我能不能学会啊?”java
讲良心话,不是我谦虚,一开始学 Java 字节码和 Java 虚拟机方面的知识我也感受头大!但硬着头皮学了一阵子以后,忽然就开窍了,以为好有意思,尤为是明白了 Java 代码在底层居然是这样执行的时候,感受既膨胀又飘飘然,浑身上下散发着自信的光芒!git
我在 掘金 共输出了 100 多篇 Java 方面的文章,总字数超过 30 万字, 内容风趣幽默、通俗易懂,收获了不少初学者的承认和支持,内容包括 Java 语法、Java 集合框架、Java 并发编程、Java 虚拟机等核心内容。 为了帮助更多的 Java 初学者,我“一怒之下”就把这些文章从新整理并开源到了 GitHub,起名《教妹学 Java》,听起来是否是就颇有趣?github
GitHub 开源地址(欢迎 star):github.com/itwanger/jm…编程
Java 官方的虚拟机 Hotspot 是基于栈的,而不是基于寄存器的。segmentfault
基于栈的优势是可移植性更好、指令更短、实现起来简单,但不能随机访问栈中的元素,完成相同功能所须要的指令数也比寄存器的要多,须要频繁的入栈和出栈。数组
基于寄存器的优势是速度快,有利于程序运行速度的优化,但操做数须要显式指定,指令也比较长。markdown
Java 字节码由操做码和操做数组成。并发
因为 Java 虚拟机是基于栈而不是寄存器的结构,因此大多数指令都只有一个操做码。好比 aload_0
(将局部变量表中下标为 0 的数据压入操做数栈中)就只有操做码没有操做数,而 invokespecial #1
(调用成员方法或者构造方法,并传递常量池中下标为 1 的常量)就是由操做码和操做数组成的。框架
加载(load)和存储(store)相关的指令是使用最频繁的指令,用于将数据从栈帧的局部变量表和操做数栈之间来回传递。jvm
1)将局部变量表中的变量压入操做数栈中
解释一下。
x 为操做码助记符,代表是哪种数据类型。见下表所示。
像 arraylength 指令,没有操做码助记符,它没有表明数据类型的特殊字符,但操做数只能是一个数组类型的对象。
大部分的指令都不支持 byte、short 和 char,甚至没有任何指令支持 boolean 类型。编译器会将 byte 和 short 类型的数据带符号扩展(Sign-Extend)为 int 类型,将 boolean 和 char 零位扩展(Zero-Extend)为 int 类型。
举例来讲。
private void load(int age, String name, long birthday, boolean sex) {
System.out.println(age + name + birthday + sex);
}
复制代码
经过 jclasslib 看一下 load()
方法(4 个参数)的字节码指令。
经过查看局部变量表就能关联上了。
2)将常量池中的常量压入操做数栈中
根据数据类型和入栈内容的不一样,此类又能够细分为 const 系列、push 系列和 Idc 指令。
const 系列,用于特殊的常量入栈,要入栈的常量隐含在指令自己。
push 系列,主要包括 bipush 和 sipush,前者接收 8 位整数做为参数,后者接收 16 位整数。
Idc 指令,当 const 和 push 不能知足的时候,万能的 Idc 指令就上场了,它接收一个 8 位的参数,指向常量池中的索引。
Idc_w
:接收两个 8 位数,索引范围更大。Idc2_w
指令。举例来讲。
public void pushConstLdc() {
// 范围 [-1,5]
int iconst = -1;
// 范围 [-128,127]
int bipush = 127;
// 范围 [-32768,32767]
int sipush= 32767;
// 其余 int
int ldc = 32768;
String aconst = null;
String IdcString = "沉默王二";
}
复制代码
经过 jclasslib 看一下 pushConstLdc()
方法的字节码指令。
3)将栈顶的数据出栈并装入局部变量表中
主要是用来给局部变量赋值,这类指令主要以 store 的形式存在。
明白了 xload_ 和 xload,再看 xstore_ 和 xstore 就会轻松得多,做用反了一下而已。
你们来想一个问题,为何要有 xstore_ 和 xload_ 呢?它们的做用和 xstore n、xload n 不是同样的吗?
xstore_ 和 xstore n 的区别在于,前者至关于只有操做码,占用 1 个字节;后者至关于由操做码和操做数组成,操做码占 1 个字节,操做数占 2 个字节,一共占 3 个字节。
因为局部变量表中前几个位置老是很是经常使用,虽然 xstore_<n>
和 xload_<n>
增长了指令数量,但字节码的体积变小了!
举例来讲。
public void store(int age, String name) {
int temp = age + 2;
String str = name;
}
复制代码
经过 jclasslib 看一下 store()
方法的字节码指令。
经过查看局部变量表就能关联上了。
算术指令用于对两个操做数栈上的值进行某种特定运算,并把结果从新压入操做数栈。能够分为两类:整型数据的运算指令和浮点数据的运算指令。
须要注意的是,数据运算可能会致使溢出,好比两个很大的正整数相加,极可能会获得一个负数。但 Java 虚拟机规范中并无对这种状况给出具体结果,所以程序是不会显式报错的。因此,你们在开发过程当中,若是涉及到较大的数据进行加法、乘法运算的时候,必定要注意!
当发生溢出时,将会使用有符号的无穷大 Infinity 来表示;若是某个操做结果没有明确的数学定义的话,将会使用 NaN 值来表示。并且全部使用 NaN 做为操做数的算术操做,结果都会返回 NaN。
举例来讲。
public void infinityNaN() {
int i = 10;
double j = i / 0.0;
System.out.println(j); // Infinity
double d1 = 0.0;
double d2 = d1 / 0.0;
System.out.println(d2); // NaN
}
复制代码
Java 虚拟机提供了两种运算模式:
我把全部的算术指令列一下:
举例来讲。
public void calculate(int age) {
int add = age + 1;
int sub = age - 1;
int mul = age * 2;
int div = age / 3;
int rem = age % 4;
age++;
age--;
}
复制代码
经过 jclasslib 看一下 calculate()
方法的字节码指令。
能够分为两种:
1)宽化,小类型向大类型转换,好比 int–>long–>float–>double
,对应的指令有:i2l、i2f、i2d、l2f、l2d、f2d。
2)窄化,大类型向小类型转换,好比从 int 类型到 byte、short 或者 char,对应的指令有:i2b、i2s、i2c;从 long 到 int,对应的指令有:l2i;从 float 到 int 或者 long,对应的指令有:f2i、f2l;从 double 到 int、long 或者 float,对应的指令有:d2i、d2l、d2f。
举例来讲。
public void updown() {
int i = 10;
double d = i;
float f = 10f;
long ong = (long)f;
}
复制代码
经过 jclasslib 看一下 updown()
方法的字节码指令。
Java 是一门面向对象的编程语言,那么 Java 虚拟机是如何从字节码层面进行支持的呢?
1)建立指令
数组也是一种对象,但它建立的字节码指令和普通的对象不一样。建立数组的指令有三种:
普通对象的建立指令只有一个,就是 new
,它会接收一个操做数,指向常量池中的一个索引,表示要建立的类型。
举例来讲。
public void newObject() {
String name = new String("沉默王二");
File file = new File("无愁河的浪荡汉子.book");
int [] ages = {};
}
复制代码
经过 jclasslib 看一下 newObject()
方法的字节码指令。
new #13 <java/lang/String>
,建立一个 String 对象。new #15 <java/io/File>
,建立一个 File 对象。newarray 10 (int)
,建立一个 int 类型的数组。2)字段访问指令
字段能够分为两类,一类是成员变量,一类是静态变量(static 关键字修饰的),因此字段访问指令能够分为两类:
举例来讲。
public class Writer {
private String name;
static String mark = "做者";
public static void main(String[] args) {
print(mark);
Writer w = new Writer();
print(w.name);
}
public static void print(String arg) {
System.out.println(arg);
}
}
复制代码
经过 jclasslib 看一下 main()
方法的字节码指令。
getstatic #2 <com/itwanger/jvm/Writer.mark>
,访问静态变量 markgetfield #6 <com/itwanger/jvm/Writer.name>
,访问成员变量 name方法调用指令有 5 个,分别用于不一样的场景:
举例来讲。
public class InvokeExamples {
private void run() {
List ls = new ArrayList();
ls.add("难顶");
ArrayList als = new ArrayList();
als.add("学不动了");
}
public static void print() {
System.out.println("invokestatic");
}
public static void main(String[] args) {
print();
InvokeExamples invoke = new InvokeExamples();
invoke.run();
}
}
复制代码
咱们用 javap -c InvokeExamples.class
来反编译一下。
Compiled from "InvokeExamples.java"
public class com.itwanger.jvm.InvokeExamples {
public com.itwanger.jvm.InvokeExamples();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
private void run();
Code:
0: new #2 // class java/util/ArrayList
3: dup
4: invokespecial #3 // Method java/util/ArrayList."<init>":()V
7: astore_1
8: aload_1
9: ldc #4 // String 难顶
11: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
16: pop
17: new #2 // class java/util/ArrayList
20: dup
21: invokespecial #3 // Method java/util/ArrayList."<init>":()V
24: astore_2
25: aload_2
26: ldc #6 // String 学不动了
28: invokevirtual #7 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
31: pop
32: return
public static void print();
Code:
0: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #9 // String invokestatic
5: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
public static void main(java.lang.String[]);
Code:
0: invokestatic #11 // Method print:()V
3: new #12 // class com/itwanger/jvm/InvokeExamples
6: dup
7: invokespecial #13 // Method "<init>":()V
10: astore_1
11: aload_1
12: invokevirtual #14 // Method run:()V
15: return
}
复制代码
InvokeExamples 类有 4 个方法,包括缺省的构造方法在内。
1)InvokeExamples()
构造方法中
缺省的构造方法内部会调用超类 Object 的初始化构造方法:
`invokespecial #1 // Method java/lang/Object."<init>":()V`
复制代码
2)成员方法 run()
中
invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
复制代码
因为 ls 变量的引用类型为接口 List,因此 ls.add()
调用的是 invokeinterface
指令,等运行时再肯定是否是接口 List 的实现对象 ArrayList 的 add()
方法。
invokevirtual #7 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
复制代码
因为 als 变量的引用类型已经肯定为 ArrayList,因此 als.add()
方法调用的是 invokevirtual
指令。
3)main()
方法中
invokestatic #11 // Method print:()V
复制代码
print()
方法是静态的,因此调用的是 invokestatic
指令。
方法返回指令根据方法的返回值类型进行区分,常见的返回指令见下图。
常见的操做数栈管理指令有 pop、dup 和 swap。
这些指令不须要指明数据类型,由于是按照位置压入和弹出的。
举例来讲。
public class Dup {
int age;
public int incAndGet() {
return ++age;
}
}
复制代码
经过 jclasslib 看一下 incAndGet()
方法的字节码指令。
控制转移指令包括:
1)比较指令
比较指令有:dcmpg,dcmpl、fcmpg、fcmpl、lcmp,指令的第一个字母表明的含义分别是 double、float、long。注意,没有 int 类型。
对于 double 和 float 来讲,因为 NaN 的存在,有两个版本的比较指令。拿 float 来讲,有 fcmpg 和 fcmpl,区别在于,若是遇到 NaN,fcmpg 会将 1 压入栈,fcmpl 会将 -1 压入栈。
举例来讲。
public void lcmp(long a, long b) {
if(a > b){}
}
复制代码
经过 jclasslib 看一下 lcmp()
方法的字节码指令。
lcmp 用于两个 long 型的数据进行比较。
2)条件跳转指令
这些指令都会接收两个字节的操做数,它们的统一含义是,弹出栈顶元素,测试它是否知足某一条件,知足的话,跳转到对应位置。
对于 long、float 和 double 类型的条件分支比较,会先执行比较指令返回一个整形值到操做数栈中后再执行 int 类型的条件跳转指令。
对于 boolean、byte、char、short,以及 int,则直接使用条件跳转指令来完成。
举例来讲。
public void fi() {
int a = 0;
if (a == 0) {
a = 10;
} else {
a = 20;
}
}
复制代码
经过 jclasslib 看一下 fi()
方法的字节码指令。
3 ifne 12 (+9)
的意思是,若是栈顶的元素不等于 0,跳转到第 12(3+9)行 12 bipush 20
。
3)比较条件转指令
前缀“if_”后,以字符“i”开头的指令针对 int 型整数进行操做,以字符“a”开头的指令表示对象的比较。
举例来讲。
public void compare() {
int i = 10;
int j = 20;
System.out.println(i > j);
}
复制代码
经过 jclasslib 看一下 compare()
方法的字节码指令。
11 if_icmple 18 (+7)
的意思是,若是栈顶的两个 int 类型的数值比较的话,若是前者小于后者时跳转到第 18 行(11+7)。
4)多条件分支跳转指令
主要有 tableswitch 和 lookupswitch,前者要求多个条件分支值是连续的,它内部只存放起始值和终止值,以及若干个跳转偏移量,经过给定的操做数 index,能够当即定位到跳转偏移量位置,所以效率比较高;后者内部存放着各个离散的 case-offset 对,每次执行都要搜索所有的 case-offset 对,找到匹配的 case 值,并根据对应的 offset 计算跳转地址,所以效率较低。
举例来讲。
public void switchTest(int select) {
int num;
switch (select) {
case 1:
num = 10;
break;
case 2:
case 3:
num = 30;
break;
default:
num = 40;
}
}
复制代码
经过 jclasslib 看一下 switchTest()
方法的字节码指令。
case 2 的时候没有 break,因此 case 2 和 case 3 是连续的,用的是 tableswitch。若是等于 1,跳转到 28 行;若是等于 2 和 3,跳转到 34 行,若是是 default,跳转到 40 行。
5)无条件跳转指令
goto 指令接收两个字节的操做数,共同组成一个带符号的整数,用于指定指令的偏移量,指令执行的目的就是跳转到偏移量给定的位置处。
前面的例子里都出现了 goto 的身影,也很好理解。若是指令的偏移量特别大,超出了两个字节的范围,可使用指令 goto_w,接收 4 个字节的操做数。
巨人的肩膀:
除了以上这些指令,还有异常处理指令和同步控制指令,我打算吊一吊你们的胃口,你们能够期待一波~~
(骚操做)
路漫漫其修远兮,吾将上下而求索
想要走得更远,Java 字节码这块就必须得硬碰硬地吃透,但愿二哥的这些分享能够帮助到你们~
二哥在 掘金 上写了不少 Java 方面的系列文章,有 Java 核心语法、Java 集合框架、Java IO、Java 并发编程、Java 虚拟机等,也算是体系完整了。
为了能帮助到更多的 Java 初学者,二哥把本身连载的《教妹学Java》开源到了 GitHub,尽管只整理了 50 篇,发现字数已经来到了 10 万+,内容更是没得说,通俗易懂、风趣幽默、图文并茂。
GitHub 开源地址(欢迎 star):github.com/itwanger/jm…
若是有帮助的话,还请给二哥点个赞,这将是我继续分享下去的最强动力!