JAVA 一个对象消耗了多少内存

请先允许我装逼的说一句:当你写代码达到一定境界的时候,你写一句代码,大概你脑子里已经知道了这句代码执行的时候CPU耗时是0.0000几ms ,内存大概消耗了几个byte。

只学不思的人愚,只思不学的人顿。  学而不思则罔,死而不学则殆。

有本书叫 《深入理解Java虚拟机》  推荐大家读一读  !

好啦装逼装完了!

对象内存大小度量

在做内存优化时,需要知道每个对象占用的内存的大小,

一个实例化的对象在内存中需要存储的信息包括:

  1. 对象的头部(对象的GC信息,hash值,类定义引用等)
  2. 对象的成员变量: 包括基本数据类型和引用。 如成员变量是一个引用, 引用了其他对象,被引用的对象内存另外计算。

如下一个简单的类的定义:


class MyClass {
    int a;
    Object object;
}

实例化一个对象:


MyClass myClass = new MyClass();

对象大小分为:

  1. 自身的大小(Shadow heap size)
  2. 所引用的对象的大小(Retained heap size)。

myClass实例创建出来之后,在内存中所占的大小就是myClass自身大小(Shadow
heap size)。包括类的头部大小以及一个int的大小和一个引用的大小。

myClass 中object 成员变量是一个对象引用,这个被引用的对象也占一定大小。myClass实例所维护的引用的对象所占的大小,称为myClass实例的Retained
heap size。

本文讨论的是对象自身的大小,即Shadow heap size。Retained heap size 递归计算即可得。

度量工具

对象大小的计算可用java.lang.instrument.Instrumentation 或者
dump内存之后用memory analyzer分析。 这是一份示例代码java-object-size

基本数据类型大小

基本数据类型大小如下: From WIKI

type size(bits) bytes
boolean 8 1
byte 8 1
char 16 2
short 16 2
int 32 4
long 64 8
float 32 4
double 64 8

引用的大小

在32位的JVM上,一个对象引用占用4个字节;在64位上,占用8个字节。通过 java
-d64 -version
可确定是否是64位的JVM。

使用8个字节是为了能够管理大于4G的内存,如果你的程序不需要访问大于4G的内存,

可通过-XX:+UseCompressedOops选项,开启指针压缩。从Java
1.6.0_23
起,这个选项默认是开的。可通过jinfo
-flag UseCompressedOops <pid>
查看。

localhost:~ srain$ jinfo -flag UseCompressedOops 13133
-XX:+UseCompressedOops

对象头部的大小

对象头,结构如下(来源):

+------------------+------------------+------------------ +---------------.
|    mark word     |   klass pointer  |  array size (opt) |    padding    |
+------------------+------------------+-------------------+---------------‘

每个对象都有一个mark work头部,以及一个引用,指向类的信息。在32位JVM上,mark word 4个字节,整个头部有8字节大小。

在未开启UseCompressedOops的64位JVM上,对象头有16字节大小。

在开启UseCompressedOops的64位机器上,引用成了4字节,一共12字节。
按照8位对齐,实际占用16字节。

对象的内存布局

  1. 每个对象的内存占用按8字节对齐
  2. 空对象和类实例成员变量

    空对象,指的非inner-class,没有实例属性的类。Object 类或者直接继承Object 没有添加任何实例成员的类。

    空对象的不包含任何成员变量,其大小即对象头大小:

    • 在32位JVM上,占用8字节;
    • 在开启UseCompressedOops的64位JVM上,12
      + 4 = 16;
    • 在未开启UseCompressedOops的64位JVM上,16
      + 4 = 20; 对齐后为24。
  3. 对象实例成员重排序

    实例成员变量紧随对象头。每个成员变量都尽量使本身的大小在内存中尽量对齐。

    比如int按4位对齐,long按8位对齐。为了内存紧凑,实例成员在内存中的排列和声明的顺序可能不一致,实际会按以下顺序排序:

    1. doubles and longs
    2. ints and floats
    3. shorts and chars
    4. booleans and bytes
    5. references

    这样做可尽量节省空间。

    如:

    
    
    class MyClass {
        byte a;
        int c;
        boolean d;
        long e;
        Object f;
    }

    未重排之前:

    
    
         32 bit                    64bit +UseCompressedOops
    
    [HEADER: 12 bytes]  8           [HEADER: 12 bytes] 12
    [a:       1 byte ]  9           [a:       1 byte ] 13
    [padding: 3 bytes] 12           [padding: 3 bytes] 16
    [c:       4 bytes] 16           [c:       4 bytes] 20
    [d:       1 byte ] 17           [d:       1 byte ] 21
    [padding: 7 bytes] 24           [padding: 3 bytes] 24
    [e:       8 bytes] 32           [e:       8 bytes] 32
    [f:       4 bytes] 36           [f:       4 bytes] 36
    [padding: 4 bytes] 40           [padding: 4 bytes] 40

    重新排列之后:

    
    
        32 bit                      64bit +UseCompressedOops
    
    [HEADER:  8 bytes]  8           [HEADER: 12 bytes] 12
    [e:       8 bytes] 16           [e:       8 bytes] 20
    [c:       4 bytes] 20           [c:       4 bytes] 24
    [a:       1 byte ] 21           [a:       1 byte ] 25
    [d:       1 byte ] 22           [d:       1 byte ] 26
    [padding: 2 bytes] 24           [padding: 2 bytes] 28
    [f:       4 bytes] 28           [f:       4 bytes] 32
    [padding: 4 bytes] 32

  4. 父类和子类的实例成员

    父类和子类的成员变量分开存放,先是父类的实例成员。父类实例成员变量结束之后,按4位对齐,随后接着子类实例成员变量。

    
    
    class A {
        byte a;
    }
    
    class B extends A {
        byte b;
    }

    内存结构如下:

    
    
        32 bit                  64bit +UseCompressedOops
    
    [HEADER:  8 bytes]  8       [HEADER: 12 bytes] 12
    [a:       1 byte ]  9       [a:       1 byte ] 13
    [padding: 3 bytes] 12       [padding: 3 bytes] 16
    [b:       1 byte ] 13       [b:       1 byte ] 17
    [padding: 3 bytes] 16       [padding: 7 bytes] 24

    如果子类首个成员变量是long或者double等8字节数据类型,而父类结束时没有8位对齐。会把子类的小于8字节的实例成员先排列,直到能8字节对齐。

    
    
    class A {
        byte a;
    }
    
    class B extends A{
        long b;
        short c;
        byte d;
    }

    内存结构如下:

    
    
        32 bit                  64bit +UseCompressedOops
    
    [HEADER:  8 bytes]  8       [HEADER:  8 bytes] 12
    [a:       1 byte ]  9       [a:       1 byte ] 13
    [padding: 3 bytes] 12       [padding: 3 bytes] 16
    [c:       2 bytes] 14       [b:       8 bytes] 24
    [d:       1 byte ] 15       [c:       4 byte ] 28
    [padding: 1 byte ] 16       [d:       1 byte ] 29
    [b:       8 bytes] 24       [padding: 3 bytes] 32

    上面的示例中,在32位的JVM上,B的2个实例成员c, d被提前了。

  5. 非静态的内部类,有一个隐藏的对外部类的引用。

数组的内存占用大小

数组也是对象,故有对象的头部,另外数组还有一个记录数组长度的int类型,随后是每一个数组的元素:基本数据类型或者引用。8字节对齐。

  • 32 位的机器上

    byte[0] 8字节的对象头部,4字节的int长度, 12字节,对齐后是16字节,实际 byte[0] ~ byte[4] 都是16字节。

  • 64 位+UseCompressedOops

    byte[0] 是16字节大小,byte[1] ~ byte[8] 24字节大小。

  • 64 位-UseCompressedOops

    byte[0], 16字节头部,4字节的int长度信息,20字节,对齐后 24 字节。byte[0] ~ byte[4] 都是24字节。

字符串大小

Field Type 64 bit -UseCompressedOops 64 bit +UseCompressedOops 32 bit
HEADER 16 12 8
value char[] 8 4 4
offset int 4 4 4
count int 4 4 4
hash int 4 4 4
PADDING 4 4 0
TOTAL 40 32 24

不计算value引用的Retained heap size, 字符串本身就需要 24 ~ 40 字节大小。

在网上搜到了一篇博客讲的非常好: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;
    }  

    /**
     * 直接计算当前对象占用空间大小,包括当前类及超类的基本类型实例字段大小、<br></br>
     * 引用类型实例字段引用大小、实例基本类型数组总占用空间、实例引用类型数组引用本身占用空间大小;<br></br>
     * 但是不包括超类继承下来的和当前类声明的实例引用字段的对象本身的大小、实例引用数组引用的对象本身的大小 <br></br>
     *
     * @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<Object> visited = new HashSet<Object>();
        Deque<Object> toBeQueue = new ArrayDeque<Object>();
        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<Object> visited, Object obj) {
        if (obj instanceof String && obj == ((String) obj).intern()) {
            return true;
        }
        return visited.contains(obj);
    }
}

参考资料



http://www.codeinstructions.com/2008/12/java-objects-memory-structure.html

http://btoddb-java-sizing.blogspot.com/2012/01/object-sizes.html

http://stackoverflow.com/questions/2120437/object-vs-byte0-as-lock

时间: 2025-01-16 09:08:24

JAVA 一个对象消耗了多少内存的相关文章

Java虚拟机垃圾收集器与内存分配策略

Java虚拟机垃圾收集器与内存分配策略 概述 那些内存需要回收,什么时候回收,如何回收是GC需要完成的3件事情. 程序计数器,虚拟机栈与本地方法栈这三个区域都是线程私有的,内存的分配与回收都具有确定性,内存随着方法结束或者线程结束就回收了. java堆与方法区在运行期才知道创建那些对象,这部分内存分配是动态的,本章笔记中分配与回收的内存指的就是:java堆与方法区. 判断对象已经死了 引用计数算法:给对象添加一个引用计数器,每当有一个地方引用它,计数器+1;引用失败,计数器-1.计数器为0则改判

JAVA对象是如何占用内存的

本文使用的是32位的JVM ,jdk1.6.本文基本是翻译的,加上了一些自己的理解,原文见文章底下链接. 在本文中,我们讨论如何计算或者估计一个JAVA对象占多少内存空间.(注意,使用 Classmexer agent 或者VM insturmentation 可以查询到一个java对象占用了多少内存.) 一般来说,我们讨论一个在堆中的对象的内存,前提是在“正常状态”下.我们忽略下面两种情况. 在某些情况下,JVM 不一定会把对象放到堆中.例如,一个简单的线程本地对象可以存放在栈中. 一个对象占

Java学习之:JVM内存模型

一.文章来由 开始实习啦,实习转战Java开发工程师... 二.JVM内存模型总图 Java中通过多线程机制使得多个任务同时执行处理,所有的线程共享JVM内存区域main memory,而每个线程又单独的有自己的工作内存,当线程与内存区域进行交互时,数据从主存拷贝到工作内存,进而交由线程处理(操作码+操作数). 在之前,我们也已经提到,JVM的逻辑内存模型如下: 三.JVM内存模型详解 1.程序计数器 程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可

Java垃圾回收机制以及内存泄漏

原文地址 前言 在segmentfault上看到一个问题:java有完善的GC机制,那么在java中是否会出现内存泄漏的问题,以及能否给出一个内存泄漏的案例.本问题视图给出此问题的完整答案. 垃圾回收机制简介 在程序运行过程中,每创建一个对象都会被分配一定的内存用以存储对象数据.如果只是不停的分配内存,那么程序迟早面临内存不足的问题.所以在任何语言中,都会有一个内存回收机制来释放过期对象的内存,以保证内存能够被重复利用. 内存回收机制按照实现角色的不同可以分为两种,一种是程序员手动实现内存的释放

二、Java如何分配和回收内存?Java垃圾收集器如何工作?

线程私有的内存区域随用户线程的结束而回收,内存分配编译期已确定,内存分配和回收具有确定性.共享线程随虚拟机的启动.结束而建立和销毁,在运行期进行动态分配.垃圾收集器主要对共享内存区域(堆和方法区)进行垃圾收集回收. Java如何实现内存动态分配和内存垃圾的回收? 1.哪些内存需要回收(垃圾收集器内存回收的对象)?已经"死亡"的对象,那如何判定对象已经"死亡"了? Java堆回收的内存:已经"死亡"的对象 方法区回收的内存:废弃的常量和无用的类 2

用MAT分析JAVA程序运行时的内存使用情况

Java出现OutOfMemoryError或者发现Java应用程序占用的内存很异常,那么我们一般采用下面的步骤分析:A. 把Java应用程序使用的heap dump下来B. 使用Java heap分析工具,找出内存占用超出预期的嫌疑对象C. 根据情况,分析嫌疑对象和其他对象的引用关系.D. 分析程序的源代码,找出嫌疑对象数量过多的原因.以下面的代码为例: public class TObject { int[] arr = new int[20000]; } public class Test

浅析Java的jvm上的内存位置的分配

浅析Java的jvm上的内存位置的分配 1.Java的内存区域简介 1>程序计数器: 一小块的内存空间,每个线程都有一个独立的计数器,线程私有;作用:作为当前线程代码行行号指示器,这个值可以选取下一条需要执行的字节码指令,例如分支,循环等,每创建一根线程会相应的产生一个程序计数器 2>栈 线程私有,用于存放局部变量,保存基本数据类型的值,操作数栈(保存着计算过程的中间结果),动态链接,方法入口和出口等信息:局部变量表中保存着函数的参数和局部变量,当调用结束以后,栈帧销毁,局部变量表也随之销毁

Java堆空间Vs栈内存

之前我写了几篇有关Java垃圾收集的文章之后,我收到了很多电子邮件,请求解释Java堆空间,Java栈内存,Java中的内存分配以及它们之间的区别. 您可能在Java,Java EE书籍和教程中看到很多有关堆和变量内存的参考,但是几乎没有就程序而言完全解释堆和栈的内存分配的. Java堆空间 Java运行时使用Java堆空间为对象和JRE类分配内存.每当我们创建任何对象时,它总是在堆空间中创建. 垃圾回收在堆内存上运行以释放没有任何引用的对象使用的内存.在堆空间中创建的任何对象都具有访问权限,并

Java虚拟机支持的最大内存限制

最近在开发Java的程序.本来我是一直很喜欢Java的内存管理的,不需要担心分配内存,只管分配,垃圾收集器自己会给你回收内存的.现在开发的程序数据量很大,为了速度快,我准备把所有的信息加载进内存,这样可以保证快速响应.我还在反复算内存,想想自己的数据量,现在刚开始的时候应该够了(我的机器是4G内存,虽然Windows就认3.5G,但是比起我现在的数据量应该没问题). 没想到第一个实验的程序,跑了几个小时,就遇到了Out of Memory Exception了.看看自己的虚拟机设置,我设置的是-