1. 概述
应用体积直接影响启动时间和内存使用,这两者都会影响性能。即使硬件强大,通过精心的编码实践和优化技术决策,我们仍能显著减少应用的内存占用。
数据类型、数据结构和类设计的选择会影响应用体积。选择最合适的数据类型可以降低生产环境运行成本。
本教程将学习如何手动估算Java应用的内存大小,探索各种减少内存占用的技术,并使用Java对象布局(JOL)库验证估算结果。最后,由于不同JVM的对象内存布局可能不同,我们将使用Hotspot JVM —— 它是OpenJDK的默认JVM。
2. Maven依赖
首先,向pom.xml添加JOL库:
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.17</version>
</dependency>
该库提供分析和报告Java对象内存布局的类。注意,内存占用计算取决于JVM架构,不同JVM可能有不同的对象内存布局。
3. Java基本类型和对象的大小
系统RAM的结构类似行列表格。每种数据类型占用特定位数,用于估算内存使用。Java基本类型和对象有不同的内存表示。
3.1. 内存字
内存字表示处理器单次操作可传输的数据量。基于系统架构,32位系统的内存字大小为4字节,64位系统为8字节。
当数据类型无法填满最小尺寸时,会被向上取整到4字节(32位系统)或8字节(64位系统),即对象会被填充以适应这些边界。
理解这点能深入洞察程序的内存使用情况。
3.2. 对象的大小
Java中不含任何字段的空类仅包含元数据。这些元数据包括:
- Mark Word:64位系统上占8字节
- 类指针:64位系统上使用压缩oops时占4字节
因此,在64位系统上,对象的最小大小为16字节(含内存对齐)。
3.3. 基本类型包装器的大小
此外,基本类型包装器是封装基本类型的对象。它们由对象头、类指针和基本类型字段组成。
以下是64位系统中考虑内存填充后的大小估算:
类型 | Mark Word | 类指针 | 基本类型值 | 内存填充 | 总计 |
---|---|---|---|---|---|
Byte | 8 | 4 | 1 | 3 | 16 |
Short | 8 | 4 | 1 | 3 | 16 |
Character | 8 | 4 | 1 | 3 | 16 |
Integer | 8 | 4 | 4 | – | 16 |
Float | 8 | 4 | 4 | – | 16 |
Long | 8 | 4 | 8 | 4 | 24 |
Double | 8 | 4 | 8 | 4 | 24 |
上表通过分析组成总大小的各组件来定义包装器的内存大小。
4. 示例设置
现在我们已了解如何估算不同Java基本类型和对象的内存大小,接下来创建一个简单项目并估算其初始大小。首先创建Dinosaur类:
class Dinosaur {
Integer id;
Integer age;
String feedingHabits;
DinosaurType type;
String habitat;
Boolean isExtinct;
Boolean isCarnivorous;
Boolean isHerbivorous;
Boolean isOmnivorous;
// constructor
}
然后定义表示恐龙分类的DinosaurType类:
class DinosaurType {
String kingdom;
String phylum;
String clazz;
String order;
String family;
String genus;
String species;
// constructor
}
这里创建了DinosaurType类,在Dinosaur类中被引用。
5. 估算初始内存大小
计算未优化应用的初始内存大小。首先实例化Dinosaur类:
DinosaurType dinosaurType
= new DinosaurType("Animalia", "Chordata", "Dinosauria", "Saurischia", "Eusaurischia", "Eoraptor", "E. lunensis");
Dinosaur dinosaur = new Dinosaur(1, 10, "Carnivorous", dinosaurType, "Land", true, false, false, true);
使用JOL库估算dinosaur对象大小:
LOGGER.info(String.valueOf(GraphLayout.parseInstance(dinosaur).totalSize()));
未优化时的大小估算为624字节。注意,该值可能因JVM实现而异。
6. 优化初始内存大小
理解初始内存占用后,探索优化方法。
6.1. 使用基本类型
修改Dinosaur类,用基本类型替换包装器:
class Dinosaur {
int id;
int age;
String feedingHabits;
DinosaurType type;
String habitat;
boolean isExtinct;
boolean isCarnivorous;
boolean isHerbivorous;
boolean isOmnivorous;
// constructor
}
将id、age、isExtinct等字段改为基本类型,节省一些字节。估算内存大小:
LOGGER.info(String.valueOf(GraphLayout.parseInstance(dinosaur).totalSize()));
日志输出:
[main] INFO com.baeldung.reducememoryfootprint.DinosaurUnitTest -- 552
新大小为552字节(初始624字节)。使用基本类型而非包装器节省了72字节。
✅ 优化效果:节省72字节内存
进一步,可用short类型存储Dinosaur的age(Java的short可存储最多32767个整数):
// ...
short age;
// ...
使用short后的控制台输出:
[main] INFO com.baeldung.reducememoryfootprint.DinosaurUnitTest -- 552
内存大小仍为552字节——此场景无差异。但为内存效率,应始终使用最窄的类型。
对于feedingHabits、habitat和分类信息字段,保留String类型因其灵活性。char[]等替代方案缺乏String提供的内置文本操作方法。
6.2. 合并相关类
进一步减少内存占用,可合并相关类的字段到单个类。合并Dinosaur和DinosaurType:
class DinosaurNew {
int id;
short age;
String feedingHabits;
String habitat;
boolean isExtinct;
boolean isCarnivorous;
boolean isHerbivorous;
boolean isOmnivorous;
String kingdom;
String phylum;
String clazz;
String order;
String family;
String genus;
String species;
// constructor
}
通过合并两个类的字段创建新类。此方法有效,因为Dinosaur实例有独特分类。若多个实例共享相同分类,则效率不高。合并类前需分析用例。
实例化新类:
DinosaurNew dinosaurNew
= new DinosaurNew(1, (short) 10, "Carnivorous", "Land", true, false, false, true, "Animalia", "Chordata", "Dinosauria", "Saurischia", "Eusaurischia", "Eoraptor", "E. lunensis");
计算内存大小:
LOGGER.info(String.valueOf(GraphLayout.parseInstance(dinosaurNew).totalSize()));
控制台输出:
[main] INFO com.baeldung.reducememoryfootprint.DinosaurUnitTest -- 536
合并Dinosaur和DinosaurType类节省了16字节(原本用于对象的mark word、类指针和内存填充)。
✅ 优化效果:再节省16字节内存
6.3. 布尔字段的位打包
存在多个boolean字段时,可将其打包到单个short类型。首先定义位位置:
static final short IS_EXTINCT = 0, IS_CARNIVOROUS = 1, IS_HERBIVOROUS = 2, IS_OMNIVOROUS = 3;
编写方法将boolean转换为short:
static short convertToShort(
boolean isExtinct, boolean isCarnivorous, boolean isHerbivorous, boolean isOmnivorous) {
short result = 0;
result |= (short) (isExtinct ? 1 << IS_EXTINCT : 0);
result |= (short) (isCarnivorous ? 1 << IS_CARNIVOROUS : 0);
result |= (short) (isHerbivorous ? 1 << IS_HERBIVOROUS : 0);
result |= (short) (isOmnivorous ? 1 << IS_OMNIVOROUS : 0);
return result;
}
该方法接收四个boolean参数,转换为单个short值(每位表示一个boolean)。若boolean为true,将1左移对应标志位;若为false,使用0。最后用位或运算符组合所有值。
再编写转换回boolean的方法:
static boolean convertToBoolean(short value, short flagPosition) {
return (value >> flagPosition & 1) == 1;
}
该方法从打包的short中提取单个boolean值。
移除四个boolean字段,替换为单个short标志位:
short flag;
实例化Dinosaur对象并计算内存占用:
short flags = DinousaurBitPacking.convertToShort(true, false, false, true);
DinousaurBitPacking dinosaur
= new DinousaurBitPacking(1, (short) 10, "Carnivorous", "Land", flags, "Animalia", "Chordata", "Dinosauria", "Saurischia", "Eusaurischia", "Eoraptor", "E. lunensis");
LOGGER.info("{} {} {} {}",
DinousaurBitPacking.convertToBoolean(dinosaur.flag, DinousaurBitPacking.IS_EXTINCT),
DinousaurBitPacking.convertToBoolean(dinosaur.flag, DinousaurBitPacking.IS_CARNIVOROUS),
DinousaurBitPacking.convertToBoolean(dinosaur.flag, DinousaurBitPacking.IS_HERBIVOROUS),
DinousaurBitPacking.convertToBoolean(dinosaur.flag, DinousaurBitPacking.IS_OMNIVOROUS));
LOGGER.info(String.valueOf(GraphLayout.parseInstance(dinosaur).totalSize()));
现在用单个short表示四个boolean值。JOL计算结果:
[main] INFO com.baeldung.reducememoryfootprint.DinosaurUnitTest -- true false false true
[main] INFO com.baeldung.reducememoryfootprint.DinosaurUnitTest -- 528
内存占用从536字节降至528字节。
✅ 优化效果:再节省8字节内存
7. Java集合
Java集合有复杂的内部结构,手动计算可能繁琐。但可使用Eclipse Collection库进一步优化内存。
7.1. 标准Java集合
计算Dinosaur类型ArrayList的内存占用:
List<DinosaurNew> dinosaurNew = new ArrayList<>();
dinosaurPrimitives.add(dinosaurNew);
LOGGER.info(String.valueOf(GraphLayout.parseInstance(dinosaurNew).totalSize()));
控制台输出616字节。
7.2. Eclipse Collection
使用Eclipse Collection库减少内存占用:
MutableList<DinosaurNew> dinosaurPrimitivesList = FastList.newListWith(dinosaurNew);
LOGGER.info(String.valueOf(GraphLayout.parseInstance(dinosaurNew).totalSize()));
使用第三方集合库创建Dinosaur的MutableList集合,输出584字节。标准集合与Eclipse集合相差32字节。
✅ 优化效果:节省32字节内存
7.3. 基本类型集合
Eclipse Collection还提供基本类型集合。这些集合避免包装类的开销,显著节省内存。
此外,Trove、Fastutil和Colt等库也提供基本类型集合。这些第三方库比Java标准集合性能更优。
⚠️ 注意:使用第三方库需权衡依赖管理成本
8. 总结
本文学习了如何估算Java基本类型和对象的内存大小,估算了应用的初始内存占用,并使用以下技术减少内存占用:
- 用基本类型替代包装器
- 合并相关对象的类
- 使用short等窄类型
- 布尔字段位打包
- 优化集合选择
完整示例代码见GitHub仓库。