Java ASM学习(2)
1.编译后的方法区,其中存储的代码都是一些字节码指令
2.Java虚拟机执行模型:
java代码是在一个线程内部执行,每个线程都有自己的执行栈,栈由帧组成,每个帧表示一个方法的调用,每调用一个方法,都将将新的帧压入执行栈,方法返回时(不管是整成return还是异常返回),该方法对应的帧都将出栈,即按照先进后出的规则。当每个frame创建的时候,其操作栈是空的,符号表由this对象和函数参数组成。
执行栈与操作数栈不一样,操作数栈包含在执行栈中。每一帧包括局部变量和操作数栈两部分,操作数栈中包括字节码指令用来当操作数的值。比如a.equals(b)将创建一帧,此时该帧将有一个空栈,并且a和b作为局部变量
字节码指令:
由标识该指令的操作码和固定数目的参数组成,操作码指定要进行哪一类操作,参数指定具体精确行为。指令分为两类,一类在局部变量和操作数栈之间传值,一类从操作数栈弹出值计算后再压入
例如:
ILOAD,LLOAD,FLOAD,DLOAD,ALOAD读取一个局部变量,并将其值压入操作数栈中,其对应的参数是其读取的局部变量索引i(因为局部变量就是通过索引来进行随机访问的),LLOAD和DLOAD加载时需要两个槽(slot),因为局部变量部分和操作数占部分的每个槽(slot)都可以保存除了long和double之外的java值(long和double需要两个槽)。
ILOAD:加载boolean、char、byte、short、int局部变量
LLOAD:加载long
FLOAD:加载float
DLOAD:加载double
ALOAD:加载对象和数组引用
对应的ISTORE,LSTORE,FSTORE,DSTORE,ASTORE从操作数栈弹出值并将其存储在指定的索引i所代表的局部变量中,所以这些操作指令是和java数据类型密切相关的。存取值和数据类型也相关,比如使用ISTORE 1 ALOAD 1,此时从操作数栈弹出一个int值存入索引1处的局部变量中,再将该值转为对象类型进行转换读取是非法的。但是对于一个局部变量位置,我们可以在运行过程中改变其类型,比如ISTORE 1 ALOAD 1非法,但是ATORE 1 ALOAD1就合法了。具体的字节码指令见ASM指南附A.1
通过一个例子来进行学习,比如以下方法:
package asm; public class bean {
private int f; public bean() {
} public void setF(int f) {
this.f = f;
} public int getF() {
return this.f;
}
}
直接通过字节码文件查看其class文件结构,其字段就一个int类型的f,访问修饰符为private
setf方法的字节码指令如下
其局部变量表如下,所以有两个值一个就是当前对象this和成员变量f,分别对应下标0和1
这里要设计到几个字节码指令:
GETFIELD owner name desc:读取一个字段的值并将其值压入操作数栈中
PUTFIELD owner name desc:从操作数弹出值存在name所代表的字段中
owner:类的全限定名
GETSTATIC owner name desc和PUTSTATIC owner name desc类似,只是为静态变量
aload 0,读取局部变量this,也就是局部变量表下标为0处的this对象(其在调用这个方法的时候就已经初始化存储在局部变量表中),然后将其压入操作数栈。
iload 1,读取局部变量f,下标为1(创建帧期间已经初始化,也就是入口参数int f),压入操作数栈中
putfield #2 <asm/bean.f> 也就是弹出压入的两个值,赋值给asm/bean.f,也就是将入口的int f的值赋给this.f
return 即该方法执行完成,那么该帧从执行栈从弹出
getf对应的字节码指令如下所示:
aload 0,即从局部变量表拿到this放入操作数栈
getfield #2 <asm/bean.f> 即从操作数栈中拿出this,并将this.f的值压入操作数栈
ireturn 返回f的值get方法的调用者,xreturn,x即返回变量对应的修饰符
bean构造方法,字节码指令如下:
aload 0: 从局部变量表拿到this,压入操作数栈
这里要设计方法的调用相关的字节码指令:
INVOKEVIRTUAL owner name desc:
调用owner所表示的类的name方法
desc用来描述一个方法的参数类型和返回类型
INVOKESTATIC:调用静态方法
INVOKESPECIAL: 调用私有方法和构造器
INVOKEINTERFACE: 接口中定义的方法
invokespecial #1 <java/lang/Object.<init>>: 调用object对象的init方法,即super()调用,最后return返回,如果是对于以下代码:
package asm; public class bean {
private int f; public void setFf(int f) {
if(f>0){
this.f = f;}
else {
throw new IllegalArgumentException();
}
} public int getF() {
return f;
} }
此时setf的字节码指令如下:
iload 1,从局部表量表中拿出入口参数 int f,压入操作数栈
ifile 9:此时弹出操作数栈中的int f和0进行比较
a.如果小于等于0(这里将大于判断转为小于等于的判断),则到第12条指令
new #2 :新建一个异常对象并压入操作数栈
dup:重复压入该值一次
invokespecial #4 : 弹出操作栈中两个对象值其中之一,并调用其构造函数实例化该对象
athrow:弹出操作数栈中剩下的值(另一个异常对象),并将其作为异常抛出
b.如果大于0,则依次执行
aload0 从局部变量表拿出this对象放入操作数栈中
iload1 拿出入口int f的值压入栈中
putfiled #2 <asm/bean.f>:将int f的值赋给this.f
goto 20: 到第20条字节码指令
return : 返回
感觉和汇编有点像,不过比汇编更容易理解,主要还是方法内的一些操作,能看懂基本的字节码指令,复杂的再去查doc,听说面试有时候会问i++和++i的区别:
package asm; public class testplus { public void plusf(){
int i=;
System.out.println(i++);
}
public void pluse(){
int i= ;
System.out.println(++i);
}
}
编译后:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
// package asm; public class testplus {
public testplus() {
}
//i++
public void plusf() {
int i = 0;
byte var10001 = i;
int var2 = i + 1;
System.out.println(var10001);
}
//++i
public void pluse() {
int i = 0;
int i = i + 1;
System.out.println(i);
}
}
首先从生成的class来看,i++编译后竟然用字节存储了i的值,然后i自增1,输出的为字节类型i即0,所以i++,最终输出为0,++i,直接是i自增1,然后输出i,所以最终输出为1,所以for循环用i++,而不用++i
从字节码指令来看:
i++
iconst 0:首先操作数栈中压入常量0
istore 1:然后弹出常量0放入局部变量表索引1处,此时局部变量表处1处从i变为0,操作数栈空
getstatic #2 :即拿到java.lang.System.out,即取静态变量System.out压入栈中,此时栈中1元素
#2在常量池中为第二个,关于该字段的引用说明如下,out对应的描述符即为Ljava/io/PrintStream; 那么类类型的描述符就是L+类的全限定名+;
iload 1:从局部变量表1处取值,压住操作数栈,即将0压入操作数栈
iinc 1 by 1:给局部变量1处的值+1,此时1处即从0变为1
invokevirtual:调用java.io.PrintStream.println,此时需要的值是从操作数栈中取的,然而此时操作数栈顶弹出的数值为0,所以输出为0
++i
iconst 0:首先操作数栈中压入常量0
istore 1:然后弹出常量0放入局部变量表索引1处,此时局部变量表处1处从i变为0,操作数栈空
getstatic #2 :即拿到java.lang.System.out,即取静态变量System.out压入栈中,此时栈中1元素
iinc 1 by 1:将局部变量表1处的值加1,即从0变为1
iload 1:加载局部变量表1处的值,压入操作数栈中,即将1压入栈中
invokevirtual:调用java.io.PrintStream.println,此时需要的值是从操作数栈中取的,然而此时操作数栈顶弹出的数值为1,所以输出为1
所以i++和++i的区别从字节码指令上来看就是局部变量表自增和压入操作数栈的顺序不一样,i++是先压栈,后局部变量表自增,++i是先局部变量表自增,后压入操作数栈,这样就完全搞懂了2333~
所以再分析一个巩固巩固:
package asm; public class testplus { public void pluse(){
int i=0 ;
int p = 2 + i++ - ++i;
System.out.println(i);
System.out.println(p);
} public static void main(String[] args) {
testplus t = new testplus();
t.pluse();
}
}
main方法:
new #4 <asm/testplus>:new一个对象压入栈中
dup:赋值一个栈顶的对象再压入操作数栈,关于为什么要压入两个重复的值原因:
首先字节码指令操作数值时基于栈实现的,那么对于同一个值从栈中操作时必定要弹出,那么如果对一个数同时操作两次,那么就要两次压栈。涉及到new一个对象操作时,java虚拟机自动dup,在new一个对象以后,栈中放入的是该对象在堆中的地址,比如声明以下两个
class1 a = new class1();
a.pp()
通常在调用对象调用其类中方法前肯定要调用其init实例化,那么init要用一次操作数栈中的地址,此时弹出一次地址参与方法调用,后面只需要再将该栈中的地址放入局部变量表,该地址的对象已经完成了实例化操作,那么后面每次调用只需要从局部变量表从取到该对象的地址,即可任意调用其类中的方法。
invokespecial #5 :这里调用testplus的init方法,所以从栈中弹出一个testplus的地址
astore 1:将实例化以后的该testplus对象地址放入局部变量表1处
aload 1:取局部变量表1处的对象地址压入栈中
invokevirtual #6:调用testplus的pluse方法
return :返回
pluse方法:
iconst 0:压入常量0
istore 1:弹出0存入局部变量表1处 (完成int i=0)
iconst 2:将2压入栈中
iload 1:取出局部变量表1处的值0压入栈中
iinc 1 by 1:局部变量表1处的值加1,即从0变为1
iadd :将栈中的两个值相加,即 stack[0] + stack[1] = 2 + 0 =2
iinc 1 by 1: 局部变量表1处的值加1,即从1变为2
iload 1:去局部变量表1处的值压入栈中,即栈顶为2
isub :将栈中两个元素相减,即stack[0] - stack[1] = 2 - 2 =0
istore 2:弹出栈中的唯一一个元素2,存入局部变量表2处,此时栈空
getstatic # 2 :拿到Syetem.out,压入栈中
iload 1:取出局部表量表1处的值压入栈中,即栈顶为2
invokevirtual #3 : 弹出栈中两个元素,调用System.out的println方法,即stack[0].print(stack[1]),即输出2
同理压入System.out,然后iload 2,取出局部变量表2处的0压入栈中,输出0
最终输出结果也是2和0
Java ASM学习(2)的更多相关文章
- Java的学习之路
记事本 EditPlus eclipse Java的学习软件,已经系统性学习Java有一段时间了,接下来我想讲一下我在Java学习用到的软件. 1.第一个软件:记事本 记事本是Java学习中最基础的编 ...
- Java多线程学习笔记
进程:正在执行中的程序,其实是应用程序在内存中运行的那片空间.(只负责空间分配) 线程:进程中的一个执行单元,负责进程汇总的程序的运行,一个进程当中至少要有一个线程. 多线程:一个进程中时可以有多个线 ...
- Java Web 学习路线
实际上,如果时间安排合理的话,大概需要六个月左右,有些基础好,自学能力强的朋友,甚至在四个月左右就开始找工作了.大三的时候,我萌生了放弃本专业的念头,断断续续学 Java Web 累计一年半左右,总算 ...
- Java基础学习-- 继承 的简单总结
代码参考:Java基础学习小记--多态 为什么要引入继承? 还是做一个媒体库,里面可以放CD,可以放DVD.如果把CD和DVD做成两个没有联系的类的话,那么在管理这个媒体库的时候,要单独做一个添加CD ...
- 20145213《Java程序设计学习笔记》第六周学习总结
20145213<Java程序设计学习笔记>第六周学习总结 说在前面的话 上篇博客中娄老师指出我因为数据结构基础薄弱,才导致对第九章内容浅尝遏止地认知.在这里我还要自我批评一下,其实我事后 ...
- [原创]java WEB学习笔记95:Hibernate 目录
本博客的目的:①总结自己的学习过程,相当于学习笔记 ②将自己的经验分享给大家,相互学习,互相交流,不可商用 内容难免出现问题,欢迎指正,交流,探讨,可以留言,也可以通过以下方式联系. 本人互联网技术爱 ...
- Java多线程学习(转载)
Java多线程学习(转载) 时间:2015-03-14 13:53:14 阅读:137413 评论:4 收藏:3 [点我收藏+] 转载 :http://blog ...
- java基础学习总结——java环境变量配置
前言 学习java的第一步就要搭建java的学习环境,首先是要安装JDK,JDK安装好之后,还需要在电脑上配置"JAVA_HOME”."path”."classpath& ...
- Java Web学习系列——Maven Web项目中集成使用Spring、MyBatis实现对MySQL的数据访问
本篇内容还是建立在上一篇Java Web学习系列——Maven Web项目中集成使用Spring基础之上,对之前的Maven Web项目进行升级改造,实现对MySQL的数据访问. 添加依赖Jar包 这 ...
随机推荐
- 事务框架之声明事务(自动开启,自动提交,自动回滚)Spring AOP 封装
利用Spring AOP 封装事务类,自己的在方法前begin 事务,完成后提交事务,有异常回滚事务 比起之前的编程式事务,AOP将事务的开启与提交写在了环绕通知里面,回滚写在异常通知里面,找到指定的 ...
- sql select sql查询
select 一.课上练习代码 1 查询所有学生信息 select * from tb_student; select * from tb_teacher; 2 查询所有课程名称及学分(投影和别名) ...
- 20175314 《Java程序设计》第十一周学习总结
20175314 <Java程序设计>第十一周学习总结 教材学习内容总结 URL类 URL类是java.net包中的一个类,用URL创建的对象可以获取URL中的资,其包括三部分信息:协议. ...
- RecyclerView的刷新分页
在开发中常常使用到刷新分页,这里实现一个 RecyclerView 的简单的刷新分页操作,测试效果见文末,实现过程参考如下: 实现思路 加载更多数据使用到 RecyclerView 加载多种布局,根据 ...
- 《Three.js 入门指南》2.3.1- 照相机 - 正交投影demo
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
- H5的新特性
https://blog.csdn.net/weixin_42441117/article/details/80705203 1.h5新语义元素(有利于代码可读性和SEO)2.本地存储 h5提供 ...
- Vulnhub bulldog靶机渗透
配置 VM运行kali,桥接模式设置virtualbox. vbox运行靶机,host-only网络. 信息搜集 nmap -sP 192.168.56.0/24 或者 arp-scan -l #主机 ...
- 详细解析 HBASE 配置的各种要点
文章更新于:2020-04-06 安装惯例,需要的文件附上链接放在文首. 文件名:hbase-2.2.4-bin.tar.gz 文件大小:213.24 MB 下载链接:http://download. ...
- docker win10 基本指令
一.镜像操作 docker images 本地镜像 docker pull imagename 获取网上获取镜像 docker run 创建docker容器 docker rmi imagename ...
- MODIS系列之NDVI(MOD13Q1)一:数据下载(二)基于FTP
这一篇我们来介绍下MODIS数据的下载方式.当然这边主要是介绍国外网站的下载方式,国内网站的普遍是在地理空间数据云和遥感集市下载.国外网站(NASA官网)下载方式主要介绍两种.本篇主要针对第一种方式, ...