天天看點

Java 對象大小:通過分析進行估計、測量和驗證

作者:一個即将退役的碼農

在本文中,我們将學習如何估計所有可能的 java 對象或原語。這些知識至關重要,特别是對于生産應用。您可能會認為現在大多數伺服器都有足夠的記憶體來滿足所有可能的應用程式需求。在某種程度上你是對的——硬體,與開發人員的薪水相比,它相當便宜。但是,仍然很容易遇到消耗嚴重的情況,例如:

  • 緩存,尤其是具有長字元串的緩存。
  • 具有大量記錄的結構(例如,具有從大型XML檔案建構的節點的樹)。
  • 從資料庫複制資料的任何結構。

在下一步中,我們開始估計從原始結構到更複雜的結構的 Java 對象。

Java語言

Java Kilomitive 的大小是衆所周知的,并且從盒子裡提供:

Java 對象大小:通過分析進行估計、測量和驗證

适用于 32 位和 64 位系統的最小記憶體字

32 位和 64 位的記憶體字的最小大小分别為 8 個和 16 個位元組。任何較小的長度都四舍五入 8。在計算過程中,我們将考慮這兩種情況。

Java 對象大小:通過分析進行估計、測量和驗證

由于記憶體(字大小)結構的性質,任何記憶體都是 8 的倍數,如果不是,系統将自動添加額外的位元組(但對于 8/32 系統,最小大小仍然是 16 和 64 位元組)

Java 對象大小:通過分析進行估計、測量和驗證

Java對象

Java 對象内部沒有字段,根據規範,它隻有稱為标頭的中繼資料。标頭包含兩部分:标記單詞和克拉斯指針。

功能目的 大小 32 位作業系統 大小 64 位
标記單詞 鎖定(同步),垃圾收集器資訊,哈希代碼(來自本機調用) 4 位元組 8 位元組
克拉斯指針 塊指針,數組長度(如果對象是數組) 4 位元組 4 位元組
8 位元組(0 位元組偏移量) 16 位元組(4 位元組偏移量)

以及它在 Java 記憶體中的樣子:

Java 對象大小:通過分析進行估計、測量和驗證

Java Primitive Wrappers

在Java中,除了原語和引用(最後一個是隐藏的)之外,所有内容都是對象。是以所有的包裝類都隻包裝相應的基元類型。是以包裝器大小一般=對象頭對象+内部基元字段大小+記憶體間隙。下表顯示了所有基元包裝器的大小:

Java 對象大小:通過分析進行估計、測量和驗證

Java數組

Java Array與對象非常相似——它們在基元值和對象值方面也有所不同。數組包含标題、數組長度及其單元格(到基元)或對其單元格的引用(對于對象)。為了澄清起見,讓我們繪制一個原始整數和大整數(包裝器)的數組。

基元數組(在本例中為整數)

Java 對象大小:通過分析進行估計、測量和驗證

對象數組(在本例中為位整數)

Java 對象大小:通過分析進行估計、測量和驗證

是以,您可以看到基元數組和對象數組之間的主要差別 — 帶有引用的附加層。在這個例子中,大多數記憶體丢失的原因 - 使用一個整數包裝器,它增加了12個額外的位元組(是原始位元組的3倍!

Java類

現在我們知道如何計算 Java 對象、Java 原語和 Java 語言包裝器和數組。Java 中的任何類都隻不過是一個混合了所有上述元件的對象:

  • 标頭(32/64 位作業系統為 8 或 12 個位元組)。
  • 基元(類型位元組取決于基元類型)。
  • 對象/類/數組(4 位元組引用大小)。

Java字元串

Java 字元串是該類的一個很好的例子,是以除了标頭和哈希之外,它還将 char 數組封裝在裡面,是以對于長度為 500 的長字元串,我們有:

Java 對象大小:通過分析進行估計、測量和驗證

但是我們必須考慮到 Java String 類有不同的實作,但一般來說,主要大小由 char 數組保持。

如何以程式設計方式計算

使用運作時檢查大小freeMemory

最簡單但不可靠的方法是比較記憶體初始化前後總記憶體和可用記憶體之間的差異:

long beforeUsedMem=Runtime.getRuntime().totalMemory()-Runtime.getRuntime().freeMemory();

Object[] myObjArray = new Object[100_000];

long afterUsedMem=Runtime.getRuntime().totalMemory()-Runtime.getRuntime().freeMemory();

使用 Jol 庫

最好的方法是使用Aleksey Shipilev編寫的Jol庫。該解決方案将使您驚喜地發現,我們可以如此輕松地研究任何對象/基元/數組。為此,您需要添加下一個 Maven 依賴項:

<dependency>

<groupId>org.openjdk.jol</groupId>

<artifactId>jol-core</artifactId>

<version>0.16</version>

</dependency>

并輸入任何您想估計的東西:ClassLayout.parseInstance

int primitive = 3; // put here any class/object/primitive/array etc

System.out.println(VM.current().details());

System.out.println(ClassLayout.parseInstance(primitive).toPrintable());

作為輸出,您将看到:

# Running 64-bit HotSpot VM.

# Using compressed oop with 0-bit shift.

# Using compressed klass with 3-bit shift.

# Objects are 8 bytes aligned.

# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]

# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]

java.lang.Integer object internals:

OFF SZ TYPE DESCRIPTION VALUE

0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)

8 4 (object header: class) 0x200021de

12 4 int Integer.value 3

Instance size: 16 bytes

Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

使用探查器

作為一個選項,您可以使用探查器(JProfiler,VM Visualizer,JConsole等)來觀察此結構或其他結構消耗了多少記憶體。但此解決方案是關于分析記憶體而不是對象結構。在下一段中,我們将使用 JProfiler 來确認我們的計算是否正确。

建立資料庫緩存類并計算其大小

作為一個實際的例子,我們建立類來表示來自某個資料庫表的資料,其中有 5 列,每個表中有 1.000.000 條記錄。

public class UserCache{

public static void main(String[] args){

User [] cachedUsers = new User[1_000_000];

while(true){}

}

private static class User{

Long id;

String name; //assume 36 characters long

Integer salary;

Double account;

Boolean isActive;

}

}

是以現在我們建立了1M使用者,對吧?好吧,不管它在 User 類中是什麼 — 我們隻是建立了 1M 引用。記憶體使用量:1M * 4 位元組 = 4000 KB 或 4MB。甚至沒有開始,但支付了 4MB。

分析 64 位系統的 Java 記憶體

為了确認我們的計算,我們執行我們的代碼并将JProfile附加到它。作為替代方案,您可以使用任何其他分析器,例如VisualVM(它是免費的)。如果您從未分析過您的應用程式,則可以檢視本文。下面是 JProfiler 中配置檔案螢幕的外觀示例(這隻是一個與我們的實作無關的示例)。

Java 對象大小:通過分析進行估計、測量和驗證

提示:分析應用時,可以不時運作 GC 來清理未使用的對象。是以分析的結果:我們有參考指向4M記錄,大小為4000KB。當我們剖析時User[]

Java 對象大小:通過分析進行估計、測量和驗證

下一步,我們初始化對象并将它們添加到我們的數組中(名稱是唯一的 UUID 36 長度大小):

for(int i = 0;i<1_000_000;i++){

User tempUser = new User();

tempUser.id = (long)i;

tempUser.name = UUID.randomUUID().toString();

tempUser.salary = (int)i;

tempUser.account = (double) i;

tempUser.isActive = Boolean.FALSE;

cachedUsers[i] = tempUser;

}

現在讓我們分析這個應用程式并确認我們的期望。您可能會提到某些值不精确,例如,字元串的大小是 24.224 而不是 24.000,但我們計算所有字元串,包括内部 JVM 字元串和與對象相關的相同字元串(估計為 16 位元組,但在配置檔案中,顯然是 32,因為 JVM 内部也使用)。Boolean.FALSEBoolean.TRUE

Java 對象大小:通過分析進行估計、測量和驗證

對于 1M 條記錄,我們花費 212MB,它隻有 5 個字段,所有字元串長度都受到 36 個字元的限制。是以正如你所看到的,對象是非常貪婪的。讓我們改進 User 對象并用原語替換所有對象(字元串除外)。

Java 對象大小:通過分析進行估計、測量和驗證

僅通過将字段更改為基元,我們就節省了 56MB(約占已用記憶體的 25%)。此外,我們還通過删除使用者和基元之間的其他引用來提高性能。

如何減少記憶體消耗

讓我們列出一些節省記憶體消耗的簡單方法:

壓縮的 OOP

對于 64 位系統,您可以使用壓縮的 oop 參數執行 JVM。

有興趣大家可以去學習下。

将資料從子對象提取到父對象

如果設計允許将字段從子類移動到父類,則可能會節省一些記憶體:

Java 對象大小:通過分析進行估計、測量和驗證

具有基元的集合

從前面的示例中,我們看到了基元包裝器如何浪費大量記憶體。原始數組不像Java集合接口那樣使用者友好。但還有另一種選擇:Trove、FastUtils、Eclipse Collection 等。讓我們比較一下simpleand 來自 Trove 庫的記憶體使用情況。ArrayList<Double>TDoubleArrayList

TDoubleArrayList arrayList = new TDoubleArrayList(1_000_000);

List<Double> doubles = new ArrayList<>();

for (int i = 0; i < 1_000_000; i++) {

arrayList.add(i);

doubles.add((double) i);

}

通常,關鍵差別隐藏在雙基元包裝器對象中,而不是在 ArrayList 或 TDoubleArrayList 結構中。是以,簡化 1M 記錄的差異:

Java 對象大小:通過分析進行估計、測量和驗證

JProfiler證明了這一點:

Java 對象大小:通過分析進行估計、測量和驗證

是以,隻需更改集合,我們就可以輕松地在 3 倍内減少消耗。

Java 對象大小:通過分析進行估計、測量和驗證

繼續閱讀