一个Java对象到底占用多大内存

做JVM调优,做内存监控与优化,但是一个Java对象到底占用多大内存空间呢?

细探究,Java对象创建的奥秘一文中讲述了Java对象的内存布局:对象头(Header),实例数据(Instance Data)和对齐填充(Padding)。另外:不同的环境结果可能有差异,我所在的环境是HotSpot虚拟机,64位Windwos。

在网上搜到了一篇博客讲的非常好:http://yueyemaitian.iteye.com/blog/2033046
里面有个很实用的类:

import java.lang.instrument.Instrumentation;  
import java.lang.reflect.Array;  
import java.lang.reflect.Field;  
import java.lang.reflect.Modifier;  
import java.util.ArrayDeque;  
import java.util.Deque;  
import java.util.HashSet;  
import java.util.Set;  
  
/** 
 * 对象占用字节大小工具类 
 * 
 * @author tianmai.fh 
 * @date 2014-03-18 11:29 
 */  
public class SizeOfObject {  
    static Instrumentation inst;  
  
    public static void premain(String args, Instrumentation instP) {  
        inst = instP;  
    }  
  
    /** 
     * 直接计算当前对象占用空间大小,包括当前类及超类的基本类型实例字段大小、

* 引用类型实例字段引用大小、实例基本类型数组总占用空间、实例引用类型数组引用本身占用空间大小;

* 但是不包括超类继承下来的和当前类声明的实例引用字段的对象本身的大小、实例引用数组引用的对象本身的大小

* * @param obj * @return */ public static long sizeOf(Object obj) { return inst.getObjectSize(obj); } /** * 递归计算当前对象占用空间总大小,包括当前类和超类的实例字段大小以及实例字段引用对象大小 * * @param objP * @return * @throws IllegalAccessException */ public static long fullSizeOf(Object objP) throws IllegalAccessException { Set visited = new HashSet(); Deque toBeQueue = new ArrayDeque(); toBeQueue.add(objP); long size = 0L; while (toBeQueue.size() > 0) { Object obj = toBeQueue.poll(); //sizeOf的时候已经计基本类型和引用的长度,包括数组 size += skipObject(visited, obj) ? 0L : sizeOf(obj); Class tmpObjClass = obj.getClass(); if (tmpObjClass.isArray()) { //[I , [F 基本类型名字长度是2 if (tmpObjClass.getName().length() > 2) { for (int i = 0, len = Array.getLength(obj); i < len; i++) { Object tmp = Array.get(obj, i); if (tmp != null) { //非基本类型需要深度遍历其对象 toBeQueue.add(Array.get(obj, i)); } } } } else { while (tmpObjClass != null) { Field[] fields = tmpObjClass.getDeclaredFields(); for (Field field : fields) { if (Modifier.isStatic(field.getModifiers()) //静态不计 || field.getType().isPrimitive()) { //基本类型不重复计 continue; } field.setAccessible(true); Object fieldValue = field.get(obj); if (fieldValue == null) { continue; } toBeQueue.add(fieldValue); } tmpObjClass = tmpObjClass.getSuperclass(); } } } return size; } /** * String.intern的对象不计;计算过的不计,也避免死循环 * * @param visited * @param obj * @return */ static boolean skipObject(Set visited, Object obj) { if (obj instanceof String && obj == ((String) obj).intern()) { return true; } return visited.contains(obj); } }

大家可以用这个代码边看边验证,注意的是,运行这个程序需要通过javaagent注入Instrumentation,具体可以看原博客。我今天主要是总结下手动计算Java对象占用字节数的基本规则,下面详细分析一下。

【1】对象头

计算公式:

Class 模板对象头 = Mark World(1字宽)+Class Metadata Address(1字宽)
+Array length(1字宽);

字宽就是字长,32位机上一个字长32位(4字节),64位机上一个字长64位(8字节)。

普通对象头(非数组)在32位系统上占用8bytes,64位系统上占用16bytes。数组对象头则分别为12字节和24字节。
在这里插入图片描述
空对象是只有对象头,没有实例数据,也无需填充对齐。因为所有的Java非基本类型的对象都需要默认继承Object对象,因此不论什么样的Java对象,其大小都必须是大于8byte(16byte)

在32位机上包装类型的大小至少是12byte(声明一个空Object至少需要的空间),而且12byte没有包含任何有效信息,同时,因为Java对象大小是8的整数倍,因此一个基本类型包装类的大小至少是16byte。

这个内存占用是很恐怖的,它是使用基本类型的N倍(N>2),有些类型的内存占用更是夸张(随便想下就知道了)。因此,可能的话应尽量少使用包装类。在JDK5.0以后,因为加入了自动类型装换,因此,Java虚拟机会在存储方面进行相应的优化。

【2】实例数据

原生类型(primitive type)的内存占用如下:

Primitive Type Memory Required(bytes)
boolean 1
byte 1
short 2
char 2
int 4
float 4
long 8
double 8

reference类型在32位系统上每个占用4bytes(一个字长), 在64位系统上每个占用8bytes(一个字长)

Integer a = new Integer();

【3】对齐填充

HotSpot的对齐方式为8字节对齐

(对象头 + 实例数据 + padding) % 8等于0,且0 <= padding < 8

【4】指针压缩

对象占用的内存大小收到VM参数UseCompressedOops的影响。

① 对对象头的影响

开启压缩(-XX:+UseCompressedOops)对象头大小为12bytes(64位机器,不开启压缩则为16字节,开启压缩少了4字节-半个字长)。

static class A {
    int a;//4字节
}

A对象占用内存情况:

  • 关闭指针压缩: 16+4=20不是8的倍数,所以+padding/4=24
    在这里插入图片描述
    这里也可以看到无实例数据和对齐补充的Object实例对象在64位机上内存大小–16字节sizeOf(new Object())=16

  • 开启指针压缩: 12+4=16已经是8的倍数了,不需要再padding。
    在这里插入图片描述

② 对reference类型的影响

64位机器上reference类型占用8个字节,开启指针压缩后占用4个字节,少半个字长

static class B2 {
        int b2a;//基本类型4字节
        Integer b2b;//引用类型在64bit机上为8字节
}

B2对象占用内存情况:

  • 关闭指针压缩:16+4+8=28不是8的倍数,所以+padding/4=32
    一个Java对象到底占用多大内存_第1张图片

  • 开启指针压缩: 12+4+4=20不是8的倍数,所以+padding/4=24
    一个Java对象到底占用多大内存_第2张图片

【5】数组对象

64位机器上,数组对象的对象头占用24个字节,启用压缩之后占用16个字节,少了一个字长。之所以比普通对象占用内存多是因为需要额外的空间存储数组的长度。

先考虑下new Integer[0]占用的内存大小,长度为0,即是对象头的大小:

  • 未开启压缩:24bytes
    在这里插入图片描述
  • 开启压缩后:16bytes
    在这里插入图片描述

接着计算new Integer[1],new Integer[2],new Integer[3]和new Integer[4]就很容易了:

  • 未开启压缩:
    在这里插入图片描述
  • 开启压缩:
    在这里插入图片描述
    拿new Integer[3]来具体解释下:

未开启压缩:24(对象头)+8*3=48,不需要padding;

开启压缩:16(对象头)+3*4=28,+padding/4=32,其他依次类推。

【6】复合对象

计算复合对象占用内存的大小其实就是运用上面几条规则,只是麻烦点。

① 对象本身的大小

直接计算当前对象占用空间大小,包括当前类及超类的基本类型实例字段大小、引用类型实例字段引用大小、实例基本类型数组总占用空间、实例引用类型数组引用本身占用空间大小。但是不包括超类继承下来的和当前类声明的实例引用字段的对象本身的大小、实例引用数组引用的对象本身的大小。

static class B {
        int a;
        int b;
    }
static class C {
        int ba;
        B[] as = new B[3];
        C() {
            for (int i = 0; i < as.length; i++) {
                as[i] = new B();
            }
        }
    }

C对象占用内存大小:

  • 未开启压缩:16(对象头)+4(ba)+8(as引用的大小)+padding/4=32
  • 开启压缩:12+4+4+padding/4=24

② 当前对象占用的空间总大小

递归计算当前对象占用空间总大小,包括当前类和超类的实例字段大小以及实例字段引用对象大小。

递归计算复合对象占用的内存的时候需要注意的是:对齐填充是以每个对象为单位进行的,看下面这个图就很容易明白。
一个Java对象到底占用多大内存_第3张图片

现在我们来手动计算下C对象占用的全部内存是多少,主要是三部分构成:C对象本身的大小+数组对象的大小+B对象的大小。

  • 未开启压缩:

(16 + 4 + 8+4(padding)) + (24+ 8*3) +(16+8)*3 = 152bytes

  • 开启压缩:

(12 + 4 + 4 +4(padding)) + (16 + 4*3 +4(数组对象padding)) + (12+8+4(B对象padding))*3= 128bytes

参考博文:
细探究,Java对象创建的奥秘
http://www.cnblogs.com/magialmoon/p/3757767.html

你可能感兴趣的:(Java对象内存占用,深入浅出JVM,深入浅出JVM)