減小 GC 開銷的 5 個編碼技巧

在這篇文章中,咱們來了解一下讓代碼變得高效的五種技巧,這些技巧可使咱們的垃圾收集器(GC)在分配內存以及釋放內存上面,佔用更少的CPU時間,減小GC的開銷。當內存被回收的時候,GC處理很長時間常常會致使咱們的代碼中斷(又叫作」stop the world」)。html

 

背景web

 

GC用來處理大量的短時間的對象的分配(試想打開一個web頁面,一旦頁面被加載以後,被分配內存的大部分對象都會被廢棄)。數組

 

GC使用一個被稱做」新生代」堆空間來完成這件事情。」新生代」是用來存放新建對象的堆內存。每個對象都有一個」age」(存儲在對象的頭信息中),用來定義存放不少沒有被回收的垃圾集合。一旦一個肯定的」age」到達,對象就會被複制到堆中的另外一塊空間,這個空間被稱做」倖存者空間」或者」老年代空間」。(譯者注:實際上倖存者空間位於新生代空間中,原文有誤,不過這裏暫時按照原文來翻譯,更詳細的內容請點擊成爲JavaGC專家Part I — 深刻淺出Java垃圾回收機制緩存

 

http://www.importnew.com/1993.html服務器

 

雖然這樣頗有效,可是仍是有很大代價的。減小臨時分配的數量確實能夠幫助咱們增長吞吐量,尤爲是在大規模數據的環境下,或者資源有限制的app中。網絡

 

下面的五種代碼方式能夠更加有效的利用內存,而且不須要花費不少的時間,也不會下降代碼可讀性。數據結構

 

一、避免隱式的String字符串app

 

String字符串是咱們管理的每個數據結構中不可分割的一部分。它們在被分配好了以後不能夠被修改。好比」+」操做就會分配一個連接兩個字符串的新的字符串。更糟糕的是,這裏分配了一個隱式的StringBuilder對象來連接兩個String字符串。工具

 

例如:ui

 

a = a + b; // a and b are Strings

 

編譯器在背後就會生成這樣的一段兒代碼:

 

StringBuilder temp = new StringBuilder(a).

temp.append(b);

a = temp.toString(); // 一個新的 String 對象被分配

// 第一個對象 「a」 如今能夠說是垃圾了

 

它變得更糟糕了。

 

讓咱們來看這個例子:

 

String result = foo() + arg;

result += boo();

System.out.println(「result = 「 + result);

 

在這個例子中,背後有三個StringBuilders 對象被分配 – 每個都是」+」的操做所產生,和兩個額外的String對象,一個持有第二次分配的result,另外一個是傳入到print方法的String參數,在看似很是簡單的一段語句中有5個額外的對象。

 

試想一下在實際的代碼場景中會發生什麼,例如,經過xml或者文件中的文本信息生成一個web頁面的過程。在嵌套循環結構,你將會發現有成百上千的對象被隱式的分配了。儘管VM有處理這些垃圾的機制,但仍是有很大代價的 – 代價也許由你的用戶來承擔。

 

解決方案:

 

減小垃圾對象的一種方式就是善於使用StringBuilder 來建對象,下面的例子實現了與上面相同的功能,然而僅僅生成了一個StringBuilder 對象,和一個存儲最終result 的String對象。

 

StringBuilder value = new StringBuilder(「result = 「);

value.append(foo()).append(arg).append(boo());

System.out.println(value);

 

經過留心String和StringBuilder被隱式分配的可能,能夠減小分配的短時間的對象的數量,尤爲在有大量代碼的位置。

 

二、計劃好List的容量

 

像ArrayList這樣的動態集合用來存儲一些長度可變化數據的基本結構。ArrayList和一些其餘的集合(如HashMap、TreeMap),底層都是經過使用Object[]數組來實現的。而String(它們本身包裝在char[]數組中),char數組的大小是不變的。那麼問題就出現了,若是它們的大小是不變的,咱們怎麼能放item記錄到集合中去呢?答案顯而易見:分配更多的數組。

 

看下面的例子:

 

List<Item> items = new ArrayList<Item>();

  

for (int i = 0; i < len; i++)

{

Item item = readNextItem();

items.add(item);

}

 

len的值決定了循環結束時items 最終的大小。然而,最初,ArrayList的構造器並不知道這個值的大小,構造器會分配一個默認的Object數組的大小。一旦內部數組溢出,它就會被一個新的、而且足夠大的數組代替,這就使以前分配的數組成爲了垃圾。

 

若是執行數千次的循環,那麼就會進行更屢次數的新數組分配操做,以及更屢次數的舊數組回收操做。對於在大規模環境下運行的代碼,這些分配和釋放的操做應該儘量從CPU週期中剔除。

 

解決方案:

 

不管何時,儘量的給List或者Map分配一個初始容量,就像這樣:

 

List<MyObject> items = new ArrayList<MyObject>(len);

 

由於List初始化,有足夠的容量,全部這樣能夠減小內部數組在運行時沒必要要的分配和釋放。若是你不知道肯定的大小,最好估算一下這個值的平均值,添加一些緩衝,防止意外溢出。

 

三、使用高效的含有原始類型的集合

 

當前版本的Java編譯器對於含有基本數據類型的鍵的數組以及Map的支持,是經過「裝箱」來實現的 – 自動裝箱就是將原始數據裝入一個對應的對象中,這個對象可被GC分配和回收。

 

這個會有一些負面的影響。Java能夠經過使用內部數組實現大多數的集合。對於每一條被添加到HashMap中的key/value記錄,都會分配一個存儲key和value的內部對象。當處理map的時候很是可怕,這意味着,每當你放一條記錄到map中的時候,就會有一次額外的分配和釋放操做發生。這極可能致使數量過大,而不得不從新分配新的內部數組。當處理有成百上千條甚至更多記錄的Map時,這些內部分配的操做將會使GC的成本增長。

 

一種常見的狀況就是保存一個原始類型(如id)和一個對象之間的映射。因爲Java的HashMap設計只能包含對象類型(而非原始類型),這意味着,每一個map的插入操做均可能分配一個額外的對象來存儲原始類型(即裝箱)。

 

Integer.valueOf 方法緩存在-128 – 127之間的數值,可是對於範圍以外的每個數值,除了內部的key/value記錄對象以外,一個新的對象也將會分配。這極可能超過了GC對於map三倍的開銷。對於一個C++開發者來講,這真是讓人不安的消息,在C++中,STL 模板能夠很是高效地解決這樣的問題。

 

很幸運,這個問題將會在Java的下一個版本獲得解決。到那時,這將會被一些提供基本的樹形結構(Tree)、映射(Map),以及List等Java的基本類型的庫迅速處理。我強力推薦Trove,我已經使用很長時間了,而且它在處理大規模的代碼時真的能夠減少GC的開銷。

 

四、使用數據流(Streams)代替內存緩衝區(in-memory buffers)

 

在服務器應用程序中,咱們操做的大多數的數據都是以文件或者是來自另外一個web服務器或DB的網絡數據流的形式呈現給咱們。大多數狀況下,傳入的數據都是序列化的形式,在咱們使用它們以前須要被反序列化成Java對象。這個過程很是容易產生大量的隱式分配。

 

最簡單的作法就是經過ByteArrayInputStream,ByteBuffer 把數據讀入內存中,而後再進行反序列化。

 

這是一個糟糕的舉動,由於完整的數據在構造新的對象的時候,你須要爲其分配空間,而後馬上又釋放空間。而且,因爲數據的大小你又不知道,你只能猜想 – 當超過初始化容量的時候,不得不分配和釋放byte[]數組來存儲數據。

 

解決方案很是簡單。像Java自帶的序列化工具以及Google的Protocol Buffers等,它們能夠未來自於文件或網絡流的數據進行反序列化,而不須要保存到內存中,也不須要分配新的byte數組來容納增加的數據。若是能夠的話,你能夠將這種方法和加載數據到內存的方法比較一下,相信GC會很感謝你的。

 

五、List集合

 

不變性是很美好的,可是在大規模情境下,它就會有嚴重的缺陷。當傳入一個List對象到方法中的情景。

 

當方法返回一個集合,一般會很明智的在方法中建立一個集合對象(如ArrayList),填充它,並以不變的集合的形式返回。

 

有些狀況下,這並不會獲得很好的效果。最明顯的就是,當來自多個方法的集合調用一個final集合。由於不變性,在大規模數據狀況下,會分配大量的臨時集合。

 

這種狀況的解決方案將不會返回新的集合,而是經過使用單獨的集合當作參數傳入到那些方法代替組合的集合。

 

例子1(低效率):

 

List<Item> items = new ArrayList<Item>();

for (FileData fileData : fileDatas)

{

// 每一次調用都會建立一個存儲內部臨時數組的臨時的列表

items.addAll(readFileItem(fileData));

}

 

例子2:

 

List<Item> items =

new ArrayList<Item>(fileDatas.size() * avgFileDataSize * 1.5);

  

for (FileData fileData : fileDatas)

{

readFileItem(fileData, items); // 在內部添加記錄

}

 

在例子2中,當違反不變性規則的時候(這一般應該被遵照),能夠節省N個list的分配(以及任何臨時數組的分配)。這將是對你GC的一個大大的優惠。

相關文章
相關標籤/搜索