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

简介:

关于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本身有8直接头部,所以占用了24byte;其次,一个String包含了子对象char数组,数组对象和普通对象的区别是需要用一个字段来保存数组的长度,所以头部变成12字节,java中一个char采用UTF-16编码,占用2个byte,所以是14byte,对其到16byte,24+16=40byte;

3、第三个在第二个基础上已经分析,就是16byte大小


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


//步骤3:现在开始运行代码:

运行代码前需要保证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;
  }
}






OK,通过上面已经可以看出,保持了原有方法,因为深度递归毕竟比较慢,我们有些时候可以选择到底用那一种:

回到步骤重新做一次:

1、编译agent

2、打包class,并修改META-INF/MANIFEST.MF文件中增加一行:Premain-Class: MySizeOf

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.fullSizeOf(new String("a")));
     System.out.println(MySizeOf.sizeOf(new char[1]));
   } 
}


4、设置环境变量开始运行(如果已经设置好了就无需重复设置):

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内存一下默认会自动打开这个参数,如下:

[xieyu@oracle001 ~]$ java -Xmx31g -XX:+PrintFlagsFinal |grep Compress
     bool SpecialStringCompress                     = true            {product}           
     bool UseCompressedOops                        := true            {lp64_product}      
     bool UseCompressedStrings                      = false           {product} 
[xieyu@oracle001 ~]$ 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是如何浪费空间的,不要一味使用一些高级的东西,在必要的时候,考虑性能还是有很大的空间,类似集合类以及多维数组,前面的引用其实和数据一点关系都没有,但是占用的空间比数据本身都要大很多。


本文只是通过一种方式让大家知道如何去测量对象大小,同时知道一个java对象如何开销内存,开销而且很大,所以回过头来说,即使java并不看重性能和空间,不过如果你的代码写得好同样会跑得更加快。

目录
相关文章
|
1月前
|
Java API Maven
如何使用Java开发抖音API接口?
在数字化时代,社交媒体平台如抖音成为生活的重要部分。本文详细介绍了如何用Java开发抖音API接口,从创建开发者账号、申请API权限、准备开发环境,到编写代码、测试运行及注意事项,全面覆盖了整个开发流程。
147 10
|
1月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
76 2
|
2天前
|
JSON Java Apache
Java基础-常用API-Object类
继承是面向对象编程的重要特性,允许从已有类派生新类。Java采用单继承机制,默认所有类继承自Object类。Object类提供了多个常用方法,如`clone()`用于复制对象,`equals()`判断对象是否相等,`hashCode()`计算哈希码,`toString()`返回对象的字符串表示,`wait()`、`notify()`和`notifyAll()`用于线程同步,`finalize()`在对象被垃圾回收时调用。掌握这些方法有助于更好地理解和使用Java中的对象行为。
|
17天前
|
算法 Java API
如何使用Java开发获得淘宝商品描述API接口?
本文详细介绍如何使用Java开发调用淘宝商品描述API接口,涵盖从注册淘宝开放平台账号、阅读平台规则、创建应用并申请接口权限,到安装开发工具、配置开发环境、获取访问令牌,以及具体的Java代码实现和注意事项。通过遵循这些步骤,开发者可以高效地获取商品详情、描述及图片等信息,为项目和业务增添价值。
51 10
|
25天前
|
存储 Java 数据挖掘
Java 8 新特性之 Stream API:函数式编程风格的数据处理范式
Java 8 引入的 Stream API 提供了一种新的数据处理方式,支持函数式编程风格,能够高效、简洁地处理集合数据,实现过滤、映射、聚合等操作。
41 6
|
25天前
|
Java API 开发者
Java中的Lambda表达式与Stream API的协同作用
在本文中,我们将探讨Java 8引入的Lambda表达式和Stream API如何改变我们处理集合和数组的方式。Lambda表达式提供了一种简洁的方法来表达代码块,而Stream API则允许我们对数据流进行高级操作,如过滤、映射和归约。通过结合使用这两种技术,我们可以以声明式的方式编写更简洁、更易于理解和维护的代码。本文将介绍Lambda表达式和Stream API的基本概念,并通过示例展示它们在实际项目中的应用。
|
1月前
|
安全 Java 编译器
Java对象一定分配在堆上吗?
本文探讨了Java对象的内存分配问题,重点介绍了JVM的逃逸分析技术及其优化策略。逃逸分析能判断对象是否会在作用域外被访问,从而决定对象是否需要分配到堆上。文章详细讲解了栈上分配、标量替换和同步消除三种优化策略,并通过示例代码说明了这些技术的应用场景。
Java对象一定分配在堆上吗?
|
1月前
|
安全 Java API
告别SimpleDateFormat:Java 8日期时间API的最佳实践
在Java开发中,处理日期和时间是一个基本而重要的任务。传统的`SimpleDateFormat`类因其简单易用而被广泛采用,但它存在一些潜在的问题,尤其是在多线程环境下。本文将探讨`SimpleDateFormat`的局限性,并介绍Java 8引入的新的日期时间API,以及如何使用这些新工具来避免潜在的风险。
39 5
|
1月前
|
开发框架 Java 关系型数据库
Java哪个框架适合开发API接口?
在快速发展的软件开发领域,API接口连接了不同的系统和服务。Java作为成熟的编程语言,其生态系统中出现了许多API开发框架。Magic-API因其独特优势和强大功能,成为Java开发者优选的API开发框架。本文将从核心优势、实际应用价值及未来展望等方面,深入探讨Magic-API为何值得选择。
50 2
|
1月前
|
缓存 监控 Java
如何运用JAVA开发API接口?
本文详细介绍了如何使用Java开发API接口,涵盖创建、实现、测试和部署接口的关键步骤。同时,讨论了接口的安全性设计和设计原则,帮助开发者构建高效、安全、易于维护的API接口。
153 4