加载中…
个人资料
  • 博客等级:
  • 博客积分:
  • 博客访问:
  • 关注人气:
  • 获赠金笔:0支
  • 赠出金笔:0支
  • 荣誉徽章:
正文 字体大小:

Java对象实例大小

(2014-11-04 19:48:05)
标签:

java

对象头

对象大小

分类: Java进阶类

Java对象实例大小

 

第1章     原生类型(primitive type)内存占用

Primitive Type             Memory Required(bytes)

—————————————————————

boolean                      1

byte                            1

short                           2

char                            2

int                               4

float                            4

long                            8

double                        8

第2章      对象内存占用

对象在内存中存储的布局可以分为三块区域:对象头(Header实例数据(Instance Data对齐填充(Padding 

HotSpot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,这部分数据的长度在32位和64位的虚拟机(暂不考虑开启压缩指针的场景)中分别为32个和64Bits,官方称它为“Mark Word”。对象需要存储的运行时数据很多,其实已经超出了3264Bitmap结构所能记录的限度,但是对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。例如在32位的HotSpot虚拟机中对象未被锁定的状态下,Mark Word32Bits空间中的25Bits用于存储对象哈希码(HashCode),4Bits用于存储对象分代年龄,2Bits用于存储锁标志 位,1Bit固定为0,在其他状态(轻量级锁定、重量级锁定、GC标记、可偏向)下对象的存储内容如下表所示。 

http://s14/mw690/003wfAbNgy6NlViyClf6d&690
HotSpot虚拟机对象头Mark Word

1. 一个object header, 也称object overhead, 保存当前实例的type信息和内置monitor信息等, 32位系统上占用8bytes64位系统上占用16bytes

2. 实例fields:reference类型在32位系统上每个占用4bytes, 64位系统上每个占用8bytes; primitive类型参考上面;

3. padding, 对步骤12之和的补长。CPU从内存中读取数据是以word为基本单位, 32位的系统中word宽度为32bits, 64位的系统中word宽度为64bits, 将整个Java对象占用内存补长为word的整倍数大大提高了CPU存取数据的性能,参考维基百科关于数据alignment的说明。 Hotspot而言,不管是32位系统还是64位系统要求(步骤1 + 步骤2 + padding % 8等于00 <= padding < 8。例如在64位系统上:

public class Student {

    private int age;

}

new Student()则其占用内存: 16 + 4 = 20,按照3中的说明则padding4bytes,这样整个内存占用为24bytes

 

第3章     一维原生数组的内存占用

3.1              32位的系统

32位的系统中, 占用内存为:

型别占用内存 * 数组长度 + 8(数组在JVM中被当成特殊的对象, object overhead占用8bytes + 4(数组长度) + padding

如:

byte[2], 型别占用内存,即byte型别占用1byte,数组长度为2,这样占用的总内存为1 * 2 + 8 + 4 = 14padding2bytes16bytes,所以byte[2]占用内存为16bytes

3.2              64位的系统

64位的系统中, 占用内存为:

型别占用内存 * 数组长度 + 16object overhead占用16bytes + 8(数组长度) + padding

如:

byte[2], 型别占用内存,即byte型别占用1byte,数组长度为2,这样占用的总内存为1 * 2 + 16 + 8 = 26padding6bytes26 + 6 = 32bytes,所以byte[2]占用内存为32bytes

 

第4章     多维数组和一维对象数组

4.1              32位的系统

32位的系统中, 占用内存为:

reference占用内存 * 数组第1维长度 +12(数组本身被当做reference8bytes,数组长度占4bytes)

如:

byte[3][7], reference占用内存4byte,数组第1维长度为3,这样占用的总内存为4 * 3 + 12 = 24,所以byte[3][7]占用内存为24bytes

再如byte[7][3], reference占用内存4byte,数组第1维长度为7,这样占用的总内存为4 * 7 + 12 = 40,所以byte[7][3]占用内存为40bytes

再如new HashMap[7][6][4]reference占用内存4byte,数组第1维长度为7,这样占用的总内存为4 * 7 + 12 = 40,所以HashMap[7][6][4]占用内存为40bytes

4.2              64位的系统

64位的系统中, 占用内存为:

reference占用内存 * 数组第1维长度 +24(数组本身被当做reference16bytes,数组长度占8bytes)

如:

byte[3][7], reference占用内存8byte,数组第1维长度为3,这样占用的总内存为8 * 3 + 24 = 48,所以byte[3][7]占用内存为48bytes

第5章     编码计算

java.lang.instrument.Instrumentation实例由JVM产生,我们需实现一个代理(agent),根据java.lang.instrumentpackage specification说明,这个代理里需有个public static void premain(String agentArgs, Instrumentation inst); 方法,这样在JVM初始化后在调用应用程序main方法前,JVM将调用我们agent里的这个premain方法,这样就注入了Instrumentation实例。

计算实例的内存大小,通过Instrumentation#getObjectSize(Object objectToSize)获得。

注意: 如果有field是常量(如, Boolean.FALSE),因为多实例共享,所以算其占用内存为0

如计算对象Deep范围内存占用的话则需递归计算引用对象占用的内存,然后进行累加。

代码实现如下MemoryCalculator.java

 

package charpter.memory;

 

import java.lang.instrument.Instrumentation;

import java.lang.reflect.Array;

import java.lang.reflect.Field;

import java.lang.reflect.Modifier;

import java.util.IdentityHashMap;

import java.util.Map;

import java.util.Stack;

 

 

public final class MemoryCalculator {

 

public static void premain(String agentArgs, Instrumentation inst) {

           instrumentation = inst;

}

 

public static long shallowSizeOf(Object obj) {

           if (instrumentation == null) {

                    throw new IllegalStateException("Instrumentation initialize failed");

           }

           if (isSharedObj(obj)) {

                    return 0;

           }

           return instrumentation.getObjectSize(obj);

}

 

public static long deepSizeOf(Object obj) {

           Map calculated = new IdentityHashMap();

           Stack unCalculated = new Stack();

           unCalculated.push(obj);

           long result = 0;

           do {

                    result += doSizeOf(unCalculated, calculated);

           } while (!unCalculated.isEmpty());

           return result;

}

 

private static boolean isSharedObj(Object obj) {

           if (obj instanceof Comparable) {

                    if (obj instanceof Enum) {

                             return true;

                    } else if (obj instanceof String) {

                             return (obj == ((String) obj).intern());

                    } else if (obj instanceof Boolean) {

                             return (obj == Boolean.TRUE || obj == Boolean.FALSE);

                    } else if (obj instanceof Integer) {

                             return (obj == Integer.valueOf((Integer) obj));

                    } else if (obj instanceof Short) {

                             return (obj == Short.valueOf((Short) obj));

                    } else if (obj instanceof Byte) {

                             return (obj == Byte.valueOf((Byte) obj));

                    } else if (obj instanceof Long) {

                             return (obj == Long.valueOf((Long) obj));

                    } else if (obj instanceof Character) {

                             return (obj == Character.valueOf((Character) obj));

                    }

           }

           return false;

}

 

private static boolean isEscaped(Object obj, Map calculated) {

           return obj == null || calculated.containsKey(obj)

                             || isSharedObj(obj);

}

 

private static long doSizeOf(Stack unCalculated, Map calculated) {

           Object obj = unCalculated.pop();

           if (isEscaped(obj, calculated)) {

                    return 0;

           }

           Class clazz = obj.getClass();

           if (clazz.isArray()) {

                    doArraySizeOf(clazz, obj, unCalculated);

           } else {

                    while (clazz != null) {

                             Field[] fields = clazz.getDeclaredFields();

                             for (Field field : fields) {

                                       if (!Modifier.isStatic(field.getModifiers())

                                                         && !field.getType().isPrimitive()) {

                                                field.setAccessible(true);

                                                try {

                                                         unCalculated.add(field.get(obj));

                                                } catch (IllegalAccessException ex) {

                                                         throw new RuntimeException(ex);

                                                }

                                       }

                             }

                             clazz = clazz.getSuperclass();

                    }

           }

           calculated.put(obj, null);

           return shallowSizeOf(obj);

}

 

private static void doArraySizeOf(Class arrayClazz, Object array,

                    Stack unCalculated) {

           if (!arrayClazz.getComponentType().isPrimitive()) {

                    int length = Array.getLength(array);

                    for (int i = 0; i < length; i++) {

                             unCalculated.add(Array.get(array, i));

                    }

           }

}

 

private static Instrumentation instrumentation = null;

}

 

 

第6章     Compressed oops的内存占用

Compressed oops只在64位的JVM中才会有,另外,在Java SE 6u23之前的1.6版本中需要通过-XX:+UseCompressedOops参数开启。从Java SE 6u23之后的64位版本就默认打开了对象指针压缩

 

压缩算法对64位对象内存占用计算的影响主要在于:

1.         object header,未压缩前由一个native-sized mark word 8bytes加上一个class word 8bytes组成,共16bytes。采用压缩后,class word缩减为4bytes,现共占用12bytes

2.         reference类型,由8bytes缩减为4bytes

3.         数组长度,由8bytes缩减为4bytes

所以,上述测试案例中:

1. 原生类型,内存占用大小不变。

2. 对象类型,object header16bytes变更为12bytesreference类型的fields8bytes变更为4bytesprimitive类型的fields保持不变,padding不变。

3. 一维原生数组,如new byte[2]占用内存的计算公式由:型别占用内存 * 数组长度 + 16 + 8 + padding变更为: 型别占用内存 * 数组长度 + 12 + 4 + padding,这样得到: 1byte * 2 + 12 + 4 = 18padding6bytes等于24bytes

4. 多维数组和一维对象数组,如new byte[3][7],计算公式由: reference占用内存 * 数组第1维长度 +24(数组本身被当做reference16bytes,数组长度占8bytes) 变更为: reference占用内存 * 数组第1维长度 + 16(object header 12bytes,数组长度占4bytes) + padding,这样得到:4bytes * 3 + 16 = 28padding4bytes等于32bytes 再如new HashMap[7]7 * 4bytes + 16 = 44bytespadding4bytes48bytes

第7章     总结

通过上述Java内存占用大小的理论分析与实际测试,给我们实际开发带来几点重要的启发:

1.         同样的程序在不同环境下运行,占用的内存不一样大小,64位系统上占用的内存要比在32位系统上多11.5倍;

2.         n个元素的数组要比n个单独元素占用更大的内存,特别是primitive类型的数组;

3.         定义多维数组时,要尽可能把长度小的放在第1,即int[9][1]要比int[1][9]占用更多内存,Integer[1000][4][3]远比Integer[3][4][1000]占用的内存要多得多;

4.         Java SE 6u23之后的64位版本要比之前的版本在对象内存占用方面小得多。

5.         jvm对于对象会启用对齐优化,我们定义类时field的顺序在运行期会被打乱

6.         开启了压缩指针模式后,Person对象体偏移由 offset = 16变成了 offset = 12

 

所以开启压缩指针模式后,对象头的_klass域得到了压缩,居然变成了32位系统时的长度4字节了,我们都知道32位的长度最多只能表示4G的内存,那么HostSpot 究竟是如何处理的呢

我们引用官方文档:Java HotSpot? Virtual Machine Performance Enhancements

这就是面对对象的好处,我们面对的最小地址单元不是byte,而是object,也就是说在jvm的世界里32位地址表示的不是4GB,而是4G个对象的指针,大概是32GB,解码过程就是把对象指针乘以8加上GC堆的初始地址就能得到操作系统本地64位地址了,编码过程相反

其中启用压指得有操作系统底层的支持:GC堆从虚拟地址0开始分配

进而我们可以得到压指面对的所有场景:

Ø  如果GC堆大小在4G以下,直接砍掉高32位,避免了编码解码过程

Ø  如果GC堆大小在4G以上32G以下,则启用UseCompressedOop

Ø  如果GC堆大小大于32G,压指失效(所以说服务器内存太大不好......

Ø  考虑到内存对齐,Person对象开压指长度为32字节,不开为40字节

0

阅读 收藏 喜欢 打印举报/Report
  

新浪BLOG意见反馈留言板 欢迎批评指正

新浪简介 | About Sina | 广告服务 | 联系我们 | 招聘信息 | 网站律师 | SINA English | 产品答疑

新浪公司 版权所有