(i << 24) | ((i & 0xff00) << 8) | ((i >>> 8) & 0xff00) | (i >>> 24);
J K L,公众号:K字的研究Java Integer变态(bit)函数浅析
前几天分享过Integer.reverseBytes函数,能把一个32bit整数里的4个byte反转过来.形如0x01020304
的数字,经过这个反转,就会变成0x04030201
.这个功能,一般被用来解决端序问题。
那么,什么是端序问题?
端序(Endianness)
端序俗称字节序(Byte Order)
.整数0x01020304
有4个字节(左侧为高字节,右侧为低字节),可以假装他是一个byte[4]
.
一般来说, 计算机访问内存,是按字(word)
进行的.我把一个4字节的int写入到某个地址, 是先写低位写成04030201
,还是先写高位写成01020304
.这就是问题的来源.
按照这个区分, 高位在前的叫大端(big endian)
,低位在前的叫小端(little endian)
.
端序之争
这事儿本身不算个事儿,只是不同硬件厂商在做一个细节时候,选择了不同的实现的问题.
可事情就是这么的反直觉, 重要的事情其实争论不起来.争起来的,反而都是些小问题,背后其实是上古时期硬件玩家们为了标准话语权做的角力.
正因为, 他不算个事儿, 导致了不管怎么实现, 都不会有问题, 就变成了一个争执点.最后这事带来的结果就是, 两种字节序共存了.
Java里的字节序
Java作为计算机大家庭的一员,不免还是会受到一点点的影响.接下来, 介绍几个Java里可以看到字节序影子.
Java ClassFile
Java的class文件, 是一个二进制文件.可以考虑成byte组成的一个流, 是一个典型的扁平结构.
我们来看下, 一个int,或者long, 是如何映射为byte流的, 哪个在前,哪个在后.
写下面的源文件:
class K {
int a = 0xcafedada;
}
使用javac
进行编译:
javac K.java
得到一个K.class
, 如果用hexdump
命令,或别的编辑器打开 K.class
,会发现类似下图的内容.
//hexdump K.class |grep "ca fe da da" -C100 --color="auto" 注意红字部分
0000000 ca fe ba be 00 00 00 3d 00 12 0a 00 02 00 03 07
0000010 00 04 0c 00 05 00 06 01 00 10 6a 61 76 61 2f 6c
0000020 61 6e 67 2f 4f 62 6a 65 63 74 01 00 06 3c 69 6e
0000030 69 74 3e 01 00 03 28 29 56 03 ca fe da da 09 00
0000040 09 00 0a 07 00 0b 0c 00 0c 00 0d 01 00 01 4b 01
0000050 00 01 61 01 00 01 49 01 00 04 43 6f 64 65 01 00
0000060 0f 4c 69 6e 65 4e 75 6d 62 65 72 54 61 62 6c 65
0000070 01 00 0a 53 6f 75 72 63 65 46 69 6c 65 01 00 06
0000080 4b 2e 6a 61 76 61 00 20 00 09 00 02 00 00 00 01
0000090 00 00 00 0c 00 0d 00 00 00 01 00 00 00 05 00 06
00000a0 00 01 00 0e 00 00 00 27 00 02 00 01 00 00 00 0b
00000b0 2a b7 00 01 2a 12 07 b5 00 08 b1 00 00 00 01 00
00000c0 0f 00 00 00 0a 00 02 00 00 00 01 00 04 00 02 00
00000d0 01 00 10 00 00 00 02 00 11
00000d9
很明显, 这个值0xcafedada
是被按照大端方式写入到class
文件里的.查阅JVM标准的第4章, The class File Format [1], 就会发现确实有这个规定.
A class
file consists of a stream of 8-bit bytes. 16-bit and 32-bit quantities are constructed by reading in two and four consecutive 8-bit bytes, respectively. Multibyte data items are always stored in big-endian order, where the high bytes come first.https://docs.oracle.com/javase/specs/jvms/se17/html/jvms-4.html
注: 一般来说应该先查标准后实验的, 我习惯不好.
Java序列化
Java提供了序列化功能, 可以把对象序列化成字节流. 这次我们先查查标准文件.然后来试一下, 这里会把对象序列化成什么样.还是刚刚的类,但是加上了可序列化接口.
K k = new K();
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream os = new ObjectOutputStream(out);
os.writeObject(k);
byte[] bytes = out.toByteArray();
for (byte b : bytes) {
String s = Integer.toHexString(Byte.toUnsignedInt(b));
System.out.print(s + " ");
}
// 输出内容
//ac ed 0 5 73 72
// 0 c 表示12个字符的类名
// 73 68 2e 6e 6f 77 2e 61 66 6b 2e 4b 对应类名 sh.now.afk.K 正好12个
// e7 da 16 5a 4f 39 5f ec 这8个byte是一个long, 表示serialVersionUID
// 2
// 0 1 属性数量
// 49 这个是I 代表一个int类型属性
// 01 代表属性名长度为 1
// 61 这个是'a'
//78 70 结束符
//ca fe da da
int在这里面哪头朝前已经很明显了, 这序列化流是大端的.
Java Object
Java对象,最后还是要加载到内存里才能真正用起来的.我们来看看内存里的Java对象是怎么样的.
这里需要一点额外的小知识,class的多个对象可以放在内存里的不同位置,但同一个属性相对于各自对象的起始位置,相对位移(offset)
是固定的.
比如假如有两个 new K()
,一个放在内存位置0, 一个放在内存位置100.他们里面的属性a
, offset是4.那么,我在 0+4
可以读到第一个对象的a
值,在100+4
可以读到第二个对象的a
值.
接下来实战一下, 先找出上面的类K
,他的a
属性offset
是多少.
JOL(Java Object Layout)
这里借用一个叫jol(Java Object Layout)
的工具.
这个工具是openjdk项目里提供的一个小玩意, 借助Unsafe, JVMTI, and Serviceability Agent
对Java 对象实施了精准防控, 详情可以看看jol官网[2].
引入jol的pom依赖.
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-cli</artifactId>
<version>0.16</version>
</dependency>
获取class K 里属性的offset
Field aField = K.class.getDeclaredField("a");
long offset = VM.current().fieldOffset(aField);
System.out.println("offset = " + offset);
//这里是12
直接从虚拟机里读取对象内存
要按照offset读取内容, 需要借助VM.current
这个方法返回的虚拟机实例.
//获取对象某偏移量下的4个byte,解释成int
int a = VM.current().getInt(new K(), offset);
System.out.printf(Integer.toHexString(a));
//输出 cafedada
很明显, 这里是已经读取到了int值, getInt
一次取了4个byte.
下面换个getByte
,一次读一个byte
for (int i = 0; i < Integer.BYTES; i++) {
byte b = VM.current().getByte(k, offset + i);
String s = Integer.toHexString(Byte.toUnsignedInt(b));
System.out.print(s + " ");
}
//da da fe ca
答案呼之欲出, 最先读到的是低位的0xda
, 这里用的小端法表示.其实很正常, 现在的主流电脑基本都是小端的了.
大端和小端的起源
很多人可能都看出来了,大端和小端, 并不是中国话, 只是英文的翻译.
英文的Big-Endians
Little-Endians
出自一位名叫Danny Cohen
的文章 《ON HOLY WARS AND A PLEA FOR PEACE》[3] .
这篇文章里, 作者借用小说《格列佛游记》小人国部分的第四章[4], 小人国因为鸡蛋🥚是从大头开还是从小头开而发生战争的问题来描述字节序之争,并把字节序的两种支持者, 称为大端党
,小端党
.
从此,这两个名词扬名全世界.
后话
其实除了端序这一点,格列佛游记也影响很多其他地方。比如宫崎骏的《天空之城》,雅虎公司的Yahoo这个名字,都跟格列佛游记有关。
不过我猜没人记得作者名字, 今天我给一个偏方, 保证看完就忘不了.
Jonathan Swift
参考资料
The class
File Format : https://docs.oracle.com/javase/specs/jvms/se17/html/jvms-4.html
jol官网: http://openjdk.java.net/projects/code-tools/jol/
[3]《ON HOLY WARS AND A PLEA FOR PEACE》: https://dcc.ufrj.br/~gabriel/progpar/danny_co.pdf
[4]小人国部分的第四章: https://www.thn21.com/Article/chang/youji/5.html
有国于蜗之左角者,曰触氏;有国于蜗之右角者,曰蛮氏。时相与争地而战,伏尸数万,逐北旬有五日而后反 《庄子·则阳》
原文始发于微信公众号(K字的研究):鸡蛋🥚从哪头敲开比较香?
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/24784.html