Java笔记之计算Java对象的大小及其应用
原理
注意 除非特殊说明,以下所说的计算Java对象大小,不涉及该对象所持有的对象本身的大小,只计算该Java对象本身的大小(其中引用类型对象大小只计算为4 bytes),如果要遍历计算Java对象大小(包含其持有对象的大小)可以参考这篇文章 Sizeof for Java
一个Java对象在内存中的大小包括以下(以64位JVM启用压缩为例,综合这里和这里的信息整理):
分类 | 大小(byte) | 备注 |
---|---|---|
对象头 | 8 | 保存对象的 class 信息、ID、在虚拟机中的状态 |
Oop指针 | 4 | |
数据区 | 对象实际包含的数据,引用类型大小为4 bytes | |
数组长度 | 4 | 只有数组对象才有 |
8比特对齐 | 将对象总大小对齐到8字节所需的填充 |
此外,如果是(非静态)内部类的话,由于他默认持有外部类的引用,所以会比普通类的对象多4个byte。
可以参照这张图
其中,数据区占用的大小如下:
(图片来自于android-memories)
#示例
根据Romain Guy在SpeakerDeck中的说法:
一个空的class占用了4+8=12个byte的内存,再加上8比特对齐,实际占用大小为16比特。
1 | class Empty{ |
占用大小:
Allocation Size in bytes dlmalloc 引用 4 Object overhead(对象头) 8 Total = 4 + 8 =12 bytes
经过8-byte aligned后: total = 16 bytes
此外还有包含了数据的对象大小计算方式如下:
对于数组的大小计算(参考一个Java对象到底占用多大内存?和romainguy/android-memories,后者关于数组大小的计算中width&padding = 8
的意义存疑):
按照开头的公式:数组大小 = 8 对象头 + 4 Oop指针 + 4 数组大小标记length + 数组数据占用大小 + 8比特对齐
1 | // arr0大小 = 8 + 4 + 4 + 0 + 8比特对齐(0) = 16 bytes |
计算对象大小的工具
具体的如何计算Java中Object大小,可以参考stackoverflow的这个回答(这里有一份Github上面的实现源码)
可以参考文章:
这里提供一个实例(参考自这里):
Sizeof.java
1 | import java.lang.instrument.Instrumentation; |
Makefile
1 | //Makefile文件 |
在使用时先新建一个Java类,在其中调用sizeof()
方法:
1 | public class Main { |
可以用如下命令:
1 | javac *.java //编译当前目录下的java文件 |
实际应用
String
最长为65534
String s = “”;
中,在编译期最多可以有65534个字符
原因是,Java中的UTF-8编码的Unicode字符串在常量池中以CONSTANT_Utf8
类型表示,常量池中的所有字面量几乎都是通过CONSTANT_Utf8_info
描述的。
这里面的u2 length
表明了该类型存储数据的长度,而u2
是无符号的16位整数,因此理论上允许的的最大长度是2^16=65536
。而 Java class 文件是使用一种变体UTF-8
格式来存放字符的,null
值使用两个字节来表示,因此只剩下65536- 2 = 65534
个字节。
1
2
3
4
5 >CONSTANT_Utf8_info {
>u1 tag;
>u2 length;
>u1 bytes[length];
>}所以,在Java中,所有需要保存在常量池中的数据,长度最大不能超过65535,这当然也包括字符串的定义
上面提到的这种String长度的限制是编译期的限制,也就是使用
String s= “”;
这种字面值方式定义的时候才会有的限制。String在运行期有没有限制呢,答案是有的,就是我们前文提到的那个
Integer.MAX_VALUE
,这个值约等于4G,在运行期,如果String的长度超过这个范围,就可能会抛出异常。(在jdk 1.9之前)
一个String对象,占用大小(JDK1.8)为24 bytes(不计算持有的char数组占用的大小):
1 | /** The value is used for character storage. */ |
再加上在64位JVM中,一个对象具有12 bytes的对象头+引用
,要求对齐到8的倍数(来源2.1. Objects, References and Wrapper Classes),所以一个String对象的大小是:
1 | size = ( 12 对象头 + 4 value + 4 hash ) + 4 8byte对齐 = 24 bytes |
枚举类enum
枚举类大小的计算
枚举类中的每个枚举都是该枚举类的一个对象。
1 | enum EnumClazz{ |
当我们用javap
查看其编译后的字节码可以看到:
1 | //javac EnumClazz.java |
简单计算一下这个EnumClazz
的大小(不含引用对象的大小):
1 | enumClassSize = 8 + 4 + 4*4 + 4 = 32 bytes |
然后,我们再看一下每个枚举类的值(以EnumClazz.Day
为例)的大小:
enum
类的每个值实际上都继承自java.lang.Enum
类:
1 | public abstract class Enum<E extends Enum<E>> |
由此,我们可以计算EnumClazz.Day
的大小:
1 | daySize = 8 + 4 + 4 + 4 + 8比特对齐(4) = 20 + 4 = 24 bytes |
也就是说,本例中每一个枚举类值占用24 bytes,由此可以计算出EnumClazz
实际占用的大小应该是:
1 | realSize = enumClassSize + daySize * 4 = 128 bytes |
Android中是否应该使用枚举
关于Android中使用枚举和常量所占用的大小对比RomainGuy有下图的对比。
关于是否应该在Android中使用枚举类,可以参考下文:
https://www.liaohuqiu.net/cn/posts/android-enum-memory-usage/
https://stackoverflow.com/a/29972028/8389461
总结起来其结论就是:
当需要用到枚举类的特性时,比如非连续判断,方法重载等时就使用枚举,否则就使用占用内存更小的常量类。
SparseArray&ArrayMap VS HashMap
HashMap
的数据是经过包装后保存在HashMap.Node<K,V>
数组中。
下面是HashMap
的结构:
1 | public class HashMap<K,V> extends AbstractMap<K,V> |
再看看Android提供的android.util.SparseArray
类(具体分析可参考:SparseArray 的使用及实现原理)
1 | public class SparseArray<E> implements Cloneable { |
再结合官方的描述,SparseArray
类很明显要比HashMap
占用更少的内存:
- 将
KEY
和VALUE
直接保存在数组中,避免了将其包装为一个Node
对象的开销 - 由于
SparseArray
类的key是int
类型而非被自动装箱后的Integer
对象,所以当同样使用int
类型的key
保存数据时,SparseArray
类的key
要占用更少的内存。
SparseArray
is intended to be more memory-efficient than aHashMap
, because it avoids auto-boxing keys and its data structure doesn’t rely on an extra entry object for each mapping.https://developer.android.google.cn/reference/android/util/SparseArray
但是,SparseArray
有以下局限性:
在每次
put/get/remove
的时候都需要使用二分法(ContainerHelpers.binarySearch(mKeys, mSize, key)
)查找是否已经存在KEY
对应的值(有的话查找其位置)在添加和删除item的时候都需要在数组中增删条目(耗时,尽管为了优化性能,
SparseArray
在删除时只是将对于的值标记为DELETED
,在下次更新该KEY
对于的值时直接覆盖,或者在GC
时删除)。private static final Object DELETED = new Object();
HashMap的删除涉及到数组、链表和红黑树(JDK1.8)
**在容纳数百个项目时性能会比HashMap小大约50%**。
每当需要增长数组或获取数组大小或获取条目值时,都必须执行垃圾回收GC。
此外,还有以下可以替换HashMap的(数据来自这里):
1 | SparseArray <Integer, Object> |
此外,还有android.util.ArrayMap
其特性与SparseArray
类似(两者占用内存小,但是慢并且最好不要用来存储大容量的数据),只不过它支持key值为其他类型,占用内存大小在SparseArray
和HashMap
之间(参考这里),此外ArrayMap
的API和HashMap
类似。
根据Romain Guy的计算:
保存1000个int对象的
SparseArray
占用大小为:8072 bytes保存1000个对象的
HashMap<Integer,Integer>
占用大小为:64136 bytes几乎相差8倍!
综上,当要保存的数据量比较小(小于几千个)的时候,如果KEY是基本类型,推荐使用SparseArray
及其衍生类以节省内存,如果KEY是其他类型则使用ArrayMap
;否则使用HashMap
更加高效。
参考资料
除文章中罗列的链接外:
https://blog.csdn.net/u013380694/article/details/102739636
Sizeof for Java – javaworld.com
RomainGuy-Android Memories(推荐)