Java对象大小内幕浅析

 最近突发奇想,忽然对Java对象的内存大小感兴趣,去网上搜集了一些资料,并且做一下整理,希望能够各位帮助。
 如果:你能算出new String(“abc”)这个对象在JVM中占用内存大小(64位JDK7中压缩大小48B,未压缩大小64B), 那么看到这里就可以结束了~



Java对象的内存布局:对象头(Header),实例数据(Instance Data)和对齐填充(Padding)
 虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据,如hashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。这部分数据的长度在32位和64的虚拟机(未开启指针压缩)中分别为4B和8B,官方称之为”Mark Word”。
 对象的另一部分是类型指针(klass),即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例。另外如果对象是一个Java数组,那再对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中却无法确定数组的大小。
对象头在32位系统上占用8B,64位系统上占16B。 无论是32位系统还是64位系统,对象都采用8字节对齐。Java在64位模式下开启指针压缩,比32位模式下,头部会大4B(mark区域变位8B,kclass区域被压缩),如果没有开启指针压缩,头部会大8B(mark和kclass都是8B),换句话说,
 HotSpot的对齐方式为8字节对齐:(对象头+实例数据+padding)%8 等于0 且 0<=padding<8。以下说明都是以HotSpot为基准。



 在参考资料2中提到,再JDK5之后提供的java.lang.instrument.Instrumentation提供了丰富的对结构的等各方面的跟踪和对象大小的测量API。但是这个东西需要采用java的agent代理才能使用,至于agent代理和Instrumentation这里就不阐述了,我这里只阐述其使用方式。
 在参考资料3中提供了这个类,个人觉得很实用,代码如下所附1所示(代码比较长,索性就放到文章最后了):
 这段代码可以直接拷贝,然后将其打成jar包(命名为agent.jar,如果没有打包成功,可以直接下载博主打包好的),注意在META-INF/MANIFEST.MF中添加一行:

Premain-Class: com.zzh.size.MySizeOf (注意":"后面的空格,否则会报错:invalid header field.)

 举个案例,代码如下(博主的系统是64位的,采用的是64位的JDK7):

import com.zzh.size.MySizeOf;
public class ObjectSize
{
    public static void  main(String args[])
    {
        System.out.println(MySizeOf.sizeOf(new Object()));
    }
}

 接下来进行编译运行,步骤如下:

  1. 编译(agent.jar放在当前目录下):javac -classpath agent.jar ObjectSize.java
  2. 运行:java -javaagent:agent.jar ObjectSize(输出结果:16,至于这个结果的分析,稍后再阐述)

 JDK6推出参数-XX:+UseCompressedOops,在32G内存一下默认会自动打开这个参数。可以在运行参数中添加-XX:-UseCompressedOops来关闭指针压缩。
 使用Instrumentation来测试对象的大小,只是为了更加形象的表示一个对象的大小,实际上当一个对象建立起来的时候可以手动计算其大小,代码案例实践用来证明理论知识的合理性及正确性,具体算法在下面的代码案例中有所体现。

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

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

 引用类型在32位系统上每个占用4B, 在64位系统上每个占用8B。



案例分析
 扯了这么多犊子,估计看的玄乎玄乎的,来几段代码案例来实践一下。

案例1:上面的new Object()的大小为16B,这里再重申一下,博主测试机是64位的JDK7,如无特殊说明,默认开启指针压缩。

new Object()的大小=对象头12B(8Bmak区,4Bkclass区)+padding的4B=16B

案例2

    static class A{
        int a;
    }
    static class B{
        int a;
        int b;
    }
    public static void  main(String args[])
    {
        System.out.println(MySizeOf.sizeOf(new Integer(1)));
        System.out.println(MySizeOf.sizeOf(new A()));
        System.out.println(MySizeOf.sizeOf(new B()));
    }

输出结果:

(指针压缩) 16    16    24
(指针未压缩)24    24    24

分析1(指针压缩):

new Integer(1)的大小=12B对象头+4B的实例数据+0B的填充=16B
new A()的大小=12B对象头+4B的实例数据+0B的填充=16B
new B()的大小=12B对象头+2*4B的实例数据=20B,填充之后=24B

分析2(指针未压缩):

new Integer(1)的大小=16B对象头+4B的实例数据+4B的填充=24B
new A()的大小=16B对象头+4B的实例数据+4B的填充=24B
new B()的大小=16B对象头+2*4B的实例数据+0B的填充=24B

案例3

System.out.println(MySizeOf.sizeOf(new int[2]));
System.out.println(MySizeOf.sizeOf(new int[3]));
System.out.println(MySizeOf.sizeOf(new char[2]));
System.out.println(MySizeOf.sizeOf(new char[3]));

输出结果:

(指针压缩) 24    32    24    24
(指针未压缩) 32    40    32    32

分析1(指针压缩):

new int[2]的大小=12B对象头+压缩情况下数组比普通对象多4B来存放长度+2*4B的int实例大小=24B
new int[3]的大小=12B对象头+4B长度+3*4B的int实例大小=28B,填充4B =32B
new char[2]的大小=12B对象头+4B长度+2*2B的实例大小=20B,填充4B=24B
new char[3]的大小=12B对象头+4B长度+3*2B的实例大小+2B填充=24B
(PS:new char[5]的大小=32B)

分析2(指针未压缩):

new int[2]的大小=16B对象头+未压缩情况下数组比普通对象多8B来存放长度+2*4B实例大小=32B
new int[3]的大小=16B+8B+3*4B+4B填充=40B
new char[2]的大小=16B+8B+2*2B+4B填充=32B
new char[2]的大小=16B+8B+3*2B+2B填充=32B
(PS:new char[5]的大小为40B)

案例4(sizeOf只计算本体对象大小,fullSizeOf计算本体对象大小和引用的大小,具体可以翻阅附录1的代码).

System.out.println(MySizeOf.sizeOf(new String("a")));
System.out.println(MySizeOf.fullSizeOf(new String("a")));
System.out.println(MySizeOf.fullSizeOf(new String("aaaaa")));

输出结果:

(指针压缩)24    48    56
(指针未压缩)32    64   72  

分析1(指针压缩):

翻看String(JDK7)的源码可以知道,String有这几个成员变量:(static变量属于类,不属于实例,所以声明为static的不计入对象的大小)

private final char value[];
private int hash;
private transient int hash32 = 0;

MySizeOf.sizeOf(new String("a"))的大小=12B对象头+2*4B(成员变量hash和hash32)+4B(压缩的value指针)=24B
MySizeOf.fullSizeOf(new String("a"))的大小=12B对象头+2*4B(成员变量hash和hash32)+4B指针+(value数组的大小=12B对象头+4B数组长度+1*2B实例大小+6B填充=24B)=12B+8B+4B+24B=48B
(PS: new String("aa"),new String("aaa"),new String("aaaa")的fullSizeOf大小都为48B)
MySizeOf.fullSizeOf(new String("aaaaa"))的大小=12B+2*4B+4B+(12B+4B+5*2B+6B填充)=24B+32B=56B

分析2(指针未压缩)

MySizeOf.sizeOf(new String("a"))的大小=16B+2*4B+8B(位压缩的指针大小) =32B
MySizeOf.fullSizeOf(new String("a"))的大小=16B对象头+2*4B(成员变量hash和hash32)+8B指针+(value数组的大小=16B对象头+8B数组长度+1*2B实例大小+6B填充=32B)=32B+32B=64B
(PS: new String("aa"),new String("aaa"),new String("aaaa")的fullSizeOf大小都为64B)
MySizeOf.fullSizeOf(new String("aaaaa"))的大小=16B+2*4B+8B+(16B+8B+5*2B+6B填充)=32B+40B=72B

 这些计算结果只会少不会多,因为在代码运行过程中,一些对象的头部会伸展,mark区域会引用一些外部的空间(轻量级锁,偏向锁,这里不展开),所以官方给出的说明也是,最少会占用多少字节,绝对不会说只占用多少字节。

如果是32位的JDK,可以算一下或者运行一下上面各个案例的结果。

 看来上面的这些我们来手动计算下new String()的大小:
1. 指针压缩的情况

12B对象头+2*4B实例变量+4B指针+(12B对象头+4B数组长度大小+0B实例大小)=24B+16B=40B
  1. 指针未压缩的情况
16B+2*4B+8B指针+(16B+8B数组长度大小+0B)=32B+24B=56B

 所以一个空的String对象最少也要占用40B的大小,所以大家在以后应该编码过程中要稍微注意下。其实也并不要太在意,相信能从文章开头看到这里的同学敲的代码也数以万计了,不在意这些也并没有什么不妥之处,只不过如果如果你了解了的话对于提升自己的逼格以及代码优化水平有很大的帮助,比如:能用基本类型的最好别用其包装类。

附:agent.jar包源码

package com.zzh.size;

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;

public class MySizeOf
{
     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<>();
            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);
        }
}


参考资料:
1.《一个Java对象到底占多大内存?
2.《如何精确地测量java对象的大小
3.《一个对象占用多少字节?
4.《深入理解Java虚拟机》周志明著。

时间: 2024-09-23 07:47:03

Java对象大小内幕浅析的相关文章

如何精确地测量java对象的大小-底层instrument API

关于java对象的大小测量,网上有很多例子,大多数是申请一个对象后开始做GC,后对比前后的大小,不过这样,虽然说这样测量对象的大小是可行的,不过未必是完全准确的,因为过程中包含对象本身的开销,也许你运气好,正好能碰上,差不多,不过这种测试往往显得十分的笨重,因为要写一堆代码才能测试一点点东西,而且只能在本地测试玩玩,要真正测试实际的系统的对象大小这样可就不行了,本文说说java一些比较偏底层的知识,如何测量对象大小,java其实也是有提供方法的.注意:本文的内容仅仅针对于Hotspot VM,如

如何精确地测量java对象的大小

[本文转载于如何精确地测量java对象的大小] 关于java对象的大小测量,网上有很多例子,大多数是申请一个对象后开始做GC,后对比前后的大小,不过这样,虽然说这样测量对象的大小是可行的,不过未必是完全准确的,因为过程中包含对象本身的开销,也许你运气好,正好能碰上,差不多,不过这种测试往往显得十分的笨重,因为要写一堆代码才能测试一点点东西,而且只能在本地测试玩玩,要真正测试实际的系统的对象大小这样可就不行了,本文说说java一些比较偏底层的知识,如何测量对象大小,java其实也是有提供方法的.注

Java对象及元素的存储区域

在JAVA平台上开发应用程序的时候,有一个很大的特点就是其是在应用程序运行的时候才建立对象. 换句话说,在程序运行的时候,才会最终确定对象的归属,即对象应该存储在什么地方.由于存储在不同 的区域,其在性能上会有所不同.为此作为Java程序开发人员需要了解各个存储区域的特点以及对性能的 影响.然后再根据需要来调整应用程序的区域分配.总的来说,在操作系统中有五个地方可以用来保存应 用程序运行中的数据.这类区域的特点以及对性能的影响分析如下. 存储区域一:寄存器 虽然同在内存中,但是不同的区域由于用途

一个Java对象到底占多大内存?(转)

最近在读<深入理解Java虚拟机>,对Java对象的内存布局有了进一步的认识,于是脑子里自然而然就有一个很普通的问题,就是一个Java对象到底占用多大内存? 在网上搜到了一篇博客讲的非常好:http://yueyemaitian.iteye.com/blog/2033046,里面提供的这个类也非常实用: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35

《JVM故障诊断指南》之2 —— 调整合适的Java堆大小的技巧

原文链接 原文作者:Byron Kiourtzoglou 翻译:梅小西(904516706) 在生产系统上决定合适的Java堆大小不是一个容易的操作.许多性能问题的发生都是由于不恰当的Java堆容量的错误调整.这部分将从介绍一些技巧作为开头,它能帮助你在当前的或者新的生产系统上决定最佳的Java堆大小.其中一些技巧对预防OutOfMemoryError问题和内存泄露方面也同样有用. 请注意这些技巧是倾向于"帮助你"决定合适的Java堆大小.因为每一个IT环境都不相同,实际上你是处于最好

HotSpotVM 对象机制实现浅析#1

今天来看下,借助HotSpot SA这个工具,HotSpot VM所实现的对象机制.关于HotSpot SA前面已有几篇博文介绍过了,这里再说一点,SA提供的大多是HotSpot的镜像,所以非常有助于我们理解HotSpotVM,不管是运行时还是具体代码实现. oop 那么HotSpot的对象机制应该从哪扯起呢?oop无疑.oop又是啥? An "oop", or "ordinary object pointer" in HotSpot parlance is a m

Java对象之生

内存.性能是程序永恒的话题,实际开发中关于卡顿.OOM也经常是打不完的两只老虎,关于卡顿.OOM的定位方法和工具比较多,这篇文章也不打算赘述了,本章主要是来整理一下JVM的内存模型以及Java对象的生与死. 生存空间(内存区域) Java程序运行在JVM之上,如果Java对象是一个有血有肉的生灵,那么它生存环境是怎样的呢?很多人把Java内存分为堆内存(Heap)和栈内存(Stack),实际上这种划分比较出粗糙和片面.比较细致的划分是这样的: 分为程序计数器.虚拟机栈.本地方法栈.方法区和堆.

源码分析:Java对象的内存分配

Java对象的分配,根据其过程,将其分为快速分配和慢速分配两种形式,其中快速分配使用无锁的指针碰撞技术在新生代的Eden区上进行分配,而慢速分配根据堆的实现方式.GC的实现方式.代的实现方式不同而具有不同的分配调用层次.  下面就以bytecodeInterpreter解释器对于new指令的解释出发,分析实例对象的内存分配过程: 一.快速分配 1.实例的创建首先需要知道该类型是否被加载和正确解析,根据字节码所指定的CONSTANT_Class_info常量池索引,获取对象的类型信息并调 用is_

JVM源码分析之Java对象的创建过程

本文将基于HotSpot实现对Java对象的创建过程进行深入分析. 定义两个简单的类AAA和BBB 通过"javap -c AAA"`查看编译之后的字节码,具体如下: Java中的new关键字对应jvm中的new指令,定义在InterpreterRuntime类中,实现如下: new指令的实现过程: 1.其中pool是AAA的constant pool,此时AAA的class已经加载到虚拟机中,new指令后面的#2表示BBB类全限定名的符号引用在constant pool的位置: 2.