大话+图说:Java字节码指令——只为让你懂
前言
随着Java开发技术不断被推到新的高度,对于Java程序员来讲越来越需要具备对更深入的基础性技术的理解,比如Java字节码指令。不然,可能很难深入理解一些时下的新框架、新技术,盲目一味追新也会越来越感乏力。
本文既不求照本宣科,亦不求炫技或著文立说,仅力图以最简明、最形象生动的方式,结合例子与实战,让小白也能搞懂这门看似复杂的技术概念。
单刀直入
闲言碎语不要讲,先表一表,什么是Java字节码指令?简而言之,Java字节码指令就是Java虚拟机能够听得懂、可执行的指令,可以说是Jvm层面的汇编语言,或者说是Java代码的最小执行单元。
有点Java基础的人一定都知道,javac命令会将Java源文件编译成字节码文件,即.class文件,其中就包含了大量的字节码指令。因此可以将javac命令理解为一个翻译命令,将源文件翻译成Jvm可以执行的指令。
那么最直观的探究方法莫过于直接对比翻译前后的内容。
具体如何对比呢?就不得不用到Java为我们一直默默提供的一项利器,javap命令,它可以解析字节码,将字节码内部逻辑以可读的方式呈现出来。为了紧贴实战,我们直接在新建的Java工程里,写这样一个UserServiceImpl类,里面包含几个由简单到复杂的方法,以及一个名为serviceType的属性:
如图,以上方法,复杂度由低到高依次为:getServiceType<setServiceType<genToken<login(以及一个实例代码块),后面我也会按照这个顺序解读其字节码指令的执行逻辑。
下面我们编译工程,然后在下图所示的目录(gradle编译工程)找到该类的字节码文件:
cd到这个路径下,运行javap命令:
javap -v -p UserServiceImpl
就可以观看到翻译版的Java字节码的胴体了!这里的-v意思是啰嗦模式,会输出全面的字节码信息,而-p是指涵盖所有成员。原字节码信息输出内容较多,基于本文的目标,取其一方法的内容,整理如下图:
方法1,getServiceType():
这个getServiceType的方法应该是再简单不过的Java代码,翻译成字节码后也变成了三行,我们先来简单推理一下:第一句,aload_0不知所云,索性略过;第二行,getfield应该可以读懂,后面这个#8似乎是他的参数(实际上是对常量池的引用),//后面注释的内容是javap给我们加上的,意思应该是#2的指向是"Field serviceType:Ljava/lang/String;"这个内容。
所以getfield这一行就是取出serviceType这个字段喽,so easy。areturn肯定就是return的意思,a的含义也先略过不表。总之就是取出serviceType字段然后return喽。
那么现在的问题就是aload_0是什么意思了,看似多余,但仔细思考一下,似乎之前给getfield指令传入了“Field serviceType:Ljava/lang/String;”这样一个并不完整的参数,其后半部分的“Ljava/lang/String;”仅仅表示这个serviceType字段的类型是String,也就是说,整个参数里没有说是取的谁的serviceType字段啊!究竟是get谁的feild呢?
由此可以想到:aload操作一定是在为getfield指令准备了一个主体。
实际上,再结合下面的局部变量表,aload_0中的0正是局部变量表里的Slot 0的含义。意思是将局部变量表里的Slot 0的东西压入操作数栈,这个Slot 0里的东西name正是this,也就是UserServiceImpl的实例,即getfield的主体。
大戏上演
好了,对于小白同学有些陌生的概念来了,啥是操作数栈?啥是局部变量表?
其实这两个东西理解好了,关于虚拟机指令就懂了一大半了。
那么,不妨删繁就简,由易入难,先讲一个这样的故事,故事起名叫:
Java方法之创世纪
话说Jvm大帝是神之旨意的履行者(Jvm大帝就是虚拟机,神就是开发者,神之旨意是开发者写好并编译后的字节码...),当Jvm大帝带领Java世界运行进入了一个新的方法后,会为这个方法在栈内存大陆上创造两个重要的领域:局部变量表和操作数栈。
要有栈。要有表。神说。
依照神之旨意,jvm大帝创造的局部变量表里一般会包含this指针(针对实例方法,静态方法当然无此)、方法的所有传入参数和方法中所开辟的本地变量。
那么操作数栈是干嘛用的呢?
我们再引入另外一个比喻,如果把运行Java方法理解为拍戏,那么局部变量表里的各个局部变量就是这部戏的核心主角,或者说领衔主演,而操作数栈正是这部戏的舞台。所谓操作数栈搭台,局部变量唱戏,是也。那么aload_0就是告诉Jvm导演(大帝已沦落为导演),请0号演员this同志登台(压栈),演后边的本子。
当然了,这个比喻并不完全恰当,因为操作数栈并不是“舞台”的结构,而是栈的结构。但是这个比喻可以很好地说明局部变量表和操作数栈之间的关系,以及aload_0的作用。
下面我们用一张图来演示一下getServiceType这个小剧本桥段所导演的故事:
好吧这部剧虽然短的可怜,但已经基本把指令、操作数栈和局部变量表三者的关系演绎了出来。
值得注意的是,getfield这条指令对操作数栈进行了复合操作,其流程可以示意如下图:
后面我们将要接触到的许多指令都如此,指令内部执行了弹出—>处理—>压回的流程。
下面我们就来分析一个相对复杂一点的方法,setServiceType(String),如下图:
这里我们看到,变化主要有,指令多了一行,多进行了一次aload,getfield变成了putfield,areturn变成了return,仅此而已。另外领衔主演也就是局部变量表里多了一位,也就是方法的传入参数serviceType字符串对象了。其情节如下:
这里,putfield只弹出栈内的操作数,而没有向操作数栈压回任何数据,而且执行putfield之前,栈内元素的位置也必须符合“值在上,主体在下”要求。
而最后的return仅表示方法结束,而不会像areturn一样返回栈顶元素。这也印证了setServiceType(String)方法没有返回参数。
融会贯通
相信有了以上的讲解,大家对指令、操作数栈、局部变量表三者的运作关系有了一定认识,为了后边能够分析更复杂的方法,这里必须概括性地讲解一下更多的Java字节码指令。虽然Java字节码指令非常多,但其实常用的不外乎几个类别,先从这几个常用类别入手理解,便可渐入佳境。
关于字节码指令的分类,可以从两个维度进行:一是指令的功能,二是指令操作的数据类型。我们先从功能说起,指令主要可以分为如下几类:
- 存储和加载类指令:主要包括load系列指令、store系列指令和ldc、push系列指令,主要用于在局部变量表、操作数栈和常量池三者之间进行数据调度;(关于常量池前面没有特别讲解,这个也很简单,顾名思义,就是这个池子里放着各种常量,好比片场的道具库)
- 对象操作指令(创建与读写访问):比如我们刚刚的putfield和getfield就属于读写访问的指令,此外还有putstatic/getstatic,还有new系列指令,以及instanceof等指令。
- 操作数栈管理指令:如pop和dup,他们只对操作数栈进行操作。
- 类型转换指令和运算指令:如add/div/l2i等系列指令,实际上这类指令一般也只对操作数栈进行操作。
- 控制跳转指令:这类里包含常用的if系列指令以及goto类指令。
- 方法调用和返回指令:主要包括invoke系列指令和return系列指令。这类指令也意味这一个方法空间的开辟和结束,即invoke会唤醒一个新的java方法小宇宙(新的栈和局部变量表),而return则意味着这个宇宙的结束回收。
如下图,展示了各类指令的作用:
再从另外一个维度,即指令操作的数据类型来讲:指令开头或尾部的一些字母,就往往表明了它所能操作的数据类型:
a对应对象,表示指令操作对象性数据,比如aload和astore、areturn等等。
i对应整形。也就有iload,istore等i系列指令。
f对应浮点型。
l对应long,b对应byte,d对应double,c对应char。
另外地,ia对应int array,aa对应object array,da对应double array。不在一一赘述。
了解了以上内容,我们再去看最后几个方法,应该就会容易理解很多了。
下面我们就直捣黄龙genToken这个方法(图中的颜色暗示了指令和方法调用之间的关系):
这个过程简单解读如下:
1.new一个StringBuilder对象(在堆内存中开辟空间),并将其引用入栈,用于实现加号连接字符串功能(相当于C++中的运算符重载);
2.dup复制栈顶的刚刚放入的引用,再次压栈,这时栈里有两个重复的内容,深度为2;
3.调用并弹出栈顶StringBuilder引用对象的<init>方法,栈深度为1;
4.(绿色部分)调用UUID.randomUUID()静态方法,结果压栈后弹出调用String的toString方法,再压栈,栈深度为2;
5.(黄色部分)将"-"和""字符压栈,此时栈深度为4,弹出(栈顶3个元素)调用replace方法,结果压栈,深度为2;
6.调用StringBuilder对象的append方法,结果压栈,深度为1;
7.(蓝色部分)将参数user压栈并调用hashCode方法,结果压栈,深度为2;
8.调用StringBuilder对象的append方法(此处和上面的append调用共同完成了加号功能,在图中为红色部分),结果压栈,深度为1,再调用toString方法后结果压栈,深度为1;
9.areturn返回栈顶对象。
再看这个包含if跳转的方法login:
如上图,图中已经说明的比较全面了,不再赘述。值得一提的是,Java的这种基于栈结构的指令,在设计上有一种非常简洁的美感,指令与指令之间并没有较重的依赖,每条指令仅仅与操作数栈等领域内的数据发生关系,充满着某种平衡与秩序感。因此也必须注意,几乎每条指令的运行都有其前提,比如在invokevirtual或invokespecial指令执行前,必须保证操作数栈内提前按顺序压入好所需的操作数,否则就会发生问题。
关于最复杂的onCreate方法,就不再啰嗦解读了,读者可以前往我的github上的对应demo repo,进入tutorial分支,拉取源码和教程资源,或者自己写demo体验这一完整过程。
地址:https://github.com/BryanSharp...
后话
关于实战,一是可以学习使用强大开源工具ASM.jar;二是,可以参考本人的另一篇文章:Java字节码修改神器HiBeaver:黑掉你的SDK以及一次Android字节码插桩实战,利用hibeaver这个助手,开发者可以非常灵活地对字节码进行修改,插入指令,hook代码,甚至建立一些简单的AOP框架,对于Java字节码学习大有裨益。
hibeaver完全开源,github项目地址:https://github.com/BryanSharp...
祝玩的愉快!
本文如有不妥之处,欢迎交流指正。
另外,本文为了尽可能地简明生动、直入核心,简化了很多概念和细节,读者须知实际情况的更为复杂。但相信在理解了本文以后,就可以抓住Java字节码指令的核心理念,也就算扣开虚拟机学习的大门并可以开始读书精进了。下面盗图一张(后有出处),可作拓展:
链接:http://blog.csdn.net/luanloui...
from; https://segmentfault.com/a/1190000008606277
大话+图说:Java字节码指令——只为让你懂的更多相关文章
- 硬核万字长文,深入理解 Java 字节码指令(建议收藏)
Java 字节码指令是 JVM 体系中非常难啃的一块硬骨头,我估计有些读者会有这样的疑惑,"Java 字节码难学吗?我能不能学会啊?" 讲良心话,不是我谦虚,一开始学 Java 字 ...
- 【JVM源码解析】模板解释器解释执行Java字节码指令(上)
本文由HeapDump性能社区首席讲师鸠摩(马智)授权整理发布 第17章-x86-64寄存器 不同的CPU都能够解释的机器语言的体系称为指令集架构(ISA,Instruction Set Archit ...
- Java字节码指令收集大全
Java字节码指令大全 常量入栈指令 指令码 操作码(助记符) 操作数 描述(栈指操作数栈) 0x01 aconst_null null值入栈. 0x02 iconst_m1 -1(int)值入栈. ...
- Java字节码指令
1. 简介 Java虚拟机的指令由一个字节长度的.代表着某种特定操作含义的数字(称为操作码)以及跟随其后的零至多个代表此操作所需参数(称为操作数)而构成. 由于Java虚拟机采用面向操作数栈而不是寄存 ...
- 在Myeclipse下查看Java字节码指令信息
在实际项目开发中,有时为了了解Java编译器内部的一些工作,需要查看Java文件对应的具体的字节码指令集,这里提供两种方式供参考. 一.使用javap命令 javap是JDK提供的 ...
- 深入了解java虚拟机(JVM) 第十章 字节码指令
一.字节码指令的含义 Java字节码指令由一个字节长度的,代表某种特定操作含义的数字(操作码)以及其后的零至多个代表此操作所需参数(操作数).此外字节码指令是面向操作数栈的,这里操作数栈在功能上对应实 ...
- Java字节码里的invoke操作&&编译时的静态绑定与动态绑定
一个一直运行正常的应用突然无法运行了.在类库被更新之后,返回下面的错误. Exception in thread "main" java.lang.NoSuchMethodErro ...
- JVM 内部原理(六)— Java 字节码基础之一
JVM 内部原理(六)- Java 字节码基础之一 介绍 版本:Java SE 7 为什么需要了解 Java 字节码? 无论你是一名 Java 开发者.架构师.CxO 还是智能手机的普通用户,Java ...
- 轻松看懂Java字节码
java字节码 计算机只认识0和1.这意味着任何语言编写的程序最终都需要经过编译器编译成机器码才能被计算机执行.所以,我们所编写的程序在不同的平台上运行前都要经过重新编译才能被执行. 而Java刚诞生 ...
随机推荐
- 【LOJ】#6437. 「PKUSC2018」PKUSC
题解 我们把这个多边形三角形剖分了,和统计多边形面积一样 每个三角形有个点是原点,把原点所对应的角度算出来,记为theta 对于一个点,相当于半径为这个点到原点的一个圆,圆弧上的弧度为theta的一部 ...
- thinkphp中table方法
table方法也属于模型类的连贯操作方法之一,主要用于指定操作的数据表. 用法 一般情况下,操作模型的时候系统能够自动识别当前对应的数据表,所以,使用table方法的情况通常是为了:切换操作的数据表: ...
- plsql oracle 使用教程
课程 一 PL/SQL 基本查询与排序 本课重点: 1.写SELECT语句进行数据库查询 2.进行数学运算 3.处理空值 4.使用别名ALIASES 5.连接列 6.在SQL PLUS中编辑缓冲,修改 ...
- MySQL数据库之索引
1 引言 在没有索引的情况下,如果要寻找特定行,数据库可能要遍历整个数据库,使用索引后,数据库可以根据索引找出这一行,极大提高查询效率.本文是对MySQL数据库中索引使用的总结. 2 索引简介 索引是 ...
- MySQL 5.7基于GTID复制的常见问题和修复步骤(一)
[问题一] 复制slave报错1236,是较为常见的一种报错 Got fatal error 1236 from master when reading data from binary log: ' ...
- js控制手机端字体大小rem
//得到手机屏幕的宽度 let htmlWidth = document.documentElement.clientWidth || document.body.clientWidth; if(ht ...
- python scrapy 调试模式
scrapy通过命令行创建工程,通过命令行启动爬虫,那么有没有方式可以在IDE中调试我们的爬虫呢? 实际上,scrapy是提供给我们工具的, 1. 首先在工程目录下新建一个脚本文件,作为我们执行爬虫的 ...
- 快速排序之Java实现
快速排序之Java实现 代码: package cn.com.zfc.lesson21.sort; /** * * @title QuickSort * @describe 快速排序 * @autho ...
- 【BZOJ-3218】a+b Problem 最小割 + 可持久化线段树
3218: a + b Problem Time Limit: 20 Sec Memory Limit: 40 MBSubmit: 1320 Solved: 498[Submit][Status] ...
- j.u.c系列(02)---线程池ThreadPoolExecutor---tomcat实现策略
写在前面 本文是以同tomcat 7.0.57. jdk版本1.7.0_80为例. 线程池在tomcat中的创建实现为: public abstract class AbstractEndpoint& ...