[讨论] 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大得回答,没想到这个问题还挺复杂
|