百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

Java虚拟机编译(java虚拟机命令配置)

mhr18 2024-10-12 04:35 19 浏览 0 评论

Java虚拟机(JVM)是为Java语言设计的,Oracle JDK 中包含了能将Java代码转换为JVM指令的编译器及运行JVM的系统。深入了解编译器和JVM对想要编写编译器或理解JVM结构的人来说很有帮助。编译器一词也指代其他类型的翻译工具,如即时编译器(JIT)

常量、局部变量和控制结构的使用

void spin() {
    int i;
    for (i = 0; i < 100; i++) {
        ;    // Loop body is empty
    }
}
0 iconst_0 // 将整数常量0压入栈顶
1 istore_1 // 将栈顶整数存入本地变量1中(即 i=0)
2 goto 8 // 首次通过时不增加
5 iinc 1 1 // 本地变量1自增1(即 i++)
8 iload_1 // 将本地变量1(即 i)压入栈顶
9 bipush 100 // 将整型常量100压入栈顶
11 if_icmplt 5 // 比较栈顶两int值大小,并如果小于(i < 100),则跳转
14 return // 执行完毕后返回void

Java虚拟机是基于栈的架构,大部分操作都涉及从操作数栈中取出一个或多个操作数,或者把结果推回操作数栈中。每当调用一个方法时,都会创建一个新的帧,为该方法创建一个新的操作数栈和一组本地变量(局部变量)。在任何一次计算过程中,由于众多嵌套的方法调用,可能会存在许多帧和同等数量的操作数栈。每个线程控制下,只有当前帧的操作数栈是活动的。

Java虚拟机指令集能够识别不同数据类型的操作数,通过使用不同的字节码来操作这些操作数。比如方法spin只操作int类型的值。它所采用的指令(如iconst_0、istore_1、iinc、iload_1、if_icmplt等)专门用于操作int类型。

在spin方法中,两个常量0和100通过两种不同的指令被推入操作数栈。0是通过iconst_0指令推入的,这属于iconst_<i>系列。而100是通过bipush指令推入的,这个指令将其推送值作为立即数携带。

Java虚拟机通过将一些操作数值(如iconst_<i>指令中的int常量-1、0、1、2、3、4、5)隐含在操作码中,充分利用这些值的可能性。比如iconst_0指令隐含了它会推送一个int类型的0,不需要存储或解析任何额外的信息。虽然用bipush 0编译推送0的操作是正确的,但这会让spin的编译代码多一个字节,并且在虚拟机执行时引入额外的获取和解码操作数的时间开销。使用隐含操作数可以使编译后的代码更加简洁高效。

在spin中,int变量i被存储在本地变量1里。由于Java虚拟机的大部分指令操作的是从操作数栈弹出的值,而不是直接在本地变量上进行操作,所以在Java虚拟机编译的代码中,经常可以看到在本地变量和操作数栈间传输值的指令。在spin方法中,istore_1和iload_1指令用来在本地变量1与操作数栈之间传递值,这些指令隐式地作用于本地变量1。istore_1从操作数栈中弹出一个int值并存储到本地变量1中,iload_1则将本地变量1的值推入操作数栈。

局部变量的使用和重复使用是编译器作者的责任,专门的加载和存储指令鼓励编译器尽可能地重用本地变量,这样生成的代码不仅运行更快,代码更紧凑,而且在帧中占用的空间也更小。

Java虚拟机对频繁进行的操作提供了特别的支持,例如iinc指令就通过一个字节的有符号值来增加本地变量的内容。在spin方法中,iinc指令增加了第一个本地变量(第一个操作数)1(第二个操作数),这在实现循环结构时特别便捷。

spin的for循环主要由这些指令完成:

5 iinc 1 1 // 本地变量1自增1(即 i++)
8 iload_1 // 将本地变量1(即 i)压入栈顶
9 bipush 100 // 将整型常量100压入栈顶
11 if_icmplt 5 // 比较栈顶两int值大小,并如果小于(i < 100),则跳转

bipush指令将整数100推入到操作数栈中,然后if_icmplt指令将这个值从操作数栈中弹出,并与i进行比较。如果比较成功(即变量i小于100),则执行流转移到索引5,开始下一次的for循环迭代。否则,执行流会继续到if_icmplt指令后的指令。

如果在spin示例中,循环计数器使用的是除int之外的其他数据类型,那么编译后的代码会必须改变以反映这个不同的数据类型。比如,如果spin示例使用的是double类型来替代int,情况将会如下:

void dspin() {
    double i;
    for (i = 0.0; i < 100.0; i++) {
        ;    // Loop body is empty
    }
}

编译后的代码是:

Method void dspin()
0 dconst_0 // 将双精度浮点数0.0推送至栈顶
1 dstore_1 // 将栈顶双精度浮点数存入局部变量1和2
2 goto 9 // 第一次通过时不递增
5 dload_1 // 将局部变量1和2中的双精度浮点数推送至栈顶
6 dconst_1 // 将双精度浮点数1.0推送至栈顶
7 dadd // 执行加法操作;没有对应的dinc指令
8 dstore_1 // 将加法操作结果存入局部变量1和2
9 dload_1 // 将局部变量1和2中的双精度浮点数推送至栈顶
10 ldc2_w #4 // 将双精度浮点数100.0推送至栈顶
13 dcmpg // 执行比较操作;没有对应的if_dcmplt指令
14 iflt 5 // 如果比较结果小于0(即i < 100.0),循环跳转至指令5
17 return // 完成后返回_void

双精度值占用两个局部变量,尽管只能使用两个局部变量中较小的索引来访问它们。对于 long 类型的值也是如此。再举个例子,

double doubleLocals(double d1, double d2) {
    return d1 + d2;
}

编译成这样

Method double doubleLocals(double,double)
0   dload_1       // 局部变量 1 和 2 中的第一个参数
1   dload_3       // 局部变量 3 和 4 中的第二个参数
2   dadd
3   dreturn

请注意,用于存储双精度值的局部变量对应的变量,不应当被单独操作。

由于Java虚拟机的操作码仅有1个字节,其编译后的代码非常紧凑。然而,1字节的操作码同时也意味着Java虚拟机指令集必须保持较小的大小。作为一种折中,Java虚拟机对所有数据类型的支持并不完全相同:它并非完全正交(见表2.11.1-A)。

例如,在示例spin中,对于类型为int的值的比较,可以使用一个单独的if_icmplt指令来实现;但是,Java虚拟机指令集中没有单个指令能够对类型为double的值执行条件分支。因此,dspin必须使用一个dcmpg指令,随后跟着一个iflt指令来实现对类型为double的值的比较。

Java虚拟机对类型为int的数据提供了最直接的支持。这部分是为了预期Java虚拟机的操作数栈和局部变量数组的高效实现。这也是基于典型程序中int类型数据出现的频率动机考虑的。其他整数类型的支持则不那么直接。例如,没有专门用于byte、char或short类型的存储、加载或添加指令。下面是使用short类型编写的spin示例:

void sspin() {
    short i;
    for (i = 0; i < 100; i++) {
        ;    // Loop body is empty
    }
}

它必须按照以下方式为Java虚拟机编译:使用操作其他类型(很可能是int类型)的指令,并在必要时在short和int值之间进行转换,以确保对short数据的操作结果保持在适当的范围内:

Method void sspin()
0   iconst_0
1   istore_1
2   goto 10
5   iload_1        // The short is treated as though an int
6   iconst_1
7   iadd
8   i2s            // Truncate int to short
9   istore_1
10  iload_1
11  bipush 100
13  if_icmplt 5
16  return

在Java虚拟机中,对byte、char和short类型缺乏直接支持并不特别棘手,因为这些类型的值在内部会被提升为int类型(byte和short通过符号扩展成int,char通过零扩展)。因此,可以使用int指令来进行对byte、char和short数据的操作。唯一的额外成本是需要将int操作的结果截断到有效范围。

在Java虚拟机中,long和浮点类型有中等程度的支持,它们仅缺少完整的条件控制转移指令集。

算术运算

Java虚拟机通常在其操作数栈上执行算术操作。(例外是iinc指令,它直接增加局部变量的值。)例如,align2grain方法将一个int值与给定的2的幂对齐:

int align2grain(int i, int grain) {
    return ((i + grain-1) & ~(grain-1));
}
align2grain的方法,它接收两个整型参数i和grain,并返回一个按照grain指定的2的幂进行对齐的整数。
下面是这个方法的逻辑解释:
i + grain - 1:首先将输入的i加上grain(2的幂)减去1。这一步是为了确保在进行位运算时能够上舍入
到最近的grain的倍数。比如,如果grain是8并且i是任意整数,如果i本身不是8的倍数,加上7之后肯定
会超过下一个8的倍数。
~(grain - 1):这是一个位取反操作。先对grain减去1得到一个数,这个数的二进制表示中,低位上
grain代表的2的幂次方位全是1,其余位是0。比如,如果grain是8,则grain-1为7,其二进制为0111。
对它取反得到~(grain-1)的结果是1000,即二进制的高位是1,其余都是0。
((i + grain - 1) & ~(grain - 1)):最终结果通过之前得到的上舍入值和位取反的值进行位与运算。
位与运算保留了两个操作数中均为1的位。这段代码通过与操作去除了除grain的最小幂次方以上的位,
确保了结果是小于或等于原来的i且为grain的倍数。

算术运算的操作数是从操作数栈中取出的,而运算的结果则会被推入操作数栈。这样,算术子计算的结果就能作为它们所在嵌套计算的操作数使用。例如,计算~(grain-1)这个操作就是通过这些指令来实现的:

5 iload_2 // 将变量grain的值压入操作数栈
6 iconst_1 // 将整数常量1压入操作数栈
7 isub // 执行减法操作,并将结果压入操作数栈
8 iconst_m1 // 将整数常量-1(即位反码表示的1)压入操作数栈
9 ixor // 执行异或操作,并将结果压入操作数栈

首先,使用本地变量2的内容和一个直接给定的整数值1来计算grain-1。这些操作数从操作数栈中弹出,它们的差值被推回操作数栈。这个差值因此立即可以作为ixor指令的一个操作数。(回想一下,~x 等价于 -1^x。)类似地,ixor指令的结果成为后续iand指令的一个操作数。

整个方法的代码如下:

Method int align2grain(int,int)
0   iload_1
1   iload_2
2   iadd
3   iconst_1
4   isub
5   iload_2
6   iconst_1
7   isub
8   iconst_m1
9   ixor
10  iand
11  ireturn

访问运行时常量池

在Java里,要通过当前类的运行时常量池去访问一大堆数值常量、对象、类中的字段和方法。至于int、long、float和double这几种类型的数据,还有对String类实例的引用,都是用ldc、ldc_w、ldc2_w这几个指令来搞定的。

说到ldc和ldc_w指令,它们主要用来从运行时常量池里拿到非double和long类型的值的,String类的实例也包括在内。如果常量池里东西太多,需要用更大的索引号去访问某个东西的时候,就用ldc_w来替换掉ldc。至于ldc2_w指令,是用来专门访问double和long类型的值的,这俩类型它没提供非宽版本的指令。

那些byte、char、short类型的整数常量,还有些小的int值,得通过bipush、sipush或iconst_<i>这几个指令来编译它们。还有,一些小的浮点数常量可能要通过fconst_<f>和dconst_<d>这俩指令来编译。

这所有的情况下,编译都挺直接,也算简单。就比如说:

void useManyNumeric() {
    int i = 100;
    int j = 1000000;
    long l1 = 1;
    long l2 = 0xffffffff;
    double d = 2.2;
}

编译如下:

Method void useManyNumeric()
0 bipush 100 // 使用bipush指令推入小int常量
2 istore_1
3 ldc #1 // 使用ldc指令推入大int常量(1000000)
5 istore_2
6 lconst_1 // 微小的long值使用快速的lconst_1指令
7 lstore_3
8 ldc2_w #6 // 推入long 0xffffffff(也就是,一个int -1)
// 任何long常量值可以使用ldc2_w推入
11 lstore 5
13 ldc2_w #8 // 推入double常量 2.200000
// 不常见的double值也可用ldc2_w推入
16 dstore 7

相关推荐

【推荐】一个开源免费、AI 驱动的智能数据管理系统,支持多数据库

如果您对源码&技术感兴趣,请点赞+收藏+转发+关注,大家的支持是我分享最大的动力!!!.前言在当今数据驱动的时代,高效、智能地管理数据已成为企业和个人不可或缺的能力。为了满足这一需求,我们推出了这款开...

Pure Storage推出统一数据管理云平台及新闪存阵列

PureStorage公司今日推出企业数据云(EnterpriseDataCloud),称其为组织在混合环境中存储、管理和使用数据方式的全面架构升级。该公司表示,EDC使组织能够在本地、云端和混...

对Java学习的10条建议(对java课程的建议)

不少Java的初学者一开始都是信心满满准备迎接挑战,但是经过一段时间的学习之后,多少都会碰到各种挫败,以下北风网就总结一些对于初学者非常有用的建议,希望能够给他们解决现实中的问题。Java编程的准备:...

SQLShift 重大更新:Oracle→PostgreSQL 存储过程转换功能上线!

官网:https://sqlshift.cn/6月,SQLShift迎来重大版本更新!作为国内首个支持Oracle->OceanBase存储过程智能转换的工具,SQLShift在过去一...

JDK21有没有什么稳定、简单又强势的特性?

佳未阿里云开发者2025年03月05日08:30浙江阿里妹导读这篇文章主要介绍了Java虚拟线程的发展及其在AJDK中的实现和优化。阅前声明:本文介绍的内容基于AJDK21.0.5[1]以及以上...

「松勤软件测试」网站总出现404 bug?总结8个原因,不信解决不了

在进行网站测试的时候,有没有碰到过网站崩溃,打不开,出现404错误等各种现象,如果你碰到了,那么恭喜你,你的网站出问题了,是什么原因导致网站出问题呢,根据松勤软件测试的总结如下:01数据库中的表空间不...

Java面试题及答案最全总结(2025版)

大家好,我是Java面试陪考员最近很多小伙伴在忙着找工作,给大家整理了一份非常全面的Java面试题及答案。涉及的内容非常全面,包含:Spring、MySQL、JVM、Redis、Linux、Sprin...

数据库日常运维工作内容(数据库日常运维 工作内容)

#数据库日常运维工作包括哪些内容?#数据库日常运维工作是一个涵盖多个层面的综合性任务,以下是详细的分类和内容说明:一、数据库运维核心工作监控与告警性能监控:实时监控CPU、内存、I/O、连接数、锁等待...

分布式之系统底层原理(上)(底层分布式技术)

作者:allanpan,腾讯IEG高级后台工程师导言分布式事务是分布式系统必不可少的组成部分,基本上只要实现一个分布式系统就逃不开对分布式事务的支持。本文从分布式事务这个概念切入,尝试对分布式事务...

oracle 死锁了怎么办?kill 进程 直接上干货

1、查看死锁是否存在selectusername,lockwait,status,machine,programfromv$sessionwheresidin(selectsession...

SpringBoot 各种分页查询方式详解(全网最全)

一、分页查询基础概念与原理1.1什么是分页查询分页查询是指将大量数据分割成多个小块(页)进行展示的技术,它是现代Web应用中必不可少的功能。想象一下你去图书馆找书,如果所有书都堆在一张桌子上,你很难...

《战场兄弟》全事件攻略 一般事件合同事件红装及隐藏职业攻略

《战场兄弟》全事件攻略,一般事件合同事件红装及隐藏职业攻略。《战场兄弟》事件奖励,事件条件。《战场兄弟》是OverhypeStudios制作发行的一款由xcom和桌游为灵感来源,以中世纪、低魔奇幻为...

LoadRunner(loadrunner录制不到脚本)

一、核心组件与工作流程LoadRunner性能测试工具-并发测试-正版软件下载-使用教程-价格-官方代理商的架构围绕三大核心组件构建,形成完整测试闭环:VirtualUserGenerator(...

Redis数据类型介绍(redis 数据类型)

介绍Redis支持五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)及Zset(sortedset:有序集合)。1、字符串类型概述1.1、数据类型Redis支持...

RMAN备份监控及优化总结(rman备份原理)

今天主要介绍一下如何对RMAN备份监控及优化,这里就不讲rman备份的一些原理了,仅供参考。一、监控RMAN备份1、确定备份源与备份设备的最大速度从磁盘读的速度和磁带写的带度、备份的速度不可能超出这两...

取消回复欢迎 发表评论: