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

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

首先,我们先写一段大家可能不怎么写或者认为不可能的代码:一个类中,几个类型都是private类型,没有public方法,如何对这些属性进行读写操作,看似不可能哦,为什么,这违背了面向对象的封装,其实在必要的时候,留一道后门可以使得语言的生产力更加强大,对象的序列化不会因为没有public方法就无法保存成功吧,OK,我们简单写段代码开个头,逐步引入到怎么样去测试对象的大小,一下代码非常简单,相信不用我解释什么:

import java.lang.reflect.Field;
class NodeTest1 {
    private int a = 13;
    private int b = 21;
} 

public class Test001 {
    public static void main(String []args) {
        NodeTest1 node = new NodeTest1();
        Field []fields = NodeTest1.class.getDeclaredFields();
        for(Field field : fields) {
            field.setAccessible(true);
            try {
                int i = field.getInt(node);
                field.setInt(node, i * 2);
                System.out.println(field.getInt(node));
            } catch (IllegalArgumentException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }
}

代码最基本的意思就是:实例化一个NodeTest1这个类的实例,然后取出两个属性,分别乘以2,然后再输出,相信大家会认为这怎么可能,NodeTest1根本没有public方法,代码就在这里,将代码拷贝回去运行下就OK了,OK,现在不说这些了,运行结果为:

26
42

为什么可以取到,是每个属性都留了一道门,主要是为了自己或者外部接入的方便,相信看代码自己仔细的朋友,应该知道门就在:field.setAccessible(true);代表这个域的访问被打开,好比是一道后门打开了,呵呵,上面的方法如果不设置这个,就直接报错。

看似和对象大小没啥关系,不过这只是抛砖引玉,因为我们首先要拿到对象的属性,才能知道对象的大小,对象如果没有提供public方法我们也要知道它有哪些属性,所以我们后面多半会用到这段类似的代码哦!

对象测量大小的方法关键为java提供的(1.5过后才有):java.lang.instrument.Instrumentation,它提供了丰富的对结构的等各方面的跟踪和对象大小的测量的API(本文只阐述对象大小的测量方法),于是乎我心喜了,不过比较恶心的是它是实例化类:sun.instrument.IntrumentationImpl是sun开头的,这个鬼东西有点不好搞,翻开源码构造方法是private类型,没有任何getInstance的方法,写这个类干嘛?看来这个只能被JVM自己给初始化了,那么怎么将它自己初始化的东西取出来用呢,唯一能想到的就是agent代理,那么我们先抛开代理,首先来写一个简单的对象测量方法:

步骤1:(先创建一个用于测试对象大小的处理类)

import java.lang.instrument.Instrumentation;
public class MySizeOf {
        private static Instrumentation inst;
        /**
         *这个方法必须写,在agent调用时会被启用
         */
        public static void premain(String agentArgs, Instrumentation instP) {
            inst = instP;
        }

        //用来测量java对象的大小(这里先理解这个大小是正确的,后面再深化)
        public static long sizeOf(Object o) {
            if(inst == null) {
                throw new IllegalStateException("Can not access instrumentation environment.\n" +
                    "Please check if jar file containing SizeOfAgent class is \n" +
                    "specified in the java‘s \"-javaagent\" command line argument.");
            }
            return inst.getObjectSize(o);
        }
}

步骤2:上面我们写好了agent的代码,此时我们要将上面这个类编译后打包为一个jar文件,并且在其包内部的META-INF/MANIFEST.MF文件中增加一行:Premain-Class: MySizeOf代表执行代理的全名,这里的类名称是没有package的,如果你有package,那么就写全名,我们这里假设打包完的jar包名称为agent.jar(打包过程这里简单阐述,就不细说了),OK,继续向下走:

步骤3:编写测试类,测试类中写:

public class TestSize {
        public static void main(String []args) {
            System.out.println(MySizeOf.sizeOf(new Integer(1)));
            System.out.println(MySizeOf.sizeOf(new String("a")));
            System.out.println(MySizeOf.sizeOf(new char[1]));
        }
}

下一步准备运行,运行前我们准备初步估算下结果是什么,目前我是在32bit模式下运行jvm(注意,不同位数的JVM参数设置不一样,对象大小也不一样大)。

(1) 首先看Integer对象,在32bit模式下,class区域占用4byte,mark区域占用最少4byte,所以最少8byte头部,Integer内部有一个int类型的数据,占4个byte,所以此时为8+4=12,java默认要求按照8byte对象对其,所以对其到16byte,所以我们理论结果第一个应该是16;
(2) 再看String,长度为1,String对象内部本身有4个非静态属性(静态属性我们不计算空间,因为所有对象都是共享一块空间的),4个非静态属性中,有offset、count、hash为int类型,分别占用4个byte,char value[]为一个指针,指针的大小在bit模式下或64bit开启指针压缩下默认为4byte,所以属性占用了16byte,String本身有8byte头部,所以占用了24byte;其次,一个String包含了子对象char数组,数组对象和普通对象的区别是需要用一个字段来保存数组的长度,所以头部变成12byte,java中一个char采用UTF-16编码,占用2个byte,所以是14byte,对其到16byte,24+16=40byte;
(3) 第三个在第二个基础上已经分析,就是16byte大小;

也就是理论结果是:16、40、16;

步骤4:现在开始运行代码:运行代码前需要保证classpath把刚才的agent.jar包含进去:

D:>javac TestSize.java
D:>java -javaagent:agent.jar TestSize
16
24
16

第一个和第三个结果一致了,不过奇怪了,第二个怎么是24,不是40,怎么和理论结果偏差这么大,再回到理论结果中,有一个24曾经出现过,24是指String而不包含char数组的空间大小,那么这么算还真是对的,可见,java默认提供的方法只能测量对象当前的大小,如果要测量这个对象实际的大小(也就是包含了子对象,那么就需要自己写算法来计算了,最简单的方法就是递归,不过递归一项是我不喜欢用的,无意中在一个地方看到有人用栈写了一个代码写得还不错,自己稍微改了下,就是下面这种了)。

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 class MySizeOf {  

    static Instrumentation inst;  

    public static void premain(String agentArgs, Instrumentation instP) {
       inst = instP;
    }  

    public static long sizeOf(Object o) {
       if(inst == null) {
          throw new IllegalStateException("Can not access instrumentation environment.\n" +
             "Please check if jar file containing SizeOfAgent class is \n" +
             "specified in the java‘s \"-javaagent\" command line argument.");
       }
       return inst.getObjectSize(o);
    }  

    public static long fullSizeOf(Object obj) {//深入检索对象,并计算大小
       Map<Object, Object> visited = new IdentityHashMap<Object, Object>();
       Stack<Object> stack = new Stack<Object>();
       long result = internalSizeOf(obj, stack, visited);
       while (!stack.isEmpty()) {//通过栈进行遍历
          result += internalSizeOf(stack.pop(), stack, visited);
       }
       visited.clear();
       return result;
    }
    //判定哪些是需要跳过的
    private static boolean skipObject(Object obj, Map<Object, Object> visited) {
       if (obj instanceof String) {
          if (obj == ((String) obj).intern()) {
             return true;
          }
       }
       return (obj == null) || visited.containsKey(obj);
    }  

    private static long internalSizeOf(Object obj, Stack<Object> stack, Map<Object, Object> visited) {
       if (skipObject(obj, visited)) {//跳过常量池对象、跳过已经访问过的对象
           return 0;
       }
       visited.put(obj, null);//将当前对象放入栈中
       long result = 0;
       result += sizeOf(obj);
       Class <?>clazz = obj.getClass();
       if (clazz.isArray()) {//如果数组
           if(clazz.getName().length() != 2) {// skip primitive type array
              int length =  Array.getLength(obj);
              for (int i = 0; i < length; i++) {
                 stack.add(Array.get(obj, i));
              }
           }
           return result;
       }
       return getNodeSize(clazz , result , obj , stack);
   }  

   //这个方法获取非数组对象自身的大小,并且可以向父类进行向上搜索
   private static long getNodeSize(Class <?>clazz , long result , Object obj , Stack<Object> stack) {
      while (clazz != null) {
          Field[] fields = clazz.getDeclaredFields();
          for (Field field : fields) {
              if (!Modifier.isStatic(field.getModifiers())) {//这里抛开静态属性
                   if (field.getType().isPrimitive()) {//这里抛开基本关键字(因为基本关键字在调用java默认提供的方法就已经计算过了)
                       continue;
                   }else {
                       field.setAccessible(true);
                      try {
                           Object objectToAdd = field.get(obj);
                           if (objectToAdd != null) {
                                  stack.add(objectToAdd);//将对象放入栈中,一遍弹出后继续检索
                           }
                       } catch (IllegalAccessException ex) {
                           assert false;
                  }
              }
          }
      }
      clazz = clazz.getSuperclass();//找父类class,直到没有父类
   }
   return result;
  }
}

修改测试类:

public class TestSize {
   public static void main(String []args) {
     System.out.println(MySizeOf.sizeOf(new Integer(1)));
     System.out.println(MySizeOf.sizeOf(new String("a")));
     System.out.println(MySizeOf.fullSizeOf(new String("a")));
     System.out.println(MySizeOf.sizeOf(new char[1]));
   }
}

D:>javac TestSize.java
D:>java -javaagent:agent.jar TestSize
16
24
40
16

这个结果是我们想要的了,看来这个测试是靠谱的,面对理论和测试结果,以及上面所谓的对齐方法,大家可以自己编写一些类的对象来测试大小看时候和实际的保持一致;

最后,文章补充一些:

  1. 对象采用8字节对齐的方式是不论32bit还是64bit都是一样的;
  2. Java在64bit模式下开启指针压缩,比32bit模式下,头部会大4byte(mark区域变成8byte,class区域被压缩),如果没有开启指针压缩,头部会大8byte(_mark和_class都会变成8byte),jdk1.6推出参数-XX:+UseCompressedOops,在32G内存一下默认会自动打开这个参数,如下:

    [[email protected] ~]$ java -Xmx31g -XX:+PrintFlagsFinal |grep Compress
    bool SpecialStringCompress = true {product}
    bool UseCompressedOops := true {lp64_product}
    bool UseCompressedStrings = false {product}
    [[email protected] ~]$ java -Xmx32g -XX:+PrintFlagsFinal |grep Compress
    bool SpecialStringCompress = true {product}
    bool UseCompressedOops = false {lp64_product}
    bool UseCompressedStrings = false {product}

简单计算一个,在指针压缩的情况下,一个new String(“a”);这个对象的空间大小为:12字节头部+4*4 = 28字节对齐到32字节,然后c所指向的char数组头部比普通对象多4个byte来存放长度,12+4+2byte的字符=16,也就是48个byte,其实即使你new String()也会占这么大的空间,因为有对齐,如果字符的长度是8个,那么就是12+4+16=32,也就是有64byte;

如果不开启指针压缩再算算:头部变成16byte + 4*3个int数据 + 8(1个指针) = 36对齐到40byte,对应的char数组的头部变成16+4 + 2 = 22对齐到24byte,40+24=64,也就是只有一个字符或者0个字符都会对齐到64byte,所以,你懂的,参数该怎么调,代码该怎么写,如果长度为8个字符的那么后面部分就会变成16+4+16=36对齐到40byte,40+40=80byte,也就是说,抛开其他的引用空间(比如通过数组或集合类引用),如果你有10来个String,每个大小就装8个字符,就会有1K的大小,你的代码里头有多少?呵呵!

这些不是我说的,这些是一种计算方法,而且这个计算结果只会少不会多,因为代码运行过程中,一些对象的头部会伸展,_mark区域装不下会用外部的空间来存放,所以官方给出的说明也是,最少会占用多少字节,绝对不会说只占用多少字节。

OK,说得挺吓人的,不过写代码还是不要怕,不过就这些而言,只是说明java是如何浪费空间的,不要一味使用一些高级的东西,在必要的时候,考虑性能还是有很大的空间,类似集合类以及多维数组,前面的引用其实和数据一点关系都没有,但是占用的空间比数据本身都要大很多。

时间: 2024-08-27 18:19:58

如何精确地测量java对象的大小的相关文章

JVM —— Java 对象占用空间大小计算

零. 为什么要知道 Java 对象占用空间大小 缓存的实现: 在设计 JVM 内缓存时(不是借助 Memcached. Redis 等), 需要知道缓存的对象是否会超过 JVM 最大堆限制, 如果会超过要设置相应算法如 LRU 来丢弃一部分缓存数据以满足后续内容的缓存 JVM 参数设置: 如果知道对象会被创建, 可以帮助判断 -Xmx 需要设置多少 只是为了好玩 一. 对象的内存布局 HotSpot 虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header).实例数据(Instan

如何准确计算Java对象的大小

     有时,我们需要知道Java对象到底占用多少内存,有人通过连续调用两次System.gc()比较两次gc前后内存的使用量在计算java对象的大小,也有人根据Java虚拟机规范中的Java对象内存排列估算对象的大小,这两种方法或多或少都有问题,因为System.gc()并不一定促发GC,同一个类型的对象在32位与64位JVM中使用的内存会不一样,在64位虚拟机中是否开启指针压缩也会影响Java对象在内存中的大小. 那么有没有一种既准确又方便的方法计算对象的大小呢?答案是肯定的.在Java

java对象的大小

原文出处:http://www.open-open.com/lib/view/open1423111722764.html 原文出处: cnblogs-zhanjindong 最近在读<深入理解Java虚拟机>,对Java对象的内存布局有了进一步的认识,于是脑子里自然而然就有一个很普通的问题,就是一个Java对象到底占用多大内存? 在网上搜到了一篇博客讲的非常好:http://yueyemaitian.iteye.com/blog/2033046,里面提供的这个类也非常实用: import j

java调优随记-java对象大小

在java中,基本数据类型的大小是固定.但是java对象的大小是不固定的,需要通过计算. 在java中,一个空对象(没有属性和方法的对象)在堆中占用8byte,比如 Object obj = new Object();另外栈中存储引用需要占用4byte的空间,总共需要16byte空间(喂,为为什么不是12byte?因为java在内存分配的时候都是以8的倍数在分配).在java中所有的对象都继承Object,所以不论什么样的对象大小都不能小于8byte. 计算一下下面的对象的大小? Class O

获取JAVA对象占用的内存大小

介绍两种获取JAVA对象内存大小的方法. 第一种:Instrumentation 简介: 使用java.lang.instrument 的Instrumentation来获取一个对象的内存大小.利用Instrumentation并且通过代理我们可以监测在JVM运行的程序的功能,它的原理是修改方法的字节码. 首先创建代理类 package com.dingtongblog.size; import java.lang.instrument.Instrumentation; public class

Java对象的内存布局以及对象的访问定位

先来看看Java对象在内存中的布局 一 Java对象的内存布局 在HotSpot虚拟机中,对象在内存中的布局分为3个区域 对象头(Header) Mark Word(在32bit和64bit虚拟机上长度分别为32bit和64bit)存储对象自身的运行时数据,包括哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时 间戳等 类型指针 即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例.但是并不是所有类型虚拟机实现都必须在对象数据上保留类型指针,如果对象是一

【深入理解JVM】:Java对象的创建、内存布局、访问定位

对象的创建 一个简单的创建对象语句Clazz instance = new Clazz();包含的主要过程包括了类加载检查.对象分配内存.并发处理.内存空间初始化.对象设置.执行ini方法等. 主要流程如下: 1. 类加载检查 JVM遇到一条new指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载.解析和初始化过.如果没有,那必须先执行相应的类的加载过程. 2. 对象分配内存 对象所需内存的大小在类加载完成后便完全确定(对象内存布局),

Java中计算对象的大小

一.计算对象大小的方法 Java中如何计算对象的大小呢,找到了4种方法: 1.java.lang.instrument.Instrumentation的getObjectSize方法: 2.BTraceUtils的sizeof方法: 3.http://yueyemaitian.iteye.com/blog/2033046中提供的代码计算: 4.https://github.com/mingbozhang/memory-measurer提供的工具包: 本质上java.lang.instrument

java对象占用内存大小计算方式

案例一: User public class User { } UserSizeTest public class UserSizeTest { static final Runtime runTime=Runtime.getRuntime(); public static void main(String[] args) { final int count = 100000; User[] us=new User[count]; long heap1 = 0; for (int i = -1;