您的位置:首页 > 职场人生

java特种兵读书笔记(3-4)——java程序员的OS之对象内存结构

2016-01-13 11:15 525 查看
原始类型与对象的拆装箱

boolean,byte,char,short,int,long,float,double都有一个装箱后的类型(Wrapper)与之对应。

JDK1.5之后它们可以互相转换,自动装箱对int来说使用Integer.valueOf()来完成。这一操作在编译期完成。

int []values = new int[1024];

for(...){value[i] = i;}

Integer []values2 = new Integer[1024];

for(...){value[i] = i;}

这两段代码的开销是不同的。

第一段代码只有一个数组,数组中每个元素就存放一个int值。

第二段代码中的数组存放的Integer对象的引用,除了数组中引用空间的大小,还要算上所有Integer对象所占据的内存——数组本身所占空间+1024个Integer对象所占据的空间。

对象内存结构

1.对象的描述——字节码

一个对象本身的内在结构需要一种描述方式,这个描述信息是以字节码的形式存储在方法区中的。

形式:字节码。

存储位置:方法区。

Class本身就是一个对象,之前总结过。Class以KB为单位。如果new Integer()为了表示一个数字,就占用KB级别的内存显然是无法接收的。因为一个int才4个字节(4B,一个Integer需要nKB?搞笑么?!)

如果使用字节码增强技术,对象结构发生改变(字节码改变,对象结构就是字节码表示的),是不是让所有对象都感知到。为了表示对象属性和方法等信息,是需要这个结构描述(Class字节码)。

2.对象头信息

HotSpot VM使用对象头部的一个指针,指向Class区域(在Perm代中)的方式来找到对象的描述,内部方法,以及属性入口。

对象头信息还包括:是否加锁,GC标志位,MinorGC次数,对象默认的hashcode(用System.identityHashCode(o)来获得)。

存储这些信息其实不需要太多空间,类似Class存储方法属性的标识那样,一个二进制位就可以表达是与否,这部分空间通常叫做mark word。

32位系统下,存放Class指针的空间大小是4字节。Mark Word空间大小也是4字节。因此头部空间是8字节。对于数组来说,还需要4个字节来表示数组的长度。

64位系统,开启指针压缩,指针所占空间不变,MarkWord空间大小变为8字节(所以头部最少需要12个字节)。

若未开启指针压缩,那么指向Class的指针需要8个字节,头部最少需要16字节。另外,如果没有开启指针压缩,64位系统的引用也是8个字节。

3.指针压缩

用算法开销节约内存。java对象是以8个字节对齐的——以8字节为内存访问的基本单元。在地址处理上有3个位是空闲的。这3个位可以用来虚拟。原先32位的地址指针可以寻址4G(4个字节,2的32次方4294967296,可以表示这么多个地址,即4G多个地址),加上这3位的8中内部运算,寻址可以达到32G(4G*8)。

补充:32位机器,有36根地址线,可用物理地址是2的36次方,即64G,但是可用地址空间是4G。

如果一个对象占用空间不是8字节的倍数,会被自动补齐为8字节的倍数,这样对象分配和查找过程中不要太多的考虑偏移量的问题。

对象空间

以32位系统为例,new Object()操作(此时该对象没有任何属性),JVM会分配8个字节的空间。128个对象会占用1KB空间(1024个字节)。包含MarkWord,指向Class的指针,对象的body部分以及对齐部分。

如果是Integer,那么对象里包含一个int,占用4字节,指向Class的指针4个字节,MarkWord四个字节,一共是12个字节。根据java的8字节对齐,要对齐到16给字节,所以对齐部分占用4个字节,共16个字节。

对比上面的数组例子,第二个数组会多出来16*1024个字节(即16KB)的开销。

原始的数组(数组对象,数组也是对象!!是new出来的)的开销:8字节头部(Class指针和MarkWord),4字节描述数组长度,1024*4个字节存放数组元素(1024个元素,每个元素是int,占4字节),对齐用4字节。即4KB+16字节。

第二个数组占用16KB(1000个Integer对象所占空间)+16字节+4KB(4KB的解释:忽略-128~127的integer缓存,1024个元素,每个元素都是对Integer对象的引用,引用占4个字节,因为java引用是用C++指针实现的,指针占4个字节,引用也占四个字节),是第一个数组所占空间的近5倍。

可以看出不同的写法对内存的消耗是由很大区别的!

除此之外,一个对象除了body所占用的空间,其他的部分占用的空间也是不可忽视的,比如头部分和补齐部分。单个对象或许差别不大,但是对象一旦多起来,多占用的那些空间也是不可忽视的。

无继承的对象属性

默认情况HotSpotVM会按照一个顺序排布对象的内部属性。long/double->int/float->short/char->byte/boolean->Reference(与对象本身的属性顺序无关)。可以看出,对于普通类型,java按照宽度从大到小的顺序排列,注意,对象的引用不是最小的,放在最后可能是为了java统一处理。

例1:

class A{byte b1;}

32位系统,头部占用8个字节,body部分的byte占1个字节,对齐占7个字节,一共需要16个字节。

class A{byte b1;byte b2....byte b8;}

一共8个属性,还是16个字节,只不过对齐部分不需要占用字节了。

class A{byte b1;byte b2....byte b9;}

9个属性,实际的body占17个字节,对齐还是要占7个字节,一共24个字节。

例2:

class A{int i;String str;}

头部8个字节,一个int4个字节,一个String引用4个字节,还是16个字节。

例3:

class A{int i;byte b;String str;}

24字节,但是补齐方式不太相同。int占4字节,byte占1字节,这时候剩3字节,啥都放不下,这里就先补3字节。然后放String引用4字节,最后再补4字节。一共补充了7字节,但是是分开补充的。

静态属性:

静态属性所占的空间通常不计算到对象本身的空间上,因为它在方法区,所以在计算body体中有多少东西时,要抛开这些静态元素。

有继承的对象属性

父类的属性不能和子类混用,它们必须单独排布在一个地方。可以认为它们就是从上到下的一个顺序。

class A{byte b;}

class B extends A{byte b;}

class C extends B{byte b;}

生成一个对象c,C.b,B.b和A.b,三个byte,但是每个byte都要做内部4字节对齐,这就是12个字节,加上头部的8个字节,一共20个字节,补齐需要4个字节,所以一共占用24个字节。

对象嵌套

对象内部有引用指向了其他对象,并且是一层套一层。

在早期的GC使用引用计数法来回收,对循环嵌套是毫无办法的。现在GC只找活着的,没有了这个问题。

嵌套层次越深,中间浪费的内存越多。例如Integer数组,真正又用的是内部的int,数组只是一个载体,也就是说数组这个空间是多出来的,并不是真正的数据。

二维数组浪费更多,因为第一维存储的是数组的引用。而第二维数组中存储的才是实际对象的引用。这其中浪费了很多空间。可以说数组维数越多,浪费的空间越多。

这在集合类中更为明显,尤其是集合类嵌套集合类。

String

new String()时,

public String{this.offset=0;this.count=0;this.value=new char[0];}// 在JDK1.6

public String{this.value=new char[0];}// 在JDK1.6

申请了一个char数组,虽然长度为0,但是同样占用数组的空间。

再看看它包括的非static属性,先看1.6:

private final char value[];

private final int offset;

private final int count;

private int hash;

再看1.7:

private final char value[];

private int hash;

private transient int hash32 = 0;

对1.6来说,value数组引用占4字节,3个int占12字节,头部8字节,共24字节。value数组对象头部8字节,数组长度4字节,字节填充4字节,共16字节。加起来共40字节。

对1.7来说,value数组引用占4字节,2个int占8字节,头部8字节,加上字节补充,供24字节。所以和1.6情况是一样的。

集合类

HashMap

HashMap对象->Entry数组->Entry链表->Entry对象->找到value。其实value才是真实需要的内容。

ArrayList

ArrayList会好一些,和String类似,将一个对象用数组来存储。到目前为止,所有基于数组结构的结合类,都逃不出内存碎片和复制拷贝的问题。

内存拷贝和垃圾

单行的String的+操作会变成StringBuilder的拼接,最后toString()。这过程中间产生了大量的垃圾和碎片。碎片和垃圾导致了频繁的GC。

StringBuilder内部也存在大量的内存拷贝(只是不会像String内部那样疯狂)。同样,对于数组类的集合,内部也有大量的数组拷贝。可控的话也可以在代码中初始化大小,避免内存浪费。

使用集合类经常做删除添加操作的话,考虑使用链表或者Tree结构。

int[2][100]和int[100][2]

new int[2][100]

第一维数组的长度是2(有两个元素)。占用空间,头部8字节,数组长度描述4字节,两个引用8字节(第一维数组的每一个元素都是指向第二维数组的引用),填充需要4个字节,一共24字节。

最多两个子数组,每个子数组长度,头部8字节,数组长度描述4字节,数组元素100个int占400字节,填充4字节,一共416字节。

所有总空间是24+416*2 = 856字节

new int[100][2]

第一维数组长度是100(有100个元素)。占用空间,头部8字节,数组长度描述4字节,100个引用占400字节,填充占4字节,一共416字节。

最多100个子数组,每个子数组长度,头部8字节,数组长度描述4字节,数组元素2个int占8字节,占位4字节,共24字节。

所有总空间是416+100*24 = 2816字节。

两者占用空间差距接近3倍。第二个二维数组的空间都浪费在了第一维的引用上,导致第二维子数组有100个,但是每个子数组元素仅有2个。

要点:第一维数组长度不要太大,第二维数组长度尽量大一些。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: