Java中使用HashMap時指定初始化容量性能解析
正文
一些Java編程老手在做CodeReview時,都會告訴其他人,使用HashMap時建議指定容量大小,原因是指定容量后,代碼性能會更好一些。后來隨著阿里Java開發(fā)手冊在業(yè)內(nèi)廣為傳播,這一點早已深入人心,我自己也早已習(xí)慣在使用HashMap時指定容量大小。
但我今天突發(fā)奇想,想知道指定容量和不指定容量時性能究竟有多少的差異,測試部分測試數(shù)據(jù)的結(jié)果讓我大跌眼睛,有些情況下指定容量的性能還比不指定容量時差??! ,但其他部分還是很符合我之前的認知的。
openjdk17和jmh單線程測試
先說下我的測試平臺和測試方法,我使用了openjdk17和jmh單線程測試,測試代碼如下:
@Benchmark @BenchmarkMode(Mode.Throughput) @Measurement(iterations = 2, time = 5) @Threads(1) @Fork(0) @Warmup(iterations = 1, time = 5) public void withoutCap() { Map<Integer, Integer> map = new HashMap<>(); for (int i = 0; i < CAP; i++) { map.put(random.nextInt(), 1); } } @Benchmark @BenchmarkMode(Mode.Throughput) @Measurement(iterations = 2, time = 5) @Threads(1) @Fork(0) @Warmup(iterations = 1, time = 5) public void withCap() { Map<Integer, Integer> map = new HashMap<>(CAP); for (int i = 0; i < CAP; i++) { map.put(random.nextInt(), 1); } }
這里為了避免Java中小數(shù)據(jù)緩存,我特意使用了隨機數(shù)作為KEY,而VALUE一視同仁都使用了1。兩個方法就是新建一個HashMap并不斷往map里put數(shù)據(jù),唯一差異就是一個指定了CAP參數(shù)。 在我設(shè)置了不同參數(shù)后,得到了以下數(shù)據(jù)(越高越好):
數(shù)據(jù)量 | 不指定容量(ops/s) | 指定容量(ops/s) |
---|---|---|
2 | 51095433 | 24000032 |
4 | 25161756 | 11813275 |
8 | 10767176 | 5900641 |
16 | 2978374 | 2987958 |
32 | 1231637 | 1545394 |
64 | 567643 | 764260 |
256 | 129350 | 185540 |
1024 | 27475 | 35799 |
1025 | 27195 | 68466 |
4096 | 6681 | 9937 |
32768 | 807 | 1177 |
65536 | 377 | 567 |
可以看出,容量16是個分水嶺,當(dāng)容量為16時,二者幾乎沒啥差異,這也很容易理解,當(dāng)不指定容量時默認初始容量就是16。
- 但容量大于16時,指定容量時的性能會高于不指定時的性能,隨著數(shù)量的增加,前者會比后者性能高出50%。
- 但當(dāng)數(shù)據(jù)量小于16時,不指定容量大小反而性能更高,最多甚至相差2倍,這就和我們之前的認知不一樣了。
上面數(shù)據(jù)中還有個很奇怪的點,那就是當(dāng)數(shù)據(jù)量為1025時,性能居然還高于1024,而且差異巨大。就好比別人比你多干了1份活,但用的時間比你少一半。我跑了多次都是這個結(jié)果,這不是測試誤差,這個結(jié)果和計算機底層存儲實現(xiàn)有關(guān),具體原理可以參考問題 為什么轉(zhuǎn)置512x512的矩陣比轉(zhuǎn)置513x513的矩陣慢?
備注:以上數(shù)據(jù)經(jīng)過多次運行測試,數(shù)據(jù)雖有波動,但數(shù)據(jù)波動基本都在3%以內(nèi)。
那為什么在大數(shù)據(jù)量的情況下,指定容量的代碼性能會更好呢?這就得說到HashMap的實現(xiàn)原理,更詳細內(nèi)容可以參考我之前寫的HashMap源碼淺析。這里為了方便大家直觀地理解性能差異產(chǎn)生的原因,我們用牧場養(yǎng)羊類比下。
假設(shè)你要開始養(yǎng)羊,你得現(xiàn)有場地吧,假設(shè)你先找了塊小場地,但隨著你的羊群發(fā)展壯大,場地不夠用了,你就得搬到一個更大的新場地,如果發(fā)展速度特別快,你就得頻繁搬家,搬家就逐漸變成了負擔(dān)。但如果你一開始就知道你最多能養(yǎng)多少的羊,直接找個足夠大的場地,不就能省去一直搬家的成本了嗎!
這里你把羊類比成數(shù)據(jù),場地類比為內(nèi)存,在HashMap中,如果開始不指定容量大小,JVM默認會給你一個非常小的(16)的容量空間,如果之后數(shù)據(jù)量變多,就需要重新申請更大的空間,并把數(shù)據(jù)遷移到新空間上,于是額外增加了時間消耗。這便是性能差異產(chǎn)生的原因。
但當(dāng)容量小于16時,指定容量的方式反而性能更差。這個我之前從未看過其他資料有說過,我簡單談下自己的分析和理解。 當(dāng)調(diào)用new HashMap()和new HashMap(CAP)時,分別執(zhí)行了不同的構(gòu)造函數(shù),而二者的構(gòu)造函數(shù)的邏輯是有差異的,當(dāng)指定容量時,執(zhí)行了容量參數(shù)檢查的代碼:
public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity); } static final int tableSizeFor(int cap) { int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1); return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }
不指定容量時,構(gòu)造方法內(nèi)只有一行this.loadFactor = DEFAULT_LOAD_FACTOR;
,在put的數(shù)據(jù)量一致時,后續(xù)所有的代碼執(zhí)行流程都是一致的,所以指定容量時,上面容量參數(shù)檢查的代碼帶來了額外的性能負擔(dān),所以導(dǎo)致數(shù)據(jù)量較小時指定容量時反而性能更差一些。
總結(jié)
最后回到文章標(biāo)題上來,Java中使用HashMap時指定初始化容量性能一定會更好嘛?答案是不一定,指定容量也有可能性能會更差。當(dāng)然,絕大多數(shù)情況下還是建議指定容量的,類似的還有ArrayList,也建議指定容量。 別人給出的結(jié)論不一定的完全正確的,只有知道產(chǎn)生結(jié)論的原因,才能更有效的利用這個結(jié)論。
以上就是Java中使用HashMap時指定初始化容量性能解析的詳細內(nèi)容,更多關(guān)于Java HashMap容量性能的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
使用Nacos實現(xiàn)動態(tài)路由的步驟和代碼示例
這篇文章主要介紹了使用 Nacos 實現(xiàn) Spring Cloud Gateway 的動態(tài)路由,本文給大家介紹了具體的實現(xiàn)步驟和代碼案例,感興趣的小伙伴跟著小編一起來看看吧2024-09-09搭建Spring MVC和Vue3的應(yīng)用程序的實現(xiàn)
本文主要介紹了搭建Spring MVC和Vue3的應(yīng)用程序的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-11-11如何使用try-with-resource機制關(guān)閉連接
這篇文章主要介紹了使用try-with-resource機制關(guān)閉連接的操作,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-07-07springboot+mybatis配置clickhouse實現(xiàn)插入查詢功能
這篇文章主要介紹了springboot+mybatis配置clickhouse實現(xiàn)插入查詢功能,本文通過實例代碼給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-08-08利用Spring?Boot和JPA創(chuàng)建GraphQL?API
這篇文章主要介紹了利用Spring?Boot和JPA創(chuàng)建GraphQL?API,GraphQL既是API查詢語言,也是使用當(dāng)前數(shù)據(jù)執(zhí)行這些查詢的運行時,下文更多相關(guān)內(nèi)容介紹需要的小伙伴可以參考一下2022-04-04springboot使用redis對單個對象進行自動緩存更新刪除的實現(xiàn)
本文主要介紹了springboot使用redis對單個對象進行自動緩存更新刪除的實現(xiàn),具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-08-08