java虚拟机(十四)--字节码指令
字节码指令其实是很重要的,在之前学习String等内容,深入到字节码层面很容易找到答案,而不是只是在网上寻找答案,还有可能是错误的。
PS:本文基于jdk1.8
首先写个简单的类:
public class Test { public static Integer f1() {
int a = 1;
int b = 2;
return a + b;
} public static void main(String[] args) {
int m = 100;
int c = f1();
System.out.println(m + c);
} }
反编译:
先通过javac编译,然后通过javap -verbose Test.class > Test.txt把反编译结果重定向到txt文件中
//类文件
Classfile /D:/Java/project/monitor/target/classes/com/it/test1/Test.class
//最后修改,文件大小
Last modified 2019-7-16; size 785 bytes
MD5 checksum 1dc6eb4c2e233f63edbb50e709c20111
//编译来自Test.java
Compiled from "Test.java"
//以下为类信息
public class com.it.test1.Test
//jdk版本
minor version: 0
major version: 52
//类的访问修饰符,public和super
flags: ACC_PUBLIC, ACC_SUPER
//2、常量池,下面1,2,3,4....50,相当于索引,这部分简单理解就行了,主要是程序部分
Constant pool:
//Methodref方法引用,#8.#29代表引用第8行和29行
#1 = Methodref #8.#29 // java/lang/Object."<init>":()V
//自动装箱,执行Integer.valueOf()
#2 = Methodref #30.#31 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
#3 = Methodref #7.#32 // com/it/test1/Test.f1:()Ljava/lang/Integer;
#4 = Methodref #30.#33 // java/lang/Integer.intValue:()I
//Fieldref字段引用,L为引用类型,格式为L ClassName;注意最后还有冒号;
#5 = Fieldref #34.#35 // java/lang/System.out:Ljava/io/PrintStream;
#6 = Methodref #36.#37 // java/io/PrintStream.println:(I)V
//类
#7 = Class #38 // com/it/test1/Test
#8 = Class #39 // java/lang/Object
#Utf8可以理解为字符串,<init>相当于构造函数
#9 = Utf8 <init>
//()V,无参,返回值为void
#10 = Utf8 ()V
#11 = Utf8 Code
#12 = Utf8 LineNumberTable
#13 = Utf8 LocalVariableTable //本地变量表
#14 = Utf8 this
#15 = Utf8 Lcom/it/test1/Test;
#16 = Utf8 f1
#17 = Utf8 ()Ljava/lang/Integer;
#18 = Utf8 a
#19 = Utf8 I
#20 = Utf8 b
#21 = Utf8 main
#22 = Utf8 ([Ljava/lang/String;)V
#23 = Utf8 args
#24 = Utf8 [Ljava/lang/String;
#25 = Utf8 m
#26 = Utf8 c
#27 = Utf8 SourceFile
#28 = Utf8 Test.java
//NameAndType,名称和返回值
#29 = NameAndType #9:#10 // "<init>":()V
#30 = Class #40 // java/lang/Integer
#31 = NameAndType #41:#42 // valueOf:(I)Ljava/lang/Integer;
#32 = NameAndType #16:#17 // f1:()Ljava/lang/Integer;
#33 = NameAndType #43:#44 // intValue:()I
#34 = Class #45 // java/lang/System
#35 = NameAndType #46:#47 // out:Ljava/io/PrintStream;
#36 = Class #48 // java/io/PrintStream
#37 = NameAndType #49:#50 // println:(I)V
#38 = Utf8 com/it/test1/Test
#39 = Utf8 java/lang/Object
#40 = Utf8 java/lang/Integer
#41 = Utf8 valueOf
#42 = Utf8 (I)Ljava/lang/Integer;
#43 = Utf8 intValue
#44 = Utf8 ()I
#45 = Utf8 java/lang/System
#46 = Utf8 out
#47 = Utf8 Ljava/io/PrintStream;
#48 = Utf8 java/io/PrintStream
#49 = Utf8 println
#50 = Utf8 (I)V
//程序部分开始
{
public com.it.test1.Test();
//默认构造器,无参,无返回值
descriptor: ()V
//修饰符public
flags: ACC_PUBLIC
//Code部分
Code:
# 操作数栈的深度2
# 本地变量表最大长度(slot为单位),64位的是2个,其他是1个,索引从0开始,如果是非static方法索引0代表this,后面是入参,后面是本地变量
# 1个参数,实例方法多一个this参数
//args_size
stack=1, locals=1, args_size=1
//aload_<n>从本地变量加载引用,n为当前栈帧中局部变量数组的索引
0: aload_0
//invokespecial调用实例方法; 对超类,私有和实例初始化方法调用的特殊处理
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
//行号的表,line后面的数字代表代码的行号,代表上面字节码中的0,就是aload_0
LineNumberTable:
line 3: 0
//本地变量表,非static方法,0位this,static方法,就是第一个变量
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/it/test1/Test; public static java.lang.Integer f1();
descriptor: ()Ljava/lang/Integer;
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=0
//将常量1push到操作数栈中
0: iconst_1
//将操作数栈中栈顶元素存储到本地变量表的索引0中
//这两步对应着int a = 1;
1: istore_0
2: iconst_2
//这两步对应着int b = 2;
3: istore_1
//将int类型的本地变量0的数据压入操作数栈
4: iload_0
5: iload_1
//int类型相加
6: iadd
//调用了第二行,是一个方法引用,执行完毕,清空操作数栈,此时本地变量表数据还在
7: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
//返回引用,会把本地变量表清空
10: areturn
LineNumberTable:
line 6: 0
line 7: 2
line 8: 4
LocalVariableTable:
Start Length Slot Name Signature
2 9 0 a I
4 7 1 b I public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=3, args_size=1
0: bipush 100
2: istore_1
//invokestatic执行静态方法,invokevirtual执行实例方法
3: invokestatic #3 // Method f1:()Ljava/lang/Integer;
6: invokevirtual #4 // Method java/lang/Integer.intValue:()I
9: istore_2
10: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
13: iload_1
14: iload_2
15: iadd
16: invokevirtual #6 // Method java/io/PrintStream.println:(I)V
19: return
LineNumberTable:
line 12: 0
line 13: 3
line 14: 10
line 15: 19
LocalVariableTable://数组类型参数,作为本地变量表索引0位置的数据
Start Length Slot Name Signature
0 20 0 args [Ljava/lang/String;
3 17 1 m I
10 10 2 c I
}
SourceFile: "Test.java"
上面对基本的字节码都有解释了,这里以f1()为例,通过图例更加详细的解释
字节码相关内容,可以参考官方文档:https://docs.oracle.com/javase/specs/jvms/se8/html/
1、flags表示类访问和属性修饰符
字段描述符:
方法描述符:
i++和++i
代码:
public static void f1() {
int i = 0;
int j = i++;
} public static void f2() {
int i = 0;
int j = ++i;
} public static void main(String[] args) {
f1();
f2();
}
反编译:
f1():
0: iconst_0 //常量0push到操作数栈的栈顶
1: istore_0 //将操作数栈的栈顶数据存储到本地变量表的索引0的位置
2: iload_0
3: iinc 0, 1 //将本地变量表的索引0的数据+1
6: istore_1 //将操作数栈的栈顶数据存储到本地变量表的索引1的位置
7: return f2():
0: iconst_0
1: istore_0
2: iinc 0, 1 //将本地变量表的索引0的数据+1
5: iload_0 //此时索引0的数据为1,load到操作数栈
6: istore_1 ////将操作数栈的栈顶数据存储到本地变量表的索引1的位置
7: return
从上面我们很容易看到二者的区别
PS:for循环中i++和++i没有效率差别,字节码完全一样的
try-Cache字节码:
代码:
public static String f1() {
String str = "hello1";
try{
return str;
}
finally{
str = "imooc";
}
} public static void main(String[] args) {
f1();
}
反编译:
0: ldc #2 //从运行时常量池中加载字符串hello,然后push到操作数栈
2: astore_0 //将操作数栈的栈顶数据存储到本地变量表的索引0的位置
3: aload_0 //
4: astore_1 //字符串hello,存在本地变量表的两个位置
5: ldc #3 // String imooc
7: astore_0 //将字符串imooc存储到本地变量表的索引0的位置,用imooc覆盖了hello
8: aload_1 //load本地变量表中索引1位置的数据
9: areturn //所以返回的是hello,而不是imooc //假如发生异常,就会走下面的代码
10: astore_2 //将异常存储到本地变量表的索引2的位置
11: ldc #3 // String imooc
13: astore_0
14: aload_2 //将索引2的位置的异常信息load出去
15: athrow //跑出异常
我们从字节码中看到无论是否发生异常,都会执行finally的内容,但是我们的return并不是finally的内容
我们再测试一下:
public static String f1() {
String str = "hello1";
try{
return str;
}
finally{
str = "imooc";
return "111";
}
} public static void main(String[] args) {
System.out.println(f1());
}
反编译:
0: ldc #2 // String hello1
2: astore_0
3: aload_0
4: astore_1
5: ldc #3 // String imooc
7: astore_0
8: ldc #4 // String 111 将字符串111push到操作数栈的栈顶
10: areturn 11: astore_2
12: ldc #3 // String imooc
14: astore_0
15: ldc #4 // String 111 将字符串111push到操作数栈的栈顶
17: areturn
所以无论是否发生异常,返回的都是字符串111,我们一般情况下不要在finally中使用return,很容易出现错误
String Constant Variable:
我们知道String的字符串拼接就是会new一个StringBuilder,然后append这个字符串,然后调用toString(),在for循环中效率很低。但是如果是final修饰的常量就不一定了。
代码:
public static void f1() {
final String x="hello";
final String y=x+"world";
String z=x+y;
System.out.println(z);
} public void f2(){
final String x="hello";
String y=x+"world";
String z=x+y;
System.out.println(z);
}
反编译:
f1():
0: ldc #2 // String hello //从常量池把hello字符串push到操作数栈
2: astore_0
3: ldc #3 // String helloworld //从常量池把helloworld字符串push到操作数栈
5: astore_1
6: ldc #4 // String hellohelloworld //从常量池把hellohelloworld字符串push到操作数栈
8: astore_2
9: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
12: aload_2
13: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
16: return f2():
0: ldc #2 // String hello
2: astore_1
3: ldc #3 // String helloworld
5: astore_2
6: new #7 // class java/lang/StringBuilder //new StringBuilder
9: dup //复制操作数栈的栈顶值
10: invokespecial #8 // Method java/lang/StringBuilder."<init>":()V //调用无参构造器初始化
13: ldc #2 // String hello
15: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
18: aload_2
19: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
22: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
25: astore_3
26: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
29: aload_3
30: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
33: return
我们可以看到对于final修饰String引用,在编译器就进行了优化,x+"world"直接优化成"helloworld"
java虚拟机(十四)--字节码指令的更多相关文章
- Java方法调用的字节码指令学习
Java1.8环境下,我们在编写程序时会进行各种方法调用,虚拟机在执行这些调用的时候会用到不同的字节码指令,共有如下五种: invokespecial:调用私有实例方法: invokestatic:调 ...
- JVM总结-虚拟机怎么执行字节码
1. JRE,JDK JRE : 包含运行 Java 程序的必需组件,Java 虚拟机+ Java 核心类库等. JDK : JRE + 一系列开发.诊断工具. 2. java字节码 编译器将 Ja ...
- [四] java虚拟机JVM编译器编译代码简介 字节码指令实例 代码到底编译成了什么形式
前言简介 前文已经对虚拟机进行过了简单的介绍,并且也对class文件结构,以及字节码指令进行了详尽的说明 想要了解JVM的运行机制,以及如何优化你的代码,你还需要了解一下,java编译器到底是 ...
- 深入理解java虚拟机(六)字节码指令简介
Java虚拟机指令是由(占用一个字节长度.代表某种特定操作含义的数字)操作码Opcode,以及跟随在其后的零至多个代表此操作所需参数的称为操作数 Operands 构成的.由于Java虚拟机是面向操作 ...
- 深入了解java虚拟机(JVM) 第十章 字节码指令
一.字节码指令的含义 Java字节码指令由一个字节长度的,代表某种特定操作含义的数字(操作码)以及其后的零至多个代表此操作所需参数(操作数).此外字节码指令是面向操作数栈的,这里操作数栈在功能上对应实 ...
- Java虚拟机-字节码指令
目录 字节码指令 字节码与数据类型 加载和存储指令 运算指令 类型转换指令 对象创建与访问指令 操作数栈管理指令 控制转移指令 方法调用和返回指令 异常处理指令 同步指令 字节码指令 Java虚拟机的 ...
- 【JVM源码解析】模板解释器解释执行Java字节码指令(上)
本文由HeapDump性能社区首席讲师鸠摩(马智)授权整理发布 第17章-x86-64寄存器 不同的CPU都能够解释的机器语言的体系称为指令集架构(ISA,Instruction Set Archit ...
- 大话+图说:Java字节码指令——只为让你懂
前言 随着Java开发技术不断被推到新的高度,对于Java程序员来讲越来越需要具备对更深入的基础性技术的理解,比如Java字节码指令.不然,可能很难深入理解一些时下的新框架.新技术,盲目一味追新也会越 ...
- Java字节码指令
1. 简介 Java虚拟机的指令由一个字节长度的.代表着某种特定操作含义的数字(称为操作码)以及跟随其后的零至多个代表此操作所需参数(称为操作数)而构成. 由于Java虚拟机采用面向操作数栈而不是寄存 ...
随机推荐
- 廖雪峰Java16函数式编程-2Stream-6reduce
1. 聚合方法 Stream.reduce()是一个Stream的聚合方法:把一个Stream的所有元素聚合成一个结果 例如: Stream.of(1, 2, 3, 4, 5).count(); // ...
- C#获取当前运行的源代码的文件名和当前源代码的行数的方法
在C#中记录日志时,为了以后查找错误或者跟踪的方便,最好能记录下出错的源代码的文件名和出错的源代码的行数. 这2个方法如下: /// <summary> /// 取得当前源 ...
- 【题解】洛谷 P1061 Jam的计数法
#include <iostream> #include <cstring> #include <cstdio> using namespace std; int ...
- idae for mac部分背景色修改收集
文章目录 所有字体默认颜色 终端背景色 行数line number背景色 line number颜色 编码区背景色 光标所在行背景色 未被使用的变量.方法或者类 控制台相关 选中文字的背景色 选中和未 ...
- import socketserver 模块 (27-03)
使用socketserver实现并发聊天 服务端可以比喻做一部电话. ("127.0.0.1", 8000) 比喻做服务端的一个号码. 1,server.py import soc ...
- vue通过修改element-ui相关类的样式修改element-ui组件的样式
可以在App.vue中的style中修改element-ui的样式. .el-menu{ width:160px !important; } 注意:一定要在属性值后面加上 !important 使自己 ...
- Java学习之创建对象内存使用机制
Java内存空间分两种,一种是栈内存,有多个,一种是堆内存,只有一个,在堆内存中又有一块方法区. 方法区中存储的是:类的信息(类名,类的直接父类,类的访问修饰符),类变量,类方法代码,实例方法代码,常 ...
- pip的使用方法简介
pip是Python包管理工具,它提供了对Python包的查找.下载.安装.卸载的功能 目前如果你在 python.org 下载最新版本的安装包,则是已经自带了该工具. 以下是pip常用命令 显示版本 ...
- WPF drag过程中显示ToolTip.
原文:WPF drag过程中显示ToolTip. 在drag/drop过程中,我们在判断出over的元素上是否可以接受drag的东西之后,通常是通过鼠标的样式简单告诉用户这个元素不接受现在drag的内 ...
- c语言排序代码实现
关于快速,冒泡,选择,插入等排序,本人用代码实现,均能运行成功. 本文除了排序,针对几种swap函数,也进行了说明,通过汇编代码分析,swap1函数的效率最高. #include<iostrea ...