1. 程式人生 > >減少GC開銷的5個編碼技巧

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

在這篇文章中,我們來了解一下讓程式碼變得高效的五種技巧,這些技巧可以使我們的垃圾收集器(GC)在分配記憶體以及釋放記憶體上面,佔用更少的CPU時間,減少GC的開銷。當記憶體被回收的時候,GC處理很長時間經常會導致我們的程式碼中斷(又叫做"stop the world")。

背景

GC用來處理大量的短期的物件的分配(試想開啟一個web頁面,一旦頁面被載入之後,被分配記憶體的大部分物件都會被廢棄)。

GC使用一個被稱作"young generation"的東西來做這件事情。" young generation "是用來存放物件的堆記憶體。每一個物件都有一個"age"(儲存在物件的頭資訊中),用來定義存放很多沒有被回收的垃圾集合。一旦一個確定的"age"到達,物件就會被複制到堆中的另一塊空間,這個空間被稱作"survivor"或者"old"空間。

雖然這樣很有效,但是還是有很大代價的。減少臨時分配的數量確實可以幫助我們增加吞吐量,尤其是在大規模資料的環境下,或者資源有限制的app。

下面的五種程式碼方式可以更加有效的利用記憶體,並且不需要花費很多的時間,也不會降低程式碼可讀性。

1、避免隱式的String字串

String字串是我們管理的每一個數據結構中不可分割的一部分。它們在被分配好了之後不可以被修改。比如"+"操作就會分配一個連結兩個字串的新的字串。更糟糕的是,這裡分配了一個隱式的StringBuilder物件來連結兩個String字串。

例如:

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被隱式分配的可能,可以減少分配的短期的物件的數量,尤其在有大量程式碼的位置。

2、計劃好List的容量

像ArrayList這樣的動態集合用來儲存一些長度可變化資料的基本結構。ArrayList和一些其他的集合(如HashMap、TreeMap),底層都是通過使用Object[]陣列來實現的。而String陣列(它們自己包裝在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初始化,有足夠的容量,所有這樣可以減少內部陣列在執行時不必要的分配和釋放。如果你不知道確定的大小,最好估算一下這個值的平均值,新增一些緩衝,防止意外溢位。

3、使用高效的原始的集合

當前Java編譯器的版本支援陣列和Map型別(基本的key/value),這些都是通過“boxing”來支援的 - “boxing”包裝標準的可被GC分配和回收利用的物件的原始value值。

這個會有一些負面的影響。Java可以通過使用內部陣列實現大多數的集合。對於每一條被新增到HashMap中的key/value記錄,都會分配一個儲存key和value的內部物件。當處理maps的時候簡直就是罪惡,這意味著,每當你放一條記錄到map中的時候,就會有一次額外的分配和釋放操作發生。這很可能導致數量過大,而不得不重新分配新的內部陣列。當處理有成百上千條甚至更多記錄的Map時,這些內部分配的操作將會使GC的成本增加。

一種常見的情況就是在一個原始值(如id)和一個物件之間的對映。由於Java的HashMap被建立是為了持有物件的型別(vs 原始),這意味著,每個map的插入操作都可能分配一個額外的物件來儲存原始的value值("boxing" it)。

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

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

4、使用資料流(Streams)代替記憶體緩衝區(in-memory buffers)

在伺服器應用程式中,我們操作的大多數的資料都是以檔案或者是來自另一個web伺服器或DB的網路資料流的形式呈現給我們。大多數情況下,傳入的資料都是序列化的形式,在我們使用它們之前需要被反序列化成Java物件。這個過程非常容易產生大量的隱式分配。

最簡單的做法就是通過ByteArrayInputStream,ByteBuffer 把資料讀入記憶體中,然後再進行反序列化。

這是一個糟糕的舉動,因為完整的資料在構造新的物件的時候,你需要為其分配空間,然後立刻又釋放空間。並且,由於資料的大小你又不知道,你只能猜測 - 當超過初始化容量的時候,不得不分配和釋放byte[]陣列來儲存資料。

解決方案非常簡單。像Java本地的serialization和Google的Protocol Buffers等,這樣大多數的持久化的庫被建立用來反序列化來自於檔案或網路流的資料,不需要儲存到記憶體中,也不需要分配新的byte陣列來容納增長的資料。如果可以的話,你可以將這種方法和載入資料到記憶體的方法比較一下,相信GC會很感謝你的。

5、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的一個大大的優惠。