[讨论] Java基本类型的物理存储大小

deyimsf 2013-08-06
在java中我们知道byte就表示一个字节,像char是两个,int是四个。

现在有这样一个问题,在32位的jvm中一个byte的实际存储大小是4个字节,int不用说也是四个,那么char是不是也是四个?怎么证明?
chong_zh 2013-08-06
在32位的jvm中一个byte的实际存储大小是1个字节,char是2个.
deyimsf 2013-08-06
chong_zh 写道
在32位的jvm中一个byte的实际存储大小是1个字节,char是2个.


32位jvm中一个变量的slot是32位的,你告诉我你是怎么把一个byte放到这个slot的?
RednaxelaFX 2013-08-06
楼主的先入观点把几种不同的概念混为一谈了。

=================================================

1、数据类型的有效宽度

JVM规范规定的是抽象的、概念中的JVM所需要支持的行为。其中对于数据类型的规定在这里:
http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-2.html#jvms-2.2
例如说,这里说明了int的语义是32位补码的整型数,其值域为 -2147483648 到 2147483647 (-2^31 到 2^31 - 1)。

那么用更大的存储空间来存它行不行呢?当然可以,只要从上层的Java代码只能感知到其中的32位是有效值即可。

char也是同理。JVM规范规定了它的语义是表示Unicode codepoint的16位无符号整型数,值域为 0 到 65535。用更大的空间来存它是没问题的,只要从上层的Java代码只能看到其中16位是有效值即可。

(例子很重要所以几乎一样的话说了两遍⋯)

那么具体到存储空间,JVM规范又做了怎样的规定呢?

在JVM栈上有局部变量与操作数栈。其中局部变量的规定是:
http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-2.html#jvms-2.6.1
引用
A single local variable can hold a value of type boolean, byte, char, short, int, float, reference, or returnAddress. A pair of local variables can hold a value of type long or double.

它只说了1个局部变量的slot至少要能存下boolean一直到returnAddress的值,但没规定一定要有多宽。少了不行,多了是没问题的。

操作数栈呢?
http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-2.html#jvms-2.6.2
引用
Each entry on the operand stack can hold a value of any Java Virtual Machine type, including a value of type long or type double.

跟局部变量的slot的规定相似。

然后看Java堆上的Java对象里的字段。JVM规范说:
http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-2.html#jvms-2.7
引用
The Java Virtual Machine does not mandate any particular internal structure for objects.

也就是说实现JVM的时候,你想如何设计Java对象在内存里的布局都没关系,只要该存的值都存着就行。跟上面一样,空间太小了不行,但多了是没问题的。

=================================================

2. 实际实现使用的存储空间大小

那么真实的JVM会怎么做呢?

--------------------------------------------------------------

以解释器为主的JVM会采用比较贴近JVM规范所说的方式来组织自己的运行时数据结构。特别是入门级、简单的JVM实现更是如此。

Sun JDK 1.0.2时代的Sun JVM的做法是:它是一个32位JVM,解释器栈用32位为单位的slot来组织。JVM规范里所规定的局部变量、操作数栈的项就跟这里的slot对应。int与比int窄的整型、float、reference、returnAddress都占1个slot,long与double占两个slot。
楼主问char实际存储占多少空间,在这个JVM里char在栈上占4字节。甚至连有效数据只有1位的boolean在栈上也占4字节。

浪费JVM栈空间影响大吗?有影响,但还通常不算糟糕。
毕竟JVM栈空间主要受局部变量的个数以及方法嵌套调用的深度影响;只要嵌套调用不太深,其实用不了多少JVM栈空间。只有在深度递归或者深度嵌套调用的情况下,JVM栈空间才会显得紧缺。

然后看这个JVM如何组织Java堆来存储Java对象。要分为Java类的实例和数组实例两方面来看。
它很偷懒,Java类的实例的字段同样使用32位为单位的slot来组织,字段在内存里的顺序与Class文件存的field_info顺序一致,没有重排序。这里1个char也是占了4字节,虽然其中只有2字节是有效数据。
而数组则采用紧凑布局,数组元素的宽度与其有效宽度一致(boolean除外,1个boolean占1字节但有效数据只有1位)。于是这个JVM里char[]里的char就占2字节。

浪费Java堆影响大吗?这就大了。典型的面向对象应用会产生相当大量的对象,每个小字段都浪费一点就可以浪费很多。特别是当编写Java代码的程序员原本为了省内存而用较窄的字段时,会失望的发现在这个JVM上几乎是无用功。

楼主有没有感受到原始数据类型“占多少字节”不是那么简单的事情了?
在哪个JVM实现里、存在什么地方都有关系。

--------------------------------------------------------------

那么拉到现在我们身边的现实,Oracle/Sun JDK里的HotSpot VM。

HotSpot VM有32位和64位版。楼主问的问题在这俩版本上表现不同。
另外在JVM栈的方面,解释器用的栈桢(interpreted frame)与被JIT编译后的代码用的栈桢(compiled frame)的布局也不一样。

32位版上,解释器的栈桢布局是以32位为单位的slot来存储局部变量与操作数栈项的。情况跟楼主的想像相同:1个char在局部变量与在操作数栈上都占4字节。
编译后代码的栈桢布局里,局部变量也是用32位slot来存储,没有操作数栈。

64位版上,解释器的栈桢的slot是64位的。这里1个char会占用8字节,1个boolean、short、int、float也是8字节;而1个long或double仍然占2个slot,也就是64*2=128字节。这主要是为了实现方便,代码容易与32位版统一起来。
编译后代码的栈桢里,局部变量也是用64位slot,但long和double可以只占1个slot;同样没有操作数栈。这里1个char也占8字节。

在Java堆上,HotSpot VM采用紧凑的对象布局。字段会根据其宽度做重排序,以期尽量有效的使用内存。除了boolean占1字节外,其它数据类型的字段都占与其有效宽度相同的大小。数组也是一样。所以在HotSpot VM里Java对象的char类型字段以及char[]里的char都是占2字节。

另外还有更有趣的:在Oracle/Sun JDK 6的后期版本里有压缩字符串功能,可以用-XX:+UseCompressedStrings打开;JDK7与JDK8暂时去掉了这个功能。
本来1个java.lang.String实例会引用着1个char[]来存储实际字符串内容。当这个功能开启的时候,构造String实例时会检查里面的内容是否只有ASCII范围内的字符,如果是就用byte[]来存字符串内容,不是就退回到用char[]来存。String的用户则完全不受影响,仍然“觉得”String里装着的是一串char。
这样,表面上有6个char的1个String,背后的每个字符可能只占了1字节(因为每个字符都是存在byte[]里的一个元素)。
要留意这里是说“压缩字符串”,而不是“压缩字符”。这种实现中char仍然是UTF-16的code point,仍然至少要占2个字节的有效空间。

=================================================

3. 如何“证明”

开源的JVM的话读源码就足以知道了。HotSpot VM不但开源,而且有许多工具便于调试。例如我以前介绍过的Serviceability Agent用来看内存布局的信息就很方便。
http://rednaxelafx.iteye.com/blog/1847971

不开源的JVM有些有零星的文档里有说实现细节。再不济可以自己用native debugger去调试一吧。windbg、gdb之类都是你的伙伴。喜欢IDA或者OllyDBG也行。上面说的JDK 1.0.2的Sun JVM我是自己调试来了解其实现细节的。
chong_zh 2013-08-06
窃以为Slot的大小实在不应该作为“实际存储大小”的依据,Slot应该被作为JVM寻址的粒度。“实际存储大小”应该就是数据所占的内存空间,所以就是byte 1字节,char 2字节。
RednaxelaFX 2013-08-06
chong_zh 写道
窃以为Slot的大小实在不应该作为“实际存储大小”的依据,Slot应该被作为JVM寻址的粒度。“实际存储大小”应该就是数据所占的内存空间,所以就是byte 1字节,char 2字节。

您所理解的就是“数据的有效宽度”的意义。
其实这问题很简单:您去上公厕蹲坑,把那个坑门一关您就把整个隔间给占用了,无论您是姚明还是小四。
别人在外面着急要进坑的时候不会在意您自身到底有多大,而会在意您占用了多少空间——一整个隔间。
chong_zh 2013-08-06
RednaxelaFX 写道
chong_zh 写道
窃以为Slot的大小实在不应该作为“实际存储大小”的依据,Slot应该被作为JVM寻址的粒度。“实际存储大小”应该就是数据所占的内存空间,所以就是byte 1字节,char 2字节。

您所理解的就是“数据的有效宽度”的意义。
其实这问题很简单:您去上公厕蹲坑,把那个坑门一关您就把整个隔间给占用了,无论您是姚明还是小四。
别人在外面着急要进坑的时候不会在意您自身到底有多大,而会在意您占用了多少空间——一整个隔间。


窃以为坑的大小还是应该定位为为“JVM内存管理方法”或者"JVM寻址粒度"的范畴,“实际存储大小”的“实际”的题中应有之义应该是数据的大小。映射到上面的例子,应该是设计厕具大小的施工人员关心的问题。
kaiyuanjava 2013-08-06
嗯,确实虚拟机内部不管什么地方,要设计到最优,就应该想好怎么分配坑的问题啊,不过要是真的这样做了,可能是不是还要添加个算法去维护一个坑的隔离问题?
比如一个32位SLOT里面放两个SHORT类型,那么维护局部变量作用域就比较复杂了吧!
RednaxelaFX 2013-08-06
chong_zh 写道
窃以为坑的大小还是应该定位为为“JVM内存管理方法”或者"JVM寻址粒度"的范畴,“实际存储大小”的“实际”的题中应有之义应该是数据的大小。映射到上面的例子,应该是设计厕具大小的施工人员关心的问题。

首先“JVM寻址粒度”是nonsense。实际的JVM实现并不存在寻址粒度上的问题。愿意的话把栈桢也设计成紧凑的也没问题,只是有别的取舍而已。底下的体系结构允许到多大粒度寻址,JVM的实现就能够在多大粒度上寻址。而且只要有mask和shift的指令,就算在128位寻址粒度的体系结构上也照样可以把boolean塞到1位里,而不让它多占1位。

主要的取舍源自两方面。
1、贴近JVM规范来实现比较容易做对,代码写起来也比较简单。这个时候当然JVM规范说概念上可以怎么做就怎么做。这可以对应JDK 1.0.2的例子
2、如果把JVM栈实现在系统栈(machine stack)或者叫做“C栈”(C stack)上,那就得遵循底下的体系结构对栈上数据的对齐要求——栈定指针必须指向符合对齐要求的地址。当然栈桢里的数据仍然可以紧凑排布,就是实现的代码写起来麻烦一点而已。这里要是偷个懒的话就会像HotSpot VM的64位版的解释器那样,把本来只需要32位的slot映射到“占用64位但只使用其中的32位”。

然后是“实际”的含义。

换个例子,Unicode字符可以用UTF-8的编码形式来表示。“您”在Unicode里是U+60A8,只需要2个字节就可以表示,但用UTF-8的编码形式会使用"\xE6\x82\xA8"的3个字节来表示。其中有些纯粹是编码用的信息藏在了这3个字节里。这个时候我们会说一个Unicode字符“您”在UTF-8这种具体编码中占用了3字节。

映射回现实也有很多这样的状况。可能有公司规定早9晚6。职员A一大早6点不到就起床,洗漱、吃早餐,然后从城东到城西,最后10点半到公司,喝杯咖啡看看新闻就到中午了,出去吃个午饭回来打个盹到2点,随便干干活到4点,烟瘾上来了,约上烟友去楼下抽一两根,到6点准时离开公司,然后又是城西到城东,晚上9点多回到家。
职员A觉得他为了工作“实际”每天花费了15个小时有多,而老板觉得他“实际”只工作了2小时。各自有合理性。

那“实际”占用了多少呢?您的观点是消费者的观点,“我只拿到了值2字节的数据,所以不管你传3字节还是多少字节给我,我都只给2字节的钱”。而生产者或许会有异议,“因为现实成本,我用了装3字节的容器才把内容传给你,所以你要给我3字节的钱”。就是这么一回事而已:您抛开现实只关心“有效”部分,自然就看到char只要2字节;别人或许关心的是为了存储这1个char在具体环境里它占了多大的坑,自然会追求别的答案。

kaiyuanjava 写道
嗯,确实虚拟机内部不管什么地方,要设计到最优,就应该想好怎么分配坑的问题啊,不过要是真的这样做了,可能是不是还要添加个算法去维护一个坑的隔离问题?
比如一个32位SLOT里面放两个SHORT类型,那么维护局部变量作用域就比较复杂了吧!

如果不是直接在原始字节码上解释执行,而是预处理一下再解释执行或者是干脆编译之后再执行,那原本JVM规范所说的概念中的slot是怎样的根本不重要。例如说在64位体系结构上把4个short类型的局部变量塞到1个64位单元里放在栈上是完全没问题的,也复杂不到哪里去。只不过带来的收益不够大,大家通常没兴趣做这种事情而已。
deyimsf 2013-08-06
非常感谢R大得回答,没想到这个问题还挺复杂
Global site tag (gtag.js) - Google Analytics