减少GC开销的5个编码技巧
在這篇文章中,我們來了解一下讓代碼變得高效的五種技巧,這些技巧可以使我們的垃圾收集器(GC)在分配內(nèi)存以及釋放內(nèi)存上面,占用更少的CPU時間,減少GC的開銷。當(dāng)內(nèi)存被回收的時候,GC處理很長時間經(jīng)常會導(dǎo)致我們的代碼中斷(又叫做”stop the world”)。
背景
GC用來處理大量的短期的對象的分配(試想打開一個web頁面,一旦頁面被加載之后,被分配內(nèi)存的大部分對象都會被廢棄)。
GC使用一個被稱作”新生代”堆空間來完成這件事情。”新生代”是用來存放新建對象的堆內(nèi)存。每一個對象都有一個”age”(存儲在對象的頭信息中),用來定義存放很多沒有被回收的垃圾集合。一旦一個確定的”age”到達(dá),對象就會被復(fù)制到堆中的另一塊空間,這個空間被稱作”幸存者空間”或者”老年代空間”。(譯者注:實(shí)際上幸存者空間位于新生代空間中,原文有誤,不過這里暫時按照原文來翻譯,更詳細(xì)的內(nèi)容請點(diǎn)擊成為JavaGC專家Part I — 深入淺出Java垃圾回收機(jī)制)
雖然這樣很有效,但是還是有很大代價的。減少臨時分配的數(shù)量確實(shí)可以幫助我們增加吞吐量,尤其是在大規(guī)模數(shù)據(jù)的環(huán)境下,或者資源有限制的app中。
下面的五種代碼方式可以更加有效的利用內(nèi)存,并且不需要花費(fèi)很多的時間,也不會降低代碼可讀性。
1、避免隱式的String字符串
String字符串是我們管理的每一個數(shù)據(jù)結(jié)構(gòu)中不可分割的一部分。它們在被分配好了之后不可以被修改。比如”+”操作就會分配一個鏈接兩個字符串的新的字符串。更糟糕的是,這里分配了一個隱式的StringBuilder對象來鏈接兩個String字符串。
例如:
| 1 | a = a + b; // a and b are Strings |
編譯器在背后就會生成這樣的一段兒代碼:
| 1 2 3 4 | StringBuilder temp = new StringBuilder(a). temp.append(b); a = temp.toString(); // 一個新的 String 對象被分配 // 第一個對象 “a” 現(xiàn)在可以說是垃圾了 |
它變得更糟糕了。
讓我們來看這個例子:
| 1 2 3 | String result = foo() + arg; result += boo(); System.out.println(“result = “ + result); |
在這個例子中,背后有三個StringBuilders 對象被分配 – 每一個都是”+”的操作所產(chǎn)生,和兩個額外的String對象,一個持有第二次分配的result,另一個是傳入到print方法的String參數(shù),在看似非常簡單的一段語句中有5個額外的對象。
試想一下在實(shí)際的代碼場景中會發(fā)生什么,例如,通過xml或者文件中的文本信息生成一個web頁面的過程。在嵌套循環(huán)結(jié)構(gòu),你將會發(fā)現(xiàn)有成百上千的對象被隱式的分配了。盡管VM有處理這些垃圾的機(jī)制,但還是有很大代價的 – 代價也許由你的用戶來承擔(dān)。
解決方案:
減少垃圾對象的一種方式就是善于使用StringBuilder 來建對象,下面的例子實(shí)現(xiàn)了與上面相同的功能,然而僅僅生成了一個StringBuilder 對象,和一個存儲最終result 的String對象。
| 1 2 3 | StringBuilder value = new StringBuilder(“result = “); value.append(foo()).append(arg).append(boo()); System.out.println(value); |
通過留心String和StringBuilder被隱式分配的可能,可以減少分配的短期的對象的數(shù)量,尤其在有大量代碼的位置。
2、計(jì)劃好List的容量
像ArrayList這樣的動態(tài)集合用來存儲一些長度可變化數(shù)據(jù)的基本結(jié)構(gòu)。ArrayList和一些其他的集合(如HashMap、TreeMap),底層都是通過使用Object[]數(shù)組來實(shí)現(xiàn)的。而String(它們自己包裝在char[]數(shù)組中),char數(shù)組的大小是不變的。那么問題就出現(xiàn)了,如果它們的大小是不變的,我們怎么能放item記錄到集合中去呢?答案顯而易見:分配更多的數(shù)組。
看下面的例子:
| 1 2 3 4 5 6 7 | List<Item> items = new ArrayList<Item>(); ?? for (int i = 0; i < len; i++) { Item item = readNextItem(); items.add(item); } |
len的值決定了循環(huán)結(jié)束時items 最終的大小。然而,最初,ArrayList的構(gòu)造器并不知道這個值的大小,構(gòu)造器會分配一個默認(rèn)的Object數(shù)組的大小。一旦內(nèi)部數(shù)組溢出,它就會被一個新的、并且足夠大的數(shù)組代替,這就使之前分配的數(shù)組成為了垃圾。
如果執(zhí)行數(shù)千次的循環(huán),那么就會進(jìn)行更多次數(shù)的新數(shù)組分配操作,以及更多次數(shù)的舊數(shù)組回收操作。對于在大規(guī)模環(huán)境下運(yùn)行的代碼,這些分配和釋放的操作應(yīng)該盡可能從CPU周期中剔除。
解決方案:
無論什么時候,盡可能的給List或者M(jìn)ap分配一個初始容量,就像這樣:
| 1 | List<MyObject> items = new ArrayList<MyObject>(len); |
因?yàn)長ist初始化,有足夠的容量,所有這樣可以減少內(nèi)部數(shù)組在運(yùn)行時不必要的分配和釋放。如果你不知道確定的大小,最好估算一下這個值的平均值,添加一些緩沖,防止意外溢出。
3、使用高效的含有原始類型的集合
當(dāng)前版本的Java編譯器對于含有基本數(shù)據(jù)類型的鍵的數(shù)組以及Map的支持,是通過“裝箱”來實(shí)現(xiàn)的 – 自動裝箱就是將原始數(shù)據(jù)裝入一個對應(yīng)的對象中,這個對象可被GC分配和回收。
這個會有一些負(fù)面的影響。Java可以通過使用內(nèi)部數(shù)組實(shí)現(xiàn)大多數(shù)的集合。對于每一條被添加到HashMap中的key/value記錄,都會分配一個存儲key和value的內(nèi)部對象。當(dāng)處理map的時候非常可怕,這意味著,每當(dāng)你放一條記錄到map中的時候,就會有一次額外的分配和釋放操作發(fā)生。這很可能導(dǎo)致數(shù)量過大,而不得不重新分配新的內(nèi)部數(shù)組。當(dāng)處理有成百上千條甚至更多記錄的Map時,這些內(nèi)部分配的操作將會使GC的成本增加。
一種常見的情況就是保存一個原始類型(如id)和一個對象之間的映射。由于Java的HashMap設(shè)計(jì)只能包含對象類型(而非原始類型),這意味著,每個map的插入操作都可能分配一個額外的對象來存儲原始類型(即裝箱)。
Integer.valueOf 方法緩存在-128 – 127之間的數(shù)值,但是對于范圍之外的每一個數(shù)值,除了內(nèi)部的key/value記錄對象之外,一個新的對象也將會分配。這很可能超過了GC對于map三倍的開銷。對于一個C++開發(fā)者來說,這真是讓人不安的消息,在C++中,STL 模板可以非常高效地解決這樣的問題。
很幸運(yùn),這個問題將會在Java的下一個版本得到解決。到那時,這將會被一些提供基本的樹形結(jié)構(gòu)(Tree)、映射(Map),以及List等Java的基本類型的庫迅速處理。我強(qiáng)力推薦Trove,我已經(jīng)使用很長時間了,并且它在處理大規(guī)模的代碼時真的可以減小GC的開銷。
4、使用數(shù)據(jù)流(Streams)代替內(nèi)存緩沖區(qū)(in-memory buffers)
在服務(wù)器應(yīng)用程序中,我們操作的大多數(shù)的數(shù)據(jù)都是以文件或者是來自另一個web服務(wù)器或DB的網(wǎng)絡(luò)數(shù)據(jù)流的形式呈現(xiàn)給我們。大多數(shù)情況下,傳入的數(shù)據(jù)都是序列化的形式,在我們使用它們之前需要被反序列化成Java對象。這個過程非常容易產(chǎn)生大量的隱式分配。
最簡單的做法就是通過ByteArrayInputStream,ByteBuffer 把數(shù)據(jù)讀入內(nèi)存中,然后再進(jìn)行反序列化。
這是一個糟糕的舉動,因?yàn)橥暾臄?shù)據(jù)在構(gòu)造新的對象的時候,你需要為其分配空間,然后立刻又釋放空間。并且,由于數(shù)據(jù)的大小你又不知道,你只能猜測 – 當(dāng)超過初始化容量的時候,不得不分配和釋放byte[]數(shù)組來存儲數(shù)據(jù)。
解決方案非常簡單。像Java自帶的序列化工具以及Google的Protocol Buffers等,它們可以將來自于文件或網(wǎng)絡(luò)流的數(shù)據(jù)進(jìn)行反序列化,而不需要保存到內(nèi)存中,也不需要分配新的byte數(shù)組來容納增長的數(shù)據(jù)。如果可以的話,你可以將這種方法和加載數(shù)據(jù)到內(nèi)存的方法比較一下,相信GC會很感謝你的。
5、List集合
不變性是很美好的,但是在大規(guī)模情境下,它就會有嚴(yán)重的缺陷。當(dāng)傳入一個List對象到方法中的情景。
當(dāng)方法返回一個集合,通常會很明智的在方法中創(chuàng)建一個集合對象(如ArrayList),填充它,并以不變的集合的形式返回。
有些情況下,這并不會得到很好的效果。最明顯的就是,當(dāng)來自多個方法的集合調(diào)用一個final集合。因?yàn)椴蛔冃?#xff0c;在大規(guī)模數(shù)據(jù)情況下,會分配大量的臨時集合。
這種情況的解決方案將不會返回新的集合,而是通過使用單獨(dú)的集合當(dāng)做參數(shù)傳入到那些方法代替組合的集合。
例子1(低效率):
| 1 2 3 4 5 6 | List<Item> items = new ArrayList<Item>(); for (FileData fileData : fileDatas) { // 每一次調(diào)用都會創(chuàng)建一個存儲內(nèi)部臨時數(shù)組的臨時的列表 items.addAll(readFileItem(fileData)); } |
例子2:
| 1 2 3 4 5 6 7 | List<Item> items = new ArrayList<Item>(fileDatas.size() * avgFileDataSize * 1.5); ?? for (FileData fileData : fileDatas) { readFileItem(fileData, items); // 在內(nèi)部添加記錄 } |
在例子2中,當(dāng)違反不變性規(guī)則的時候(這通常應(yīng)該被遵守),可以節(jié)省N個list的分配(以及任何臨時數(shù)組的分配)。這將是對你GC的一個大大的優(yōu)惠。
轉(zhuǎn)載于:https://www.cnblogs.com/xianDan/p/4846820.html
總結(jié)
以上是生活随笔為你收集整理的减少GC开销的5个编码技巧的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 涂鸦WIFI模组方案(模组 SDK)
- 下一篇: Jlink commander、MCU