优质博文:IT-BLOG-CN
通过掌握每种数据类型的大小,就可以更准确地预测对象和数据的内存消耗。
一、基础数据类型
Java
基础数据类型结构,在64
位系统开启指针压缩情况下的内存占用字节数:
boolean | byte | char | short | int | long | float | double | 引用 |
---|---|---|---|---|---|---|---|---|
1 | 1 | 2 | 2 | 4 | 8 | 4 | 8 | 4 |
这些基础数据类型是轻量级的,应尽可能多地使用他们以减少内存消耗。
二、对象内存占用情况
Java
对象的内存占用包括标记字mark word
、类指针klass pointer
、数据字段(实例变量)以及填充padding
markword | klass pointer | data | padding |
---|---|---|---|
8 | 4 | 所有字段占用内存之和 | 将内存占用补齐至8的整数倍 |
由此可见,一个没有任何数据字段的空对象,也会占用16
字节的内存空间
三、数组内存占用
数组在Java
中是一种特殊类型的对象,它还包括一个额外的数组长度字段。数组的内存占用包括标记字、类指针、数组长度字段、数组元素以及填充。与对象类型,数组的占用长度也必须是8
的倍数。需要注意的是,数组元素的大小取决于元素的类型和数量。
markword | klass pointer | 数组长度字段 | 数组元素 | padding |
---|---|---|---|---|
8 | 4 | 4 | 所有字段占用内存之和 | 将内存占用补齐至8的整数倍 |
四、String对象内存占用
这里,我们以Java8
为例说说空String
的内存占用情况,我们来看看String
类中的成员变量。
java
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
根据上面我们对对象和数组内存的分析以及String
的成员属性可以得出一个空String
对象所占用的内存空间,如下所示。
java
对象头(8 字节)+ 引用 (4 字节 ) + char 数组(16 字节)+ 1个 int(4字节)+ 1个long(8字节)= 40 字节
如果String
字符串的长度大于0
的话,我们也可以得出String
占用内存的计算公式,如下所示。
java
40 + 2 * n
五、内存占用分析工具
位了更准确地估计内存占用情况,我们可以借助一些内存分析工具。
jol-core
是一个java
官方推出的jar
包,地址为GitHub-openjdk/jol
pom
依赖:
xml
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.14</version>
</dependency>
JOL
中常用的几个方法:
java
ClassLayout.parseInstance(obj).toPrintable():查看对象内部的内存布局;
ClassLayout.parseInstance(obj).instanceSize():计算对象的大小,单位为字节;
GraphLayout.parseInstance(obj).toPrintable():查看对象外部信息,包括引用的对象;
GraphLayout.parseInstance(obj).totalSize():查看对象占用空间总大小;
注:除了parseInstance(Object obj)
外,还有parseClass(Class<?> class)
,用法基本相同。
代码使用场景:
java
public class JolTest {
public static void main(String[] args) {
Object obj = new Test();
// 查看对象内部信息
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}
class Test{
private long p;
private byte p1;
private short p2;
}
六、防止内存膨胀的策略
【1】使用基础类型代替String
: 在处理业务时,短字符串(如城市三字码)频繁出现。标准String
对象在Java
中占用至少50
字节内存,这对于仅包含2-3
个字符的字符串来说是一种浪费。我们可以使用CodeUtil
工具将这些字符串编码为原生数据类型,从而显著降低内存占用并可能提升性能。
将字符串编码为浮点数和解码为字符串
java
public class Main {
public static void main(String[] args) {
String doubleString = "123.45";
// 将字符串编码为浮点数
double encodedDouble = CodeUtil.encodeToDouble(doubleString);
System.out.println("Encoded Double: " + encodedDouble);
// 将浮点数解码为字符串
String decodedDoubleString = CodeUtil.decodeFromDouble(encodedDouble);
System.out.println("Decoded Double String: " + decodedDoubleString);
String input = "Hello, World!";
// 使用CodeUtil工具将字符串编码为char[]
char[] encodedChars = CodeUtil.encodeToCharArray(input);
// 打印编码后的char数组
System.out.println("Encoded char array:");
for (char c : encodedChars) {
System.out.print(c + " ");
}
}
}
输出:
text
Encoded Double: 123.45
Decoded Double String: 123.45
Encoded char array:
H e l l o , W o r l d !
【2】使用对象池: 在某些业务场景中,可能存在大量完全相同或相似的对象。位了减少这些对象的内存占用,可以使用对象池技术来重用这些对象。通过对象池,我们可以确保相同的对象在内存中只有一份拷贝,从而节省大量的内存空间。需要注意的是,在使用对象池技术时,需要确保对象的正确性和线程安全性。
【3】限制容器最大容量: HashMap
等容器类在Java
中广泛使用,但他们没有容量上限。如果不断向容器中添加元素,它们会持续扩容以及容纳更多的元素,从而导致内存占用不断增加。为了避免这种情况的发生,我们使用具有容量限制的容器类(如Guava Cache
)来替代HashMap
等无限制容器。这样可以将内存占用控制在一个合理范围内,防止内存膨胀现象的发生。需要注意的是,在设置容量限制时,需要根据实际业务需求和系统性能进行合理权衡。