JVM虚拟机指令

HelloWorld

// getstatic: 获取静态字段.指令的操作码是0xB2,该指令需要一个操作数,该操作数是常量池中某个CONSTANT_Fieldref_info常量的索引。
// 该指令表示获取System的out静态字段,该静态字段的类型为java.io.PrintStream。该指令执行完成后,操作数栈顶存放的就是System的out静态字段的引用
 getstatic #2 <java/lang/System.out>
// ldc 指令的操作码是0x12,该指令也需要一个操作数,值为常量池中的某个CONSTANT_String_info常量的索引。
// 将常量池中的“Hello Word”字符串的引用放入操作数栈顶。该指令执行完后,操作数栈顶存放的就是字符串“Hello Word”的引用
3 ldc #3 <Hello Word>
// invokevirtual指令的操作码是0xB6,该指令也需要一个操作数,值为常量池中某个CONSTANT_Methodref_info常量的索引。
// 调用PrintStream对象的println方法。
// invokevirtual指令要求将调用目标方法所需要的参数压入栈顶,除静态方法、类初始化方法<clinit>之外,每个类的成员方法以及类的实例初始化方法<init>的第一个参数都是this引用,在java代码中不需要传递,由编译器编译后生成。
// 在本例中invokevirtual指令执行之前,操作数栈必须存在一个System.out对象的引用,和println方法所需的参数,并且顺序是严格要求的,正是前面getstatic、ldc两条指令执行的结果
5 invokevirtual #4 <java/io/PrintStream.println>
8 return

读写局部变量表与操作数栈

读写局部变量表与操作数栈就是: 将局部变量push进操作数栈将操作数栈的栈顶元素存储到局部变量表的操作。

将局部变量表中的元素放入操作数栈只能放入栈顶,而将操作数栈的栈顶元素存到局部变量表是可以指定存到局部变量表的位置的,这个过程其实就是给局部变量赋值。

与汇编语言有相似之处就是字节码指令不能直接将局部变量表的某个元素赋值给局部变量表的另一个元素,必须通过操作数栈完成。这也是为什么说字节码指令集是基于栈的指令集。

局部变量表的大小与操作数栈的深度是在Java代码编译成class字节码文件时就已经确定

使用javap -v命令可以查看当前class文件中每个方法的操作数栈深度与局部变量表大小
stack表示当前方法的操作数栈最大深度,locals表示当前方法的局部变量表最大槽数.

// (1) int a = 10, b = 20;
// 偏移量为0的指令为bipush指令,该指令是将一个立即数10放入操作数栈顶.(操作数10占据偏移量1)
 0 bipush 10
// 偏移量为2的指令是istore_1,该指令是将当前操作数栈顶的元素存储到局部变量表索引为1的Slot(第二个Slot)。
// 该指令执行完成后,局部变量表索引为1的Slot存储整数10,操作数栈顶的元素已经出栈,此时操作数栈为空 
// Java虚拟机执行字节码指令并不关心局部变量表索引为1的元素在源码中叫什么名字。那我们怎么知道这个位置是局部变量a、b、c的哪个呢?这就需要通过查看LocalVariableTable属性了
 2 istore_1
//  偏移量为3的字节码指令为bipush指令,该指令的作用是将立即数20放入操作数栈顶。该指令执行完成后,局部变量a的值还是10,操作数栈顶存储立即数20
 3 bipush 20
// 偏移量为5的字节码指令为istore_2,该指令不需要操作数,作用是将当前操作数栈的栈顶元素存储到局部变量表索引为2的Slot。该指令执行完成后,a=10,b=20,操作数栈顶的元素出栈,操作数栈为空
 5 istore_2
 6 iload_2
 7 istore_3
 8 iload_1
 9 istore_2
10 return

LocalVariableTable:
Start Length Slot Name Signature
0 11 0 args [Ljava/lang/String;
3 8 1 a I
6 5 2 b I
8 3 3 c I
第一行:局部变量的作用范围为[0,11)[1],使用局部变量表中的第一个Slot存储,该局部变量的名称为“args”,变量的类型签名为“[Ljava/lang/String”;

第二行:局部变量的作用范围为[3,11),使用局部变量表中的第二个Slot存储,该局部变量的名称为“a”,类型签名为“I”;

第三行:局部变量的作用范围为[6,11),使用局部变量表中的第三个Slot存储,该局部变量的名称为“b”,类型签名为“I”;

第四行:局部变量的作用范围为[8,11),使用局部变量表中的第四个Slot存储,该局部变量的名称为“c”,类型签名为“I”。其实不管访问局部变量表的哪个位置,都可以通过iload和istore指令访问,那为什么还要iload_xx和istore_xx指令呢。因为iload和istore指令需要操作数,而iload_xx和istore_xx不需要操作数,在编译后能减少Code属性的code[]字节数组的大小,而且大多数方法都不会超过3个参数。因为非静态方法的局部变量表的下标0用于保存this引用,所以是4减1个参数。

例子中的iload_xx指令和istore_xx指令只能操作Java中int类型的变量,与之对应的还有操作float类型的fload_xx和fstore_xx指令,操作long类型的lload_xx和lstore_xx指令,操作double类型的dload_xx和dstore_xx指令,以及操作引用类型的aload_xx和astore_xx指令,还有fload、lload、dload、aload指令

ipush用于将一个int型的立即数放入操作数栈的栈顶,该指令属于操作常量与立即数入栈一类的指令。除bipush之外还有将null放入操作数栈栈顶的iconst_null指令、将常量池中的常量值放入操作数栈顶的指令ldc。

创建对象指令

// 1.UserService service = new UserService();

 0 new #2 <com/zc/my/bytecode/UserService>
 // dup指令用来`复制栈顶的元素并压入栈顶`,此处用于 复制一份 UserService 放在栈顶
 3 dup
 // 调用 UserService的<init> 方法,该方法需要隐式传入this,则会使用到上一步中复制的 UserService
 4 invokespecial #3 <com/zc/my/bytecode/UserService.<init>>
 // 将当前栈顶的 引用类型的数据 存储到 局部变量表 索引为1的Slot
 7 astore_1

// 2.User user = service.getUser()
// aload_1,该指令将局部变量表索引为1的元素放到操作数栈顶,即将第一部分字节码创建出来的UserService对象的引用放入操作数栈顶
 8 aload_1
// invokevirtual,调用对象的实例方法,需要一个操作数。调用该方法只需要一个隐式参数,因此需要将一个UserService对象的引用放入操作数栈顶。
// UserService的getUser方法有一个返回值,返回值的类型为引用类型,即返回一个User实例的引用.此时操作栈中 保存着 User对象的引用
 9 invokevirtual #4 <com/zc/my/bytecode/UserService.getUser>
// 将当前操作数栈顶的元素User对象的引用,存储到局部变量表索引为2的Slot.执行完后操作栈为空.
12 astore_2

// 3.String name = user.getName()
// 将局部变量表索引为2的Slot存储的User对象的引用,推送至操作数栈的栈顶
13 aload_2
// invokevirtual,操作数#5执向常量池中索引为5的CONSTANT_Methodref_info常量,表示User类的getName方法.
// User的getName方法返回值类型为String,该指令执行完成后,操作数栈顶的元素是getName方法返回的String对象的引用
14 invokevirtual #5 <com/zc/my/bytecode/User.getName>
// 将上一步调用User对象getName方法的返回值String类型的引用存储到局部变量表索引为3的Slot,也就是给局部变量name赋值
17 astore_3

18 getstatic #6 <java/lang/System.out>
21 aload_3
22 invokevirtual #7 <java/io/PrintStream.println>
25 return

偏移量为4的字节码指令是invokespecial,这是一条方法调用指令,操作码为0xB7,该指令需要一个操作数,操作数的值是常量池中某个CONSTANT_Methodref_info常量的索引,#3常量表示UserService的方法。

偏移量为7的指令是 astore_1,该指令是将当前栈顶的 引用类型的数据 存储到 局部变量表 索引为1的Slot。如果查看LocalVariableTable,局部变量表下标1的位置存储的是局部变量service。

dup指令用于复制当前栈顶元素,invokespecial和invokevirtual指令用于方法的调用

new对象的指令步骤

new一个对象需要四条字节码指令,
1.先创建对象存放在栈顶,
2.然后将栈顶存放的对象复制一份,用于调用类的实例初始化方法,
3.调用初始化方法
4.最后还是将new指令创建出来的那份赋值给局部变量或者字段。

这也就很好理解并发编程中,为什么单例要使用双重检测,因为new一个对象在字节码层面看并不是一个原子操作。

本地变量表槽位1存储了上面创建出来的service对象.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kmkBccsu-1619534564824)(/assets/2021-04-27-21-31-41.png)]

读写this的字段

// (1) this.userDao
// aload_0将局部变量表索引为0的Slot存储的值放入操作数栈顶,对于非静态方法,局部变量表索引为0的Slot存储的变量就是this引用
0 aload_0
// 使用getfield指令获取this的userDao字段,getfield指令要求一个操作数,操作数的值为常量池中某个CONSTANT_Fieldref_info常量的索引,
// 本例中索引为4的常量表示userDao字段,字段的类型描述符为“Lxxx/UserDao;”。该指令执行完成后返回this.userDao,存储在操作数栈顶。
1 getfield #4 <com/zc/my/bytecode/C0302.userDao>

4 aload_1
5 invokevirtual #5 <com/zc/my/bytecode/UserDao.getUserByName>
8 areturn

赋值

// 偏移量为0的aload_0指令是将this引用放入操作数栈栈顶
 0 aload_0
 // 偏移量为1、4、5三条指令是创建一个UserDao对象,并放入操作栈栈顶
 1 new #2 <com/zc/my/bytecode/UserDao>
 4 dup
 5 invokespecial #3 <com/zc/my/bytecode/UserDao.<init>>
 // putfield指令与getfield指令需要的操作数都是一样,
 // 偏移量为8的putfield指令是将当前栈顶元素赋值给this.userDao字段
 8 putfield #4 <com/zc/my/bytecode/C0302.userDao>
11 return

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

FlyingZCC

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值