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
}

idageisExtinct等字段改为基本类型,节省一些字节。估算内存大小:

LOGGER.info(String.valueOf(GraphLayout.parseInstance(dinosaur).totalSize()));

日志输出:

[main] INFO com.baeldung.reducememoryfootprint.DinosaurUnitTest -- 552

新大小为552字节(初始624字节)。使用基本类型而非包装器节省了72字节。

优化效果:节省72字节内存

进一步,可用short类型存储Dinosaurage(Java的short可存储最多32767个整数):

// ...
short age;
// ...

使用short后的控制台输出:

[main] INFO com.baeldung.reducememoryfootprint.DinosaurUnitTest -- 552

内存大小仍为552字节——此场景无差异。但为内存效率,应始终使用最窄的类型。

对于feedingHabitshabitat和分类信息字段,保留String类型因其灵活性。char[]等替代方案缺乏String提供的内置文本操作方法

6.2. 合并相关类

进一步减少内存占用,可合并相关类的字段到单个类。合并DinosaurDinosaurType

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

合并DinosaurDinosaurType类节省了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)。若booleantrue,将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()));

使用第三方集合库创建DinosaurMutableList集合,输出584字节。标准集合与Eclipse集合相差32字节。

优化效果:节省32字节内存

7.3. 基本类型集合

Eclipse Collection还提供基本类型集合。这些集合避免包装类的开销,显著节省内存。

此外,TroveFastutilColt等库也提供基本类型集合。这些第三方库比Java标准集合性能更优

⚠️ 注意:使用第三方库需权衡依赖管理成本

8. 总结

本文学习了如何估算Java基本类型和对象的内存大小,估算了应用的初始内存占用,并使用以下技术减少内存占用:

  • 用基本类型替代包装器
  • 合并相关对象的类
  • 使用short等窄类型
  • 布尔字段位打包
  • 优化集合选择

完整示例代码见GitHub仓库


原始标题:Reduce Memory Footprint in Java | Baeldung