減少 Java 中的記憶體佔用
1. 概述
應用程式大小對於啟動時間和記憶體使用至關重要,這兩者都會影響效能。即使使用當今強大的硬件,我們也可以透過仔細的編碼實踐和優化的技術決策來顯著減少應用程式的記憶體佔用。
資料類型、資料結構和類別設計的選擇會影響應用程式的大小。選擇最合適的資料類型可以降低在生產中運行應用程式的成本。
在本教程中,我們將學習如何手動估計 Java 應用程式的記憶體大小,探索減少記憶體佔用的各種技術,並使用 Java 物件佈局 (JOL) 庫來驗證我們的估計。最後,由於 JVM 可以對物件有不同的記憶體佈局,因此我們將使用 Hotspot JVM 。它是 OpenJDK 中的預設 JVM。
2.Maven依賴
首先,讓我們將JOL 庫加入pom.xml
中:
<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位元組。
當資料類型無法填入最小大小時,它會在 32 位元系統中四捨五入到 4 字節,在 64 位元系統中四捨五入到 8 位元組,這表示物件會被填滿以適應這些邊界。
理解這一點可以讓我們深入了解程式記憶體的使用情況。
3.2.物體的大小
Java 中沒有任何欄位的空類別僅包含元資料。該元資料包括:
- 標記字:64 位元系統上為 8 個位元組
- 類別指標:4 個位元組,在 64 位元系統上有壓縮的 oops
因此,在 64 位元系統上進行記憶體對齊時,物件的最小大小為 16 位元組。
3.3.原始包裝器的大小
此外,原始包裝器是封裝原始類型的物件。它們由物件頭、類別指標和原始欄位組成。
以下是 64 位元系統中記憶體填充的大小估計:
類型 | 標記詞 | 類別指針 | 原始價值 | 記憶體填充 | 全部的 |
---|---|---|---|---|---|
Byte |
8 | 4 | 1 | 3 | 16 |
Short |
8 | 4 | 1 | 3 | 16 |
Characters |
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
}
接下來,讓我們定義一個代表Dinosaur
分類的類別:
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,
isCarnivorous,
和其他包裝器的類型以改用原始類型。此操作為我們節省了一些位元組。接下來,我們來估計一下記憶體大小:
LOGGER.info(String.valueOf(GraphLayout.parseInstance(dinosaur).totalSize()));
這是日誌輸出:
[main] INFO com.baeldung.reducememoryfootprint.DinosaurUnitTest -- 552
與最初的 624 位元組相比,新大小為 552 位元組。因此,使用原始類型而不是包裝器可以節省 72 個位元組。
此外,我們可以使用Dinosaur
age
的short
類型。 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
實例具有獨特的分類法。假設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 個位元組,否則這些位元組將用於物件的標記字、類別指標和記憶體填充。
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 向左移動該標誌的位置。如果boolean
值為false
,則它使用零而不設定任何位元。最後,我們使用位元或運算子來組合所有這些值。
另外,讓我們寫一個方法來轉換回boolean
:
static boolean convertToBoolean(short value, short flagPosition) {
return (value >> flagPosition & 1) == 1;
}
上面的方法從打包的short
中提取單一boolean
值。
接下來,讓我們刪除四個boolean
字段並用一個short flag
替換它們:
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 位元組。
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 庫來減少記憶體佔用:
MutableList<DinosaurNew> dinosaurPrimitivesList = FastList.newListWith(dinosaurNew);
LOGGER.info(String.valueOf(GraphLayout.parseInstance(dinosaurNew).totalSize()));
在上面的程式碼中,我們使用第三方集合庫建立了Dinosaur
的MutableList
集合,該集合向控制台輸出 584 位元組。這意味著標準集合庫和 Eclipse 集合庫之間存在 32 位元組的差異。
7.3.原始集合
此外,Eclipse Collection 還提供原始類型的集合。這些原始集合避免了包裝類別的開銷成本,從而節省了大量的記憶體。
此外,Trove、Fastutil 和 Colt 等庫提供原始集合。此外,與 Java 標準集合相比,這些第三方函式庫具有更高的效能效率。
八、結論
在本文中,我們學習如何估計 Java 基元和物件的記憶體大小。此外,我們還估計了應用程式的初始記憶體佔用量,然後使用一些方法,例如使用原語而不是包裝器、組合相關物件的類別以及使用short
等窄類型來減少記憶體佔用量。
與往常一樣,完整的範例程式碼可以在 GitHub 上找到。