最近接盘了公司的分布式文件存储系统,其底层不出意外的采用FastDFS以及HBase作为存储中间件,在熟悉代码的时候,对FastDFS客户端的部分代码产生了疑惑,如果你看完没有疑惑就没必要继续往下阅读了,关掉页面左转,刷刷沸点,摸摸鱼不香吗?

如下图所示这是一个将字节数组转换为long的函数, 格式为big-endian(大端)

FastDFS的协议头中有8个字节用来标识数据包的长度,此函数就用于获取数据包的长度

初看觉得这就是普通的移位操作没有任何疑惑,再细看发现不少问题

  • 为什么对正负数区别对待
  • 为什么值为负数的时候要先加上负数再移动位数呢” />

    但凡谈及二进制,有符号数和无符号数的话题就不得不说道说道了,但是由于Java中不存在无符号数,因此重点谈一下有符号数的表示方法。

    对于如何表示有符号数,通常有以下几种二进制编码方案

    • 反码
    • 原码
    • 补码

    反码和原码的表示方法都有一个奇怪的熟悉,那就是对于数字0有两种不同的编码方式。这两种表示方法都有一个奇怪的属性,把[00..0]都解释为+0,而-0在 在原码中表示[10..0],在反码中表示为[11…1]. 但是几乎所有的现代的机器都使用补码来表示有符号数,包括Java。

    引用自《CSAPP》

    anyway,反码和原码并不是讨论的重点,重点看一下补码是怎么一回事.

    对于一个补码,其最高位用来表示正负,为0为正数, 为1则为负数.

    一个严谨的补码定义如下

    还是引自《CSAPP》

    • 向量指的是二进制编码的数据,如x6指的就是二进制编码中第6位的值,x只可能取1或0
    • 通过此公式我们可以将补码转为对应的十进制数

    如以下例子

    从位运算表达式中看JVM的栈帧设计

    0x02 你确定byte真的只占一个字节吗” />

    这有可能的是语法层面的限制, 又或许有其他原因呢” />

    Java代码与字节码代码的对应的关系如下图所示

    本次代码涉及到的指令不多,咱先简单介绍一下

    字节码指令作用iconst_1将int值1推入操作数栈iconst_5将int值5推入操作数栈istore_1对操作数栈执行出栈操作,将返回的值赋值本地给变量表的第一个元素,此值必须是intiload_1将本地变量表的第一个元素推入操作数栈,此时该值位于操作数栈顶ishl从操作数栈中出战两个元素val1, val2,将val1左移val2位,val1和val2类型必须为int,并将结果入栈(保存到栈顶)istore_2对操作数栈执行出栈操作,将返回的值赋值本地给变量表的第二个元素,此值必须是int

    操作数栈和本地变量表是啥玩意咱先暂且不论(下文再谈),但根据字节码指令来分析的话,不难得出结论,你以为你用的是byte实际上在JVM的视角来说你用的是int.

    这也就是意味 byte a =-5 的实际上的二进制补码是
    11111111111111111111111111111011,我们的目的是将 111111011 左移N位让其回到原来的位置. 此时,如果不对负数进行处理的情况下将byte数组还原为long则必然会遇到与原数据不一致的情况,对于此种情况只需要将其与0xFF进行与运算即可获取到原数据

    11111111 11111111 11111111 11111011 & 00000000 00000000  00000000 11111111=00000000 00000000  00000000 11111011复制代码

    经过如此操作再对其进行移位操作就可以将数据正确的还原到原本的位置,皆大欢喜.

    上文中 256+bs[offset] 实际上等效于 bs[offset] & 0xFF

    为什么会等效呢” />

    那么在执行方法调用时,其操作数栈和本地变量表如下图所示

    从位运算表达式中看JVM的栈帧设计

    初看此图,你可能会有疑惑,为啥本地变量表里面还有this” />

    接下来,我们跟字节码走一遍,看看JVM是如何执行字节码的

    iconst_1 将常量1推入操作数栈(push),执行完后操作数栈如下所示

    istore_1 将操作数栈顶的元素出栈,赋值给本地变量表的第一个Slot

    即 本地变量表[1] = 操作数栈.pop()

    从位运算表达式中看JVM的栈帧设计

    iload_1 将本地变表的第一个Slot值入栈,执行完后操作数栈如下所示

    即 操作数栈.push(本地变量表[1])

    iconst_5 将常量值5推入操作数栈,执行完后操作数栈如下所示

    从位运算表达式中看JVM的栈帧设计

    ishl 出栈两个元素执行左移位操作,将结果入栈即,执行完之后操作数栈如下所示

    var1 = 操作数栈.pop();var2 = 操作数栈.pop();操作数栈.push(var2 << var1);复制代码

    istore_2 将操作数栈顶的元素出栈,赋值给本地变量表的第二个Slot

    即 本地变量表[1] = 操作数栈.pop()

    从位运算表达式中看JVM的栈帧设计

    理解完操作数栈和本地变量表是如何互相搭配完成工作的之后,还有一个疑问没解决,从上面的分析可以看出本地变量表是以为Slot(槽位)作为基本分配单位的,那么问题来了本地变量表的一个Slot(槽位)占据多少空间呢” />

    其本地变量表如下图所示

    从位运算表达式中看JVM的栈帧设计

    0x04 一点疑惑

    就我而言,由于学习过汇编的原因,了解JVM字节码执行原理时,用标题党的话来说就是震惊,没想到还有这种操作,JVM竟然是基于栈的虚拟机执行引擎,其特点就是进行数据运算的时候要先把数据出栈,执行完之后再将结果入栈,相反直觉.相反,寄存器的设计可以在寄存间直接进行数据运算,并将结果保存到寄存器.

    但实际上性能并不低,本地变量表的设计和操作数栈都能很有效的利用CPU的高速缓存.

    那有没有同汇编一样基于寄存器的执行引擎呢?

    还真有,它经常作为内嵌的执行引擎引入到各大应用如Redis,Nginx,没错它就是Lua.