本篇文章,我們將學(xué)習(xí)如何估計(jì)所有可能的 Java 對象或原始數(shù)據(jù)類型(?Primitive
?)。這些知識(shí)非常重要,尤其是 對于生產(chǎn)應(yīng)用程序。你可能認(rèn)為現(xiàn)在大多數(shù)服務(wù)器都有足夠的內(nèi)存來滿足所有可能的應(yīng)用程序需求。在某種程度而言你是對的——硬件,它相對于一個(gè)開發(fā)人員的薪水算是比較便宜。但是,仍然很容易滿足非常消耗性的情況,例如:
- 緩存特別是長字符串。
- 結(jié)構(gòu)有一個(gè)大數(shù)目的記錄(例如,具有節(jié)點(diǎn)樹從構(gòu)建龐大的XML文件)。
- 從數(shù)據(jù)庫復(fù)制數(shù)據(jù)的任何結(jié)構(gòu)。
在下一步中,我們開始估計(jì)從原始結(jié)構(gòu)到更復(fù)雜結(jié)構(gòu)的 Java 對象。
Java 原始數(shù)據(jù)類型
Java ?Primitives
?的大小是眾所周知的,并且從包裝盒中提供:
32 位和 64 位系統(tǒng)的最小內(nèi)存字
32 位和 64 位內(nèi)存字的最小大小分別為 8 和 16 字節(jié)。任何較小的長度都按 8 舍入。在計(jì)算過程中,我們將考慮這兩種情況。
由于內(nèi)存(字節(jié)大小)結(jié)構(gòu)的性質(zhì),任何內(nèi)存都是 8 的倍數(shù),如果不是系統(tǒng)會(huì)自動(dòng)添加額外的字節(jié)(但 32/64 系統(tǒng)的最小大小仍然是 8 和 16 字節(jié))
Java對象
Java 對象內(nèi)部沒有字段,根據(jù)規(guī)范,它只有稱為header 的元數(shù)據(jù)。Header 包含兩部分:標(biāo)記詞(?Mark Word
?) 和 類指針(?class pointer
?)。
功能用途 | 大小 32 位操作系統(tǒng) | 大小 64 位 | |
標(biāo)記詞 | 鎖(同步)、垃圾收集器信息、哈希碼(來自本機(jī)調(diào)用) | 4字節(jié) | 8 字節(jié) |
類指針 | 塊指針,數(shù)組長度(如果對象是數(shù)組) | 4字節(jié) | 4字節(jié) |
全部的 | 8 字節(jié)(0 字節(jié)偏移) | 16 字節(jié)(4 字節(jié)偏移) |
以及它在 Java Memory 中的樣子:
Java 原始包裝器
在 Java 中,除了基元和引用(最后一個(gè)是隱藏的)之外,一切都是對象。所以所有的包裝類只是包裝相應(yīng)的原始類型。所以包裝器大小一般=對象頭對象+內(nèi)部原始字段大小+內(nèi)存間隙。所有原始包裝器的大小如下表所示:
類型 | 內(nèi)部原始尺寸 | 標(biāo)題 32 位 | 標(biāo)題 64 位 | 總大小 32 位 | 總大小 64 位 | 總大小 32 位,帶間隙 | 總大小 64 位,帶間隙 |
Byte | 1 | 8 | 12 | 9 | 13 | 16 | 16 |
Boolean | 1 | 8 | 12 | 9 | 13 | 16 | 16 |
Int | 4 | 8 | 12 | 12 | 16 | 16 | 16 |
Float | 4 | 8 | 12 | 12 | 16 | 16 | 16 |
Short | 2 | 8 | 12 | 10 | 14 | 16 | 16 |
Char | 2 | 8 | 12 | 10 | 14 | 16 | 16 |
Long | 8 | 8 | 12 | 16 | 20 | 16 | 24 |
Double | 8 | 8 | 12 | 16 | 20 | 16 | 24 |
Java數(shù)組
Java數(shù)組是非常相似的對象-他們也有不同的原始和對象值。該數(shù)組包含headers、數(shù)組長度及其單元格(到基元)或?qū)ζ鋯卧竦囊茫▽τ趯ο螅?。為了方便大家清楚明白,我們繪制一個(gè)原始整數(shù)和大整數(shù)(包裝器)的數(shù)組。
基元數(shù)組(在我們的例子中是整數(shù))
對象數(shù)組(在我們的例子中是位整數(shù))
因此,你可以看到原始數(shù)組和對象數(shù)組之間的主要區(qū)別——帶有引用的附加層。在這個(gè)例子中,大多數(shù)內(nèi)存丟失的原因是使用一個(gè)整數(shù)包裝器,它增加了 12 個(gè)額外的字節(jié)(比原始數(shù)據(jù)多 3 倍?。?/p>
Java類
現(xiàn)在我們知道如何計(jì)算 Java Object、Java Primitive 和 Java Primitive Wrapper 和 Arrays。Java 中的任何類都不過是一個(gè)混合了所有提到的組件的對象:
- 標(biāo)頭(32/64 位操作系統(tǒng)的 8 或 12 字節(jié))。
- 原始(類型字節(jié)取決于原始類型)。
- 對象/類/數(shù)組(4 字節(jié)參考大小)。
Java字符串
Java string 它是類的一個(gè)很好的例子,所以除了 header 和 hash 之外,它還封裝了 char 數(shù)組,所以對于長度為 500 的長字符串,我們有:
String | 封裝字符數(shù)組 | ||
header | 8-12 字節(jié)(32/64 位操作系統(tǒng)) | header | 8-12 字節(jié)(32/64 位操作系統(tǒng)) |
hash | 4字節(jié) | 數(shù)組長度 | 4字節(jié) |
char[](參考) | 4字節(jié) | 500 個(gè)字符 | 500 * 2 字節(jié) = 1000 字節(jié) |
字符串大小 | 16 或 24 字節(jié) | 總陣列大小 | 16(考慮間隙)+ 1000 字節(jié) = 1016 字節(jié) |
總尺寸 | (16 or 24) + 1016 = 1032 or 1040 bytes (for 32 and 64 bit os) |
但是我們要考慮到Java String 類有不同的實(shí)現(xiàn),但一般來說,主要大小由char 數(shù)組保存。
如何以編程方式計(jì)算
使用運(yùn)行時(shí)檢查大小 freeMemory
最簡單但不可靠的方法是比較內(nèi)存初始化前后總內(nèi)存和空閑內(nèi)存的差異:
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 庫。這個(gè)解決方案會(huì)讓您驚喜地發(fā)現(xiàn)我們可以輕松地調(diào)查任何對象/原語/數(shù)組。為此,您需要添加下一個(gè) Maven 依賴項(xiàng):
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.16</version>
</dependency>
并提供給 ClassLayout.parseInstance 你想要估計(jì)的任何內(nèi)容:
int primitive = 3; // put here any class/object/primitive/array etc
System.out.println(VM.current().details());
System.out.println(ClassLayout.parseInstance(primitive).toPrintable());
作為輸出,你將看到:
純文本1
# 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
? 等)來觀察此結(jié)構(gòu)或其他結(jié)構(gòu)消耗了多少內(nèi)存。但是這個(gè)解決方案是關(guān)于分析內(nèi)存而不是對象結(jié)構(gòu)。在下一段中,我們將使用 JProfiler 來確認(rèn)我們的計(jì)算是正確的。
制作數(shù)據(jù)庫緩存類并計(jì)算其大小
作為一個(gè)現(xiàn)實(shí)的例子,我們創(chuàng)建類來表示某個(gè)數(shù)據(jù)庫表中的數(shù)據(jù),其中包含 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;
}
}
所以現(xiàn)在我們創(chuàng)建了 100 萬用戶,對嗎?好吧,它在 User 類中的內(nèi)容并不重要——我們剛剛創(chuàng)建了 1M 個(gè)引用。內(nèi)存使用:1M * 4 字節(jié) = 4000 KB 或 4MB。甚至沒有開始,但支付了 4MB。
為 64 位系統(tǒng)分析 Java 內(nèi)存
為了確認(rèn)我們的計(jì)算,我們執(zhí)行我們的代碼并將?JProfile
?附加到它。作為替代方案,你可以使用任何其他分析器,例如?VisualVM
?(它是免費(fèi)的)。。
提示:當(dāng)你分析應(yīng)用程序時(shí),你可以不時(shí)運(yùn)行 GC 以清理未使用的對象。所以分析的結(jié)果:我們有User[]4M 記錄的參考點(diǎn),大小為 4000KB。當(dāng)我們分析:
作為下一步,我們初始化對象并將它們添加到我們的數(shù)組中(名稱是唯一的 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;
}
現(xiàn)在讓我們分析這個(gè)應(yīng)用程序并確認(rèn)我們的期望。你可能會(huì)提到某些值不精確,例如,字符串的大小為 24.224 而不是 24.000,但我們計(jì)算了所有字符串,包括內(nèi)部 JVM 字符串以及與Boolean.FALSE對象相關(guān)的相同內(nèi)容(估計(jì)為 16 字節(jié),但在配置文件中,它顯然Boolean.TRUE是32,因?yàn)橐彩荍VM 內(nèi)部使用)。
對于 1M 記錄,我們花費(fèi)了 212MB,它只有 5 個(gè)字段,并且所有字符串長度都受 36 個(gè)字符的限制。正如你所看到的,對象非常貪婪。讓我們改進(jìn) User 對象并用原語替換所有對象(字符串除外)。
僅僅通過將字段更改為基元,我們就節(jié)省了 56MB(大約 25% 的已用內(nèi)存)。但我們還通過刪除用戶和原語之間的額外引用來提高性能。
如何減少內(nèi)存消耗
讓我們列出一些簡單的方法來節(jié)省內(nèi)存消耗:
壓縮的 OOP
對于 64 位系統(tǒng),你可以使用壓縮的 oop 參數(shù)執(zhí)行 JVM。這是一個(gè)相當(dāng)大的主題,
將數(shù)據(jù)從子對象提取到父對象
如果設(shè)計(jì)允許將字段從子類移動(dòng)到父類,則可能會(huì)節(jié)省一些內(nèi)存:
帶有原語的集合
從前面的例子中,我們看到了原語包裝器是如何浪費(fèi)大量內(nèi)存的。原始數(shù)組不像 ?Java Collection
? 接口那樣用戶友好。但是還有一個(gè)替代方案:?Trove
?、?FastUtils
?、?Eclipse Collection
? 等。讓我們比較 ?來自?Trove 庫
?的?simpleArrayList<Double>
?和?TDoubleArrayListd
內(nèi)存使用情況。
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);
}
通常,關(guān)鍵區(qū)別隱藏在 ?Double Primitive Wrapper
? 對象中,而不是 ?ArrayList
?或 ?TDoubleArrayList
?結(jié)構(gòu)中。因此簡化 1M 記錄的差異:
?JProfiler
?證實(shí)了這一點(diǎn):
因此,只需更改集合,我們就可以輕松地將消耗減少 3 倍。
本文有關(guān) Java 內(nèi)存結(jié)構(gòu)的基本內(nèi)容就介紹到此結(jié)束了,感謝各位的閱讀。