new一个Object对象占用多少内存?

本文探讨了Java中new一个Object对象所占用的内存情况,通过实验得出结论并解释了对象在堆内存中的存储结构。同时,强调保持技术好奇心对于持续进步的重要性。

Java的自动内存管理机制省却了很多编码工作,大大地提高了Java的生产力,而且JVM的性能也越来越好,特别是G1的出现,改善了垃圾回收中stop the world的状况。

也许很多人都没有考虑过这个问题,new一个Object对象到底占用多少内存呢( Object obj = new Object() )?

这里很明确的是obj是一个指向对象的引用,引用的长度决定了Java的寻址能力,32位的JDK是4字节,64位的JDK是8字节。

因为obj对象没有任何数据(field),会在堆上为它分配空间吗?如果分配空间,里面存储了什么内容?

以面向对象的思维来分析,对象封装了数据和行为,是一个统一的整体,虽然obj对象没有数据,但是有行为(Object类定义了12个方法)。当我们执行完new操作后,obj的值为堆内存的地址,既然obj都指向一块内存了,说明是会在堆上为其分配空间的。

那么分配的空间有多大,存储了什么内容呢?我在JVM规范和JAVA语言规范里面没有找到相关的描述,这可能是属于JVM实现自由控制的范畴了。我们可以利用JDK自带的工具jvisualvm.exe来查看分配的空间有多大,至于存了什么内容,现在还不清楚。

Java代码 收藏代码
  1. /**
  2. *构造一个无任何字段的空对象占多少内存
  3. *@author杨尚川
  4. */
  5. publicclassEmptyTest{
  6. publicstaticvoidmain(String[]args)throwsInterruptedException{
  7. //加到集合中,使垃圾无法回收
  8. List<EmptyObject>emptys=newArrayList<>();
  9. for(inti=0;i<100;i++){
  10. emptys.add(newEmptyObject());
  11. }
  12. //打开jvisualvm,查看EmptyObject的大小为16字节
  13. Thread.sleep(60*1000);
  14. }
  15. privatestaticclassEmptyObject{}
  16. }

我们在这里面通过new不同的对象数(for循环次数),来分析内存占用,new 1个对象是16字节,new 2个对象是32字节,new 100个对象是1600字节,通过很多次的尝试,我们从jvisualvm里面可以看到 字节数=对象数*16 ,我们有理由相信对象数跟字节数的线性关系。从这里可以看出,jvisualvm显示的内存占用跟引用的4字节或8字节是没有关系的,也就是说,jvisualvm显示的是堆内存占用,这也很好理解,毕竟所有引用的字节占用是固定的。我的JDK是64位,8字节是引用,16字节是堆内存,总共是8+16=24字节,所以new一个Object对象占用24字节。如果JDK是32位,则new一个Object对象占用4+16=20字节,如下图所示:



那么分配的16字节的堆空间中存储了什么内容呢?当我们Object obj = new Object();的时候,在栈内存中有一个引用obj,他可能是32位也可能是64位,obj实质只是一个内存地址,那么当我们调用obj.xxx()的时候,JVM怎么知道obj是哪个类的实例呢?所以,可以大胆地推测,obj对象的16字节的堆内存中记录了对象属于哪个类的信息,问题是这16字节的结构是什么样的?不清楚!

我们研究new一个Object对象占多少内存可能没什么实际意义,因为我们在编程的时候就可以确定对象树,基本可以确定对象大小,除了变长字段,当然,变长字段我们一般也会有长度限制。所以我们真正关心的是所有数据最终的大小,也就是数据库的大小。

那么这种没有什么实际意义的问题为什么要研究呢?我觉得只能用三个字来形容:好奇心。好奇心是驱使我们研究技术的强大推力,当我们工作了很多年,尤其是在不重视技术的公司,我们对技术还有激情吗?保持一颗敏感好奇的心,也许技术之路可以走的更长更远。

<think> 我们被要求计算一个包含100万元素的Map在内存占用的空间大小。根据引用[3],我们知道在Java中,HashMap的每个键值对对应一个Node对象。Node对象的结构如下: ```java static class Node<K,V> implements Map.Entry<K,V> { final int hash; // int类型,4字节 final K key; // 对象引用 V value; // 对象引用 Node<K,V> next; // 对象引用 // 构造方法等 } ``` 在32位JVM上,每个对象引用占用4字节,在64位JVM上(通常开启压缩指针,默认开启),每个对象引用占用4字节(如果未开启压缩指针则为8字节)。我们以64位JVM开启压缩指针(-XX:+UseCompressedOops)的情况为主,因为这是现代JVM的默认配置。 一个Node对象内存占用包括: - 对象头(Object Header):在64位JVM中,对象头通常为12字节(如果对象是普通Java对象,且没有数组长度,则Mark Word占8字节,压缩后的Klass指针占4字节;但为了对齐,可能会填充。然而,在计算时,我们通常按12字节计算,但实际占用可能因JVM实现和是否开启压缩指针等而有所不同。这里我们采用常见的计算方式:对象头12字节(8字节Mark Word + 4字节Klass指针))。 - 实例数据: - `int hash`:4字节 - 三个对象引用:`key`(4字节)、`value`(4字节)、`next`(4字节),共12字节 - 对齐填充:由于对象头加上实例数据为12(对象头)+4(hash)+12(三个引用)=28字节,而JVM要求对象大小必须是8字节的整数倍,因此需要填充4字节,使总大小为32字节。 因此,一个Node对象占用32字节。 但是,这仅仅是Node对象本身。我们还需要考虑key和value对象的大小。题目中指定了键值对都是Int类型。这里有两种情况: 1. 使用基本类型int:在Java中,我们不能直接将基本类型作为泛型,所以通常我们会使用包装类型Integer。 2. 使用包装类型Integer:每个Integer对象占用多少内存? Integer对象: - 对象头:12字节(64位JVM开启压缩指针) - 一个int类型的字段`value`:4字节 - 对齐填充:12+4=16字节,已经是8的倍数,所以不需要填充,共16字节。 但是,对于Integer,Java缓存了[-128,127]之间的值,所以如果在这个范围内,可能不会创建新对象,而是使用缓存对象。但是,对于100万个不同的整数,很可能大部分不在这个范围内,因此我们需要为每个键和值都创建一个Integer对象。 因此,每个键值对: - 一个Node对象:32字节 - 一个key(Integer):16字节 - 一个value(Integer):16字节 - 总基本占用:32+16+16=64字节。 但是,这还没有结束。HashMap本身是一个数组加上链表(或红黑树)的结构。HashMap内部有一个Node数组(在JDK8中称为table),这个数组的长度是2的幂,且为了减少哈希冲突,数组长度通常会大于元素个数(负载因子默认0.75)。因此,当有100万个元素时,数组长度应该至少为100万/0.75≈133.33万,向上取整为2的幂,即2^21=2097152(因为2^20=1048576小于133.33万,所以取2^21)。 所以,数组大小为2097152。每个数组元素是一个Node引用(在未存放元素的位置为null)。因此,数组本身占用: - 数组对象头:在64位JVM中,数组对象头包含Mark Word(8字节)、压缩后的Klass指针(4字节)、数组长度(4字节),共16字节。 - 数组元素:2097152个引用,每个引用4字节,共2097152*4=8388608字节。 - 数组总大小:16(对象头)+8388608=8388624字节。由于对象需要对齐,但这里16+8388608=8388624,已经是8的倍数(因为8388608是8的倍数,16也是8的倍数),所以不需要填充。 另外,我们还有100万个Node对象,每个32字节,共32000000字节(1000000*32)。 以及100万个key(Integer)和100万个value(Integer),各16字节,共32000000字节(1000000*32)。 但是,注意:数组中的每个位置是一个引用,指向一个Node。而Node对象中又包含key和value的引用。所以,我们上面已经分别计算了Node数组、Node对象、Integer对象。 所以,整个HashMap的总内存占用为: - Node数组:8388624字节 - Node对象:1000000*32=32000000字节 - key(Integer):1000000*16=16000000字节 - value(Integer):1000000*16=16000000字节 总计:8388624+32000000+16000000+16000000 = 72388624字节。 换算成MB:72388624/(1024*1024)≈69.04 MB。 但是,这只是一个近似值,因为: 1. 我们忽略了Integer缓存的影响(对于在缓存范围内的整数,可能不会创建新的Integer对象,但是100万个整数,大部分可能不在缓存范围内,所以这里我们按全部创建新对象计算)。 2. 我们假设没有链表或红黑树(在哈希冲突少的情况下,链表很短,可以忽略链表指针的额外开销。但如果有冲突,则会有一些Node的next指针指向另一个Node,这些Node已经在100万个Node中计算过了,所以不需要额外计算。但是,如果冲突多,可能会有一些Node对象因为形成链表而多占用一点空间(每个Node的next指针指向下一个Node,这些Node已经计算在内)。 3. 我们假设HashMap的数组大小是2097152(即2^21),这是根据默认负载因子0.75计算出来的。实际中,如果指定了初始容量,可能避免扩容,但数组大小不会小于这个值(除非调整负载因子)。 另外,HashMap本身也有字段占用空间: - 对象头:12字节 - 字段:如负载因子(float,4字节)、阈值(int,4字节)、大小(int,4字节)、修改次数(int,4字节)等,这些字段大约需要20字节左右,加上对象头12字节,共32字节(需要对齐?实际可能为24或32字节,这里按24字节计算,因为对象头12字节,再加上几个字段,然后对齐到8的倍数。但相对于整个Map的大小,这个可以忽略不计)。 因此,总占用大约69MB。 但是,引用[1]中提到一个Map端内存占用3G,显然比我们这个100万元素的Map大得多。因此,实际应用中,如果键值对不是基本类型的包装类,而是复杂对象,那么内存占用会更大。 另外,引用[2]中提到Map的空间利用率问题,在Go中,但原理类似。我们这里计算的是Java的HashMap。 综上所述,一个包含100万个键值对(键和值都是Integer)的HashMap,在64位JVM(开启压缩指针)下,大约占用69MB内存。 注意:以上计算基于理论,实际JVM实现可能有优化,或者有额外的开销(例如对齐填充等),但大致在这个数量级。 相关问题: 1. 如何减少Java HashMap的内存占用? 2. 基本数据类型(如int)的Map在Java中如何更高效地存储? 3. HashMap的负载因子对内存占用有什么影响? </think>### HashMap 内存占用计算(100万元素) #### 1. **基础对象内存占用** - **Node 对象结构**(每个键值对对应一个 Node): ```java static class Node<K,V> { final int hash; // 4 字节 final K key; // 引用 4 字节 (压缩指针) V value; // 引用 4 字节 (压缩指针) Node<K,V> next; // 引用 4 字节 (压缩指针) } ``` - **Node 总占用**: - 对象头:12 字节(64 位 JVM + 压缩指针) - 字段:`hash`(4) + `key`(4) + `value`(4) + `next`(4) = 16 字节 - **对齐填充**:总 28 字节 → 填充至 32 字节(8 的倍数) #### 2. **键值对内存计算(Int 类型)** - **Integer 对象占用**: - 对象头:12 字节 - `int value` 字段:4 字节 - **对齐后总占用**:16 字节/个 - **单组键值对总占用**: ```math \begin{align*} \text{总占用} &= \text{Node} + \text{Key(Integer)} + \text{Value(Integer)} \\ &= 32 \text{ 字节} + 16 \text{ 字节} + 16 \text{ 字节} \\ &= 64 \text{ 字节} \end{align*} ``` #### 3. **HashMap 底层数组占用** - **数组长度计算**(负载因子 0.75): ```math \text{最小容量} = \frac{1,000,000}{0.75} \approx 1,333,333 \rightarrow \text{向上取整为 } 2^{21} = 2,097,152 ``` - **数组占用**: - 数组对象头:16 字节(含长度标记) - 引用数组:2,097,152 × 4 字节 = 8,388,608 字节 - **数组总大小**:16 + 8,388,608 = 8,388,624 字节 #### 4. **总体内存占用** ```math \begin{align*} \text{总占用} &= \text{键值对占用} \times \text{数量} + \text{数组占用} \\ &= (64 \text{ 字节} \times 1,000,000) + 8,388,624 \text{ 字节} \\ &= 64,000,000 + 8,388,624 \\ &= 72,388,624 \text{ 字节} \\ &\approx 69.04 \text{ MB} \end{align*} ``` > **关键说明**: > 1. 基于 **64 位 JVM + 压缩指针**(`-XX:+UseCompressedOops`) > 2. 实际占用受 **JVM 对齐填充**、**垃圾回收器** 和 **具体数据分布** 影响 > 3. 若禁用压缩指针,内存占用增加约 30% #### 内存分布详情表 | 组件 | 数量 | 单元素占用 | 总占用 | 占比 | |---------------------|------------|------------|------------|--------| | Node 对象 | 1,000,000 | 32 字节 | 32,000,000 | 44.2% | | Key (Integer) | 1,000,000 | 16 字节 | 16,000,000 | 22.1% | | Value (Integer) | 1,000,000 | 16 字节 | 16,000,000 | 22.1% | | 桶数组 (Node[]) | 2,097,152 | 4 字节/槽 | 8,388,624 | 11.6% | | **总计** | - | - | 72,388,624 | 100% | --- ### 优化建议 1. **使用原始类型集合**(如 Eclipse Collections 的 `IntIntHashMap`): - 避免 `Integer` 对象开销,内存降至约 32 MB ```java IntIntHashMap map = new IntIntHashMap(1_000_000); ``` 2. **调整负载因子**: ```java new HashMap<>(1_000_000, 0.9f); // 减少数组扩容 ``` 3. **预分配大小**: ```java new HashMap<>(1_333_334); // 直接避免扩容 ``` --- ### 相关问题 1. 如何准确测量 JVM 中 HashMap 的真实内存占用? 2. 使用 `SparseArray` 替代 HashMap 能节省多少内存? 3. 高负载因子(如 0.9)对 HashMap 性能有何影响? [^1]: 项目中 Map 端内存占用的分析,强调内存优化需求 [^3]: HashMap 的 Node 结构定义和空间计算基础
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值