java各种类型对象占用内存情况分析(经典篇,有图有真相)

写在转载文章前面:

在项目中遇到了诡异的内存占用过高的问题,其实搬到服务器上内存也够,但是某位大神还是锲而不舍的追求问题本质,于是我们学习了这篇文章。

项目中读了一个1.7G的词向量文件(对,用Java搞NLP),文件每行为一个词语及300维度的词向量,自然是要存为一个hashmap,便于查找;

诡异的是这个map整体上似乎占用了过高的内存(>6G),导致在16G的台式机上跑不动。

最后我们查到本质的原因是 hashmap 为 String(词语):ArrayList<Double>(词向量),问题在于这个封装类型Double;

众所周知啊,double占8b,但是list<>里只能用封装类型,所以用了Double,然后Double竟然占用24b,在本项目这个场景下,60w*300的double数据就生生扩大了两三倍的存储空间。

我们学习一下这篇文章了解一下吧。

当然最后的解决方案是 hashmap 为String:WordVector,其中WordVector就是把double[] 这个数组封装了一下。最后占用的内存基本相当于文件大小了。


为什么写这篇文章?

其实一般的程序猿根本不用了解这么深,只有当你到了一定层次,需要了解jvm内部运行机制,或者高并发多线程下,你写的代码对内存有影响,你想做性能优化。。。等等等等,一句话,当你想深入了解java对象在内存中,如何存储,或者每个对象占用多大空间时,你会感谢这篇文章

本文主要分析jvm中的情况,实验环境为64位window10系统、JDK1.8,使用JProfiler进行结论验证

很多描述以及 概念是基于你懂基本java知识的,如果你看起来有点吃力,要加油咯

本片更偏重验证,更多理论,请参考:https://segmentfault.com/a/1190000006933272


基本数据类型占用

类型占用空间
boolean、byte1byte
short、char2byte
int、float4byte
long、double8byte

接下来用JProfiler验证:

  1. 新建一个空对象,观察空对象内存占用
1
2
public class TestObject {
}

对象占用内存 16b,如图

在这里插入图片描述

结论:一般自建空对象占用内存 16b,16 = 12 + 4

  1. 在TestObj中新增一个 int 属性,观察对象内存占用
1
2
3
4
public class TestObj {
    private int i;

}

对象占用内存 16b,如图
在这里插入图片描述

结论:int 占用 4b, 4 = 16 -12

  1. 在TestObj中新增一个 long 属性,观察对象内存占用
1
2
3
4
public class TestObj {
    private long i;

}

对象占用内存 24b,如图
在这里插入图片描述

结论:long 占用 8b, 8 = 24 -12 - 4

其余基本类型可以参照以上自行验证,原理一样


包装类型占用

包装类(Boolean/Byte/Short/Character/Integer/Long/Double/Float)占用内存的大小等于对象头大小加上底层基础数据类型的大小。

类型占用空间
Boolean、Byte16byte
Short、Char16byte
Integer、Float16byte
Long、Double24byte
  1. 在TestObj中新增一个 Integer 属性,观察对象内存占用
1
2
3
4
public class TestObj {
   private Integer  i =128;

}

对象占用内存 32b,如图

在这里插入图片描述

结论:Integer 占用 16b, 16 = 32 - 16

特别的:-128~127 之间的封装类型,只占用 4b**

  1. 在TestObj中新增一个 Long 属性,观察对象内存占用
1
2
3
4
public class TestObj {
   private Long  l = new Long(1);

}

对象占用内存 40b,如图

在这里插入图片描述

结论:Long 占用 24b, 16 = 40 - 16

其余包装类型可以参照以上自行验证,原理一样


基本类型数组占用

64位机器上,数组对象的对象头占用24 bytes,启用压缩后占用16字节。比普通对象占用内存多是因为需要额外的空间存储数组的长度(普通16b-12b)。

对象数组本身的大小=数组对象头 + length * 存放单个元素大小

  1. 在TestObj中新增一个 char[] 属性,观察对象内存占用
1
2
3
4
public class TestObj {
   private char[] c = {'a','b','c'};

}

对象占用内存 40b,如图

在这里插入图片描述

结论:char[3] 占用 24b, 24 = 40 - 16,24 = 16 + 3 * 2 + 2


封装类型数组占用

封装类型数组比基本类型的数组,需要多管理元素的引用

对象数组本身的大小=数组对象头+length 引用指针大小 + length 存放单个元素大小

  1. 在TestObj中新增一个 Integer[] 属性,观察对象内存占用
1
2
3
4
public class TestObj {
    private Integer[] i = {128,129,130};

}

对象占用内存 80b,如图

在这里插入图片描述

结论:Integer[3] 占用 80b, 80 = 96 - 16 , 80 = 16 + 3 4 + 3 16 +4


String占用内存

  1. 在TestObj中新增一个空 String 属性,观察对象内存占用
1
2
3
4
public class TestObj {
    private String s = new String("");

}

对象占用内存 40b,如图

在这里插入图片描述

结论:String 本身占用 24b, 24 = 40 -16,另外,String的属性value还需要 16b,也就是说空””也需要16b

注意:这里为什么要写String s = new String(“”)?请自己思考,不写会怎么样?

答:如果写成String s = “”,是不会再堆中开辟内存的,也就看不到String占用的空间,你看到的将会是下面的,至于为什么,都是因为final

在这里插入图片描述

ArrayList, HashMap的内存占用

这些参考文章开头提到的那篇文章,下面给出计算公式:

  1. 一个ArrayList实例本身的的大小为

12(header) + 4(modCount) + 4(size) + 4(elementData reference) = 24 (bytes)

下面分析一个只有一个Integer(1)元素的ArrayList实例占用的内存大小。

ArrayList<Integer> testList = Lists.newArrayList();
testList.add(1);

根据上面对ArrayList原理的介绍,当调用add方法时,ArrayList会初始化一个默认大小为10的数组,而数组中保存的Integer(1)实例大小为16 bytes。

则testList占用的内存大小为:

24(ArrayList itselft) + 16(elementData array header) + 10 * 4(elemetData reference) + 16(Integer) = 96 (bytes)

JProfiler中的结果验证了上述分析:

在这里插入图片描述
2. HashMap内存占用

这里分析一个只有一组键值对的HashMap, 结构如下:

Map<Integer, Integer> testMap = Maps.newHashMap();
testMap.put(1, 2);

首先分析HashMap本身的大小。HashMap对象拥有的属性包括:

/**
   * The table, initialized on first use, and resized as
   * necessary. When allocated, length is always a power of two.
   * (We also tolerate length zero in some operations to allow
   * bootstrapping mechanics that are currently not needed.)
   */
  transient Node<K,V>[] table;

  /**
   * Holds cached entrySet(). Note that AbstractMap fields are used
   * for keySet() and values().
   */
  transient Set<Map.Entry<K,V>> entrySet;

  /**
   * The number of key-value mappings contained in this map.
   */
  transient int size;

  /**
   * The number of times this HashMap has been structurally modified
   * Structural modifications are those that change the number of mappings in
   * the HashMap or otherwise modify its internal structure (e.g.,
   * rehash).  This field is used to make iterators on Collection-views of
   * the HashMap fail-fast.  (See ConcurrentModificationException).
   */
  transient int modCount;

  /**
   * The next size value at which to resize (capacity * load factor).
   *
   * @serial
   */
  // (The javadoc description is true upon serialization.
  // Additionally, if the table array has not been allocated, this
  // field holds the initial array capacity, or zero signifying
  // DEFAULT_INITIAL_CAPACITY.)
  int threshold;

  /**
   * The load factor for the hash table.
   *
   * @serial
   */
  final float loadFactor;

HashMap继承了AbstractMap<K,V>, AbstractMap有两个属性:

transient Set<K>        keySet;
 transient Collection<V> values;

所以一个HashMap对象本身的大小为:

12(header) + 4(table reference) + 4(entrySet reference) + 4(size) +
4(modCount) + 4(threshold) + 8(loadFactor) + 4(keySet reference) +
4(values reference) = 48(bytes)

接着分析testMap实例在总共占用的内存大小。
根据上面对HashMap原理的介绍,可知每对键值对对应一个Node对象。根据上面的Node的数据结构,一个Node对象的大小为:

12(header) + 4(hash reference) + 4(key reference) + 4(value reference)+ 4(next pointer reference) = 28 (padding) -> 32(bytes)

加上Key和Value两个Integer对象,一个Node占用内存总大小为:32 + 2 * 16 = 64(bytes)

JProfiler中结果:
在这里插入图片描述

下面分析HashMap的Node数组的大小。
根据上面HashMap的原理可知,在不指定容量大小的情况下,HashMap初始容量为16,所以testMap的Node[]占用的内存大小为:

16(header) + 16 * 4(Node reference) + 64(Node) = 144(bytes)

JProfile结果:

clipboard.png

所以,testMap占用的内存总大小为:

48(map itself) + 144(Node[]) = 192(bytes)

JProfile结果:

在这里插入图片描述

这里只用一个例子说明如何对HashMap进行占用内存大小的计算,根据HashMap初始化容量的大小,以及扩容的影响,HashMap占用内存大小要进行具体分析,
不过思路都是一致的。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java对象数组占用内存的计算方法和Java基本类型数组类似,但需要考虑每个对象占用内存大小。 Java对象占用内存大小包括对象头部信息和实例数据两部分。Java对象头部信息包括以下三个部分: 1. 标记字:用于标记该对象是否被回收,占用4个字节。 2. 类型指针:指向该对象所属的类,占用4个字节。 3. 实例数据长度:表示该对象实例数据所占用的字节数,占用4个字节。 因此,Java对象头部信息的总大小为12个字节。 Java对象实例数据包括该对象的所有实例变量。实例变量的类型和数量不同,因此每个对象的实例数据大小也不同。 因此,Java对象数组占用内存大小等于数组头部信息的大小加上每个对象实例数据占用的大小之和乘以数组长度。 下面以Student对象数组为例,介绍Java对象数组内存大小的计算方法: ```java class Student { String name; int age; } Student[] students = new Student[10]; int size = 12 + (ObjectSizeCalculator.getObjectSize(new Student()) * students.length); ``` 上述代码中,`students`是一个长度为10的Student对象数组,`size`即为该数组占用内存大小,其中12为数组头部信息的大小,`ObjectSizeCalculator.getObjectSize(new Student())`为一个Student对象实例占用内存大小,乘以数组长度即为所有Student对象实例占用内存大小之和。 需要注意的是,上述方法只适用于对象没有任何引用类型成员变量的情况。对于对象数组中包含引用类型成员变量的情况,还需要考虑这些成员变量所占用内存大小。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值