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 依賴項:

org。openjdk。jol

jol-core

0。16

並輸入任何您想估計的東西: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 庫的記憶體使用情況。ArrayListTDoubleArrayList

TDoubleArrayList arrayList = new TDoubleArrayList(1_000_000);

List 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 物件大小:透過分析進行估計、測量和驗證