5个编码技巧以减少GC开销
在本文中,我們將介紹五種方法,這些方法可以使用有效的編碼來幫助垃圾回收器減少分配和釋放內(nèi)存的CPU時間,并減少GC開銷。 較長的GC通常會導(dǎo)致我們的代碼在回收內(nèi)存時被停止(也稱為“停止世界”)。
一些背景
GC的建立是為了處理大量短期對象的分配(例如渲染網(wǎng)頁等,其中大部分分配的對象在服務(wù)頁面后就已過時)。
GC使用所謂的“年輕”來完成此工作–分配新對象的堆段。 每個對象都有一個“年齡”(放置在對象的標(biāo)題位中),該年齡定義了它在沒有回收的情況下“生存”了多少個集合。 一旦達(dá)到一定年齡,該對象將被復(fù)制到堆中稱為“幸存者”或“舊”世代的另一部分。
該過程雖然有效,但仍然要付出代價。 能夠減少臨時分配的數(shù)量確實可以幫助我們提高吞吐量,尤其是在大規(guī)模應(yīng)用程序中。
以下是五種我們可以編寫日常代碼的方法,這些代碼可以提高內(nèi)存效率,而不必花費(fèi)大量時間或降低代碼的可讀性。
1.避免隱式字符串
字符串幾乎是我們管理的每個數(shù)據(jù)結(jié)構(gòu)不可或缺的一部分。 它們比其他原始值重得多,它們對內(nèi)存使用量的影響更大。
要注意的最重要的事情之一是字符串是不可變的 。 分配后不能修改它們。 用于連接的運(yùn)算符(例如“ +”)實際上分配了一個新的String,其中包含要連接的字符串的內(nèi)容。 更糟糕的是,有一個隱式的StringBuilder對象被分配來實際完成組合它們的工作。
例如 -
a = a + b; // a and b are Strings編譯器在后臺生成可比較的代碼:
StringBuilder temp = new StringBuilder(a). temp.append(b); a = temp.toString(); // a new String is allocated here.// The previous “a” is now garbage.但情況變得更糟。
讓我們看這個例子–
String result = foo() + arg; result += boo(); System.out.println(“result = “ + result);在此示例中,我們在后臺分配了3個StringBuilder-每個加號操作一個,另外兩個Strings-一個用于保存第二個賦值的結(jié)果,另一個用于保存?zhèn)鬟f給print方法的字符串。 那是另外5個對象 ,否則看起來很簡單。
考慮一下在現(xiàn)實世界中的代碼場景中會發(fā)生什么,例如生成網(wǎng)頁,使用XML或從文件中讀取文本。 嵌套在循環(huán)結(jié)構(gòu)中,您可能正在查看成百上千個隱式分配的對象。 盡管VM具有處理此問題的機(jī)制, 但它是有代價的 –由用戶支付。
解決方案:減少這種情況的一種方法是主動使用StringBuilder分配。 下面的示例獲得與上面的代碼相同的結(jié)果,同時僅分配一個StringBuilder和一個String來保存最終結(jié)果,而不是原來的五個對象。
StringBuilder value = new StringBuilder(“result = “); value.append(foo()).append(arg).append(boo()); System.out.println(value);通過注意隱式分配Strings和StringBuilders的方式,可以從實質(zhì)上減少大規(guī)模代碼位置中的短期分配量。
2.計劃清單的能力
諸如ArrayLists之類的動態(tài)集合是保存動態(tài)長度數(shù)據(jù)的最基本的結(jié)構(gòu)之一。 ArrayList和其他集合(例如HashMaps和TreeMaps)是使用基礎(chǔ)Object []數(shù)組實現(xiàn)的。 像字符串(char []數(shù)組本身包裝)一樣,數(shù)組也是不可變的。 顯而易見的問題變成了-如果基礎(chǔ)數(shù)組的大小是不變的,我們?nèi)绾卧诩现刑砑?放置項目? 答案也很明顯–通過分配更多的數(shù)組 。
讓我們看這個例子–
List<Item> items = new ArrayList<Item>();for (int i = 0; i < len; i++) {Item item = readNextItem();items.add(item); }len的值確定循環(huán)結(jié)束后項目的最終長度。 但是,此值對于ArrayList的構(gòu)造函數(shù)是未知的,該構(gòu)造函數(shù)分配具有默認(rèn)大小的新Object數(shù)組。 每當(dāng)超出內(nèi)部陣列的容量時,就會用足夠長的新陣列替換內(nèi)部陣列的容量,從而使以前的陣列成為垃圾。
如果要執(zhí)行數(shù)千次循環(huán),則可能會強(qiáng)制分配一個新數(shù)組,并多次收集前一個數(shù)組。 對于在大規(guī)模環(huán)境中運(yùn)行的代碼,這些分配和取消分配都從計算機(jī)的CPU周期中扣除。
解決方案:盡可能分配具有初始容量的列表和地圖,如下所示:
List<MyObject> items = new ArrayList<MyObject>(len);這樣可以確保在運(yùn)行時不會發(fā)生內(nèi)部數(shù)組的不必要分配和取消分配,因為列表現(xiàn)在具有足夠的容量開始。 如果您不知道確切的大小,最好對平均大小進(jìn)行估算(例如1024、4096),并添加一些緩沖區(qū)以防止意外溢出。
3.使用有效的原始集合
Java編譯器的當(dāng)前版本通過使用“裝箱”來支持具有原始鍵或值類型的數(shù)組或映射-將原始值包裝在可由GC分配和回收的標(biāo)準(zhǔn)對象中。
這可能會帶來一些負(fù)面影響 。 Java使用內(nèi)部數(shù)組實現(xiàn)大多數(shù)集合。 對于添加到HashMap的每個鍵/值條目,分配一個內(nèi)部對象來容納兩個值。 這在處理地圖時是必不可少的,這意味著您每次將商品放入地圖時都會進(jìn)行額外的分配和可能的重新分配。 還可能會增加容量并不得不重新分配新的內(nèi)部陣列。 當(dāng)處理包含數(shù)千個或更多條目的大型地圖時,這些內(nèi)部分配可能會增加GC的成本。
一種非常常見的情況是在原始值(例如Id)和對象之間保留映射。 由于Java的HashMap是為保存對象類型(相對于基元)而構(gòu)建的,因此這意味著映射中的每個插入都可以潛在地分配另一個對象來保存基元值(“裝箱”它)。
標(biāo)準(zhǔn)的Integer.valueOf方法緩存0到255之間的值,但是對于每個大于0的數(shù)字,除了內(nèi)部鍵/值輸入對象之外,還將分配一個新對象。 這可能會使映射的GC開銷增加三倍以上。 對于那些來自C ++背景的人來說,這確實是令人不安的消息,因為STL模板可以非常有效地解決此問題。
幸運(yùn)的是,此問題正在Java的下一版本中進(jìn)行。 在此之前,一些出色的庫已經(jīng)對其進(jìn)行了相當(dāng)有效的處理,這些庫為Java的每種原始類型提供了原始樹,映射和列表。 我強(qiáng)烈推薦Trove ,我已經(jīng)使用了很長時間 ,發(fā)現(xiàn)它確實可以減少大規(guī)模代碼中的GC開銷。
4.使用流而不是內(nèi)存緩沖區(qū)
我們在服務(wù)器應(yīng)用程序中處理的大多數(shù)數(shù)據(jù)都是通過文件或數(shù)據(jù)從另一個Web服務(wù)或數(shù)據(jù)庫通過網(wǎng)絡(luò)流式傳輸給我們的。 在大多數(shù)情況下,傳入的數(shù)據(jù)是序列化的,在我們開始對其進(jìn)行操作之前,需要將其反序列化為Java對象。 這個階段很容易出現(xiàn)大量的隱式分配 。
通常最簡單的方法是使用ByteArrayInputStream,ByteBuffer將數(shù)據(jù)讀取到內(nèi)存中,然后將其傳遞給反序列化代碼。
這可能是一個錯誤的舉動 ,因為您需要在構(gòu)造出新的對象時為整個數(shù)據(jù)分配和釋放空間。 而且,由于數(shù)據(jù)的大小可能是未知的,您猜到了–您必須分配和取消分配內(nèi)部byte []數(shù)組,以便在數(shù)據(jù)超出初始緩沖區(qū)的容量時容納它們。
解決方案非常簡單。 大多數(shù)持久性庫(例如Java的本機(jī)序列化,Google的協(xié)議緩沖區(qū)等)都可以直接從傳入的文件或網(wǎng)絡(luò)流中反序列化數(shù)據(jù),而不必將其保留在內(nèi)存中,也不必分配新的內(nèi)部字節(jié)數(shù)組來保存數(shù)據(jù)。隨著數(shù)據(jù)的增長。 如果可以的話,請采用這種方法,而不是將數(shù)據(jù)加載到內(nèi)存。 您的GC將感謝您。
5.匯總列表
不變性是一件美麗的事情,但是在某些大規(guī)模情況下,它可能會存在一些嚴(yán)重的缺點。 一種情況是在方法之間傳遞List對象。
從函數(shù)返回集合時,通常建議在方法內(nèi)創(chuàng)建集合對象(例如ArrayList),將其填充并以不可變的Collection接口的形式返回。
在某些情況下這不能很好地工作 。 最引人注目的是將集合從多個方法調(diào)用中聚合到最終集合中。 雖然不變性提供了更高的清晰度,但在大規(guī)模情況下,這也可能意味著臨時集合的大量分配。
在這種情況下,解決方案不是返回新的集合,而是將值聚合到單個集合中,該集合作為參數(shù)傳遞到這些方法中。
示例1(效率低下)–
List<Item> items = new ArrayList<Item>();for (FileData fileData : fileDatas) {// Each invocation creates a new interim list with possible// internal interim arraysitems.addAll(readFileItem(fileData)); }示例2 –
List<Item> items =new ArrayList<Item>(fileDatas.size() * avgFileDataSize * 1.5);for (FileData fileData : fileDatas) {readFileItem(fileData, items); // fill items inside }示例2在遵守不變性規(guī)則(通常應(yīng)遵守該規(guī)則)的同時,可以保存N個列表分配(以及任何臨時數(shù)組分配)。 在大規(guī)模情況下,這可以為您的GC帶來福音。
補(bǔ)充閱讀
- 字符串實習(xí)– http://plumbr.eu/blog/reducing-memory-usage-with-string-intern
- 高效的包裝器– http://vanillajava.blogspot.co.il/2013/04/low-gc-coding-efficient-listeners.html
- 使用Trove – http://java-performance.info/primitive-types-collections-trove-library/
此帖子也可以在Speaker Deck上找到
翻譯自: https://www.javacodegeeks.com/2013/07/5-coding-hacks-to-reduce-gc-overhead.html
總結(jié)
以上是生活随笔為你收集整理的5个编码技巧以减少GC开销的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 安卓音乐软件破解版(安卓音乐软件)
- 下一篇: 集成JavaFX和Swing(修订版)