程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> JAVA綜合教程 >> jvm學習筆記二(減少GC開銷的建議),jvmgc

jvm學習筆記二(減少GC開銷的建議),jvmgc

編輯:JAVA綜合教程

jvm學習筆記二(減少GC開銷的建議),jvmgc


 

一:觸發主GC(Garbage Collector)的條件

  JVM進行次GC的頻率很高,但因為這種GC占用時間極短,所以對系統產生的影響不大。更值得關注的是主GC的觸發條件,因為它對系統影響很明顯。總的來說,有兩個條件會觸發主GC:

  1)當應用程序空閒時,即沒有應用線程在運行時,GC會被調用。因為GC在優先級最低的線程中進行,所以當應用忙時,GC線程就不會被調用,但以下條件除外。

  2)Java堆內存不足時,GC會被調用。當應用線程在運行,並在運行過程中創建新對象,若這時內存空間不足,JVM就會強制地調用GC線程,以便回收內存用於新的分配。若GC一次之後仍不能滿足內存分配的要求,JVM會再進行兩次GC作進一步的嘗試,若仍無法滿足要求,則 JVM將報“out of memory”的錯誤,Java應用將停止。

  由於是否進行主GC由JVM根據系統環境決定,而系統環境在不斷的變化當中,所以主GC的運行具有不確定性,無法預計它何時必然出現,但可以確定的是對一個長期運行的應用來說,其主GC是反復進行的。

 

二:減少GC開銷的5個編程技巧

  根據上述GC的機制,程序的運行會直接影響系統環境的變化,從而影響GC的觸發。若不針對GC的特點進行設計和編碼,就會出現內存駐留等一系列負面影響。為了避免這些影響,基本的原則就是盡可能地減少垃圾和減少GC過程中的開銷。具體措施包括以下幾個方面:

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

三、 減少GC開銷的措施

根據上述GC的機制,程序的運行會直接影響系統環境的變化,從而影響GC的觸發。若不針對GC的特點進行設計和編碼,就會出現內存駐留等一系列負面影響。為了避免這些影響,基本的原則就是盡可能地減少垃圾和減少GC過程中的開銷。具體措施包括以下幾個方面:

  (1)不要顯式調用System.gc()

  此函數建議JVM進行主GC,雖然只是建議而非一定,但很多情況下它會觸發主GC,從而增加主GC的頻率,也即增加了間歇性停頓的次數。

  (2)盡量減少臨時對象的使用

  臨時對象在跳出函數調用後,會成為垃圾,少用臨時變量就相當於減少了垃圾的產生,從而延長了出現上述第二個觸發條件出現的時間,減少了主GC的機會。

  (3)對象不用時最好顯式置為Null

  一般而言,為Null的對象都會被作為垃圾處理,所以將不用的對象顯式地設為Null,有利於GC收集器判定垃圾,從而提高了GC的效率。

  (4)盡量使用StringBuffer,而不用String來累加字符串

  由於String是固定長的字符串對象,累加String對象時,並非在一個String對象中擴增,而是重新創建新的String對象,如Str5=Str1+Str2+Str3+Str4,這條語句執行過程中會產生多個垃圾對象,因為對次作“+”操作時都必須創建新的String對象,但這些過渡對象對系統來說是沒有實際意義的,只會增加更多的垃圾。避免這種情況可以改用StringBuffer來累加字符串,因StringBuffer是可變長的,它在原有基礎上進行擴增,不會產生中間對象。

  (5)能用基本類型如Int,Long,就不用Integer,Long對象

  基本類型變量占用的內存資源比相應對象占用的少得多,如果沒有必要,最好使用基本變量。

  (6)盡量少用靜態對象變量

  靜態變量屬於全局變量,不會被GC回收,它們會一直占用內存。

  (7)分散對象創建或刪除的時間

  集中在短時間內大量創建新對象,特別是大對象,會導致突然需要大量內存,JVM在面臨這種情況時,只能進行主GC,以回收內存或整合內存碎片,從而增加主GC的頻率。集中刪除對象,道理也是一樣的。它使得突然出現了大量的垃圾對象,空閒空間必然減少,從而大大增加了下一次創建新對象時強制主GC的機會。

四、垃圾回收的幾個特點

(1)垃圾收集發生的不可預知性:由於實現了不同的垃圾回收算法和采用了不同的收集機制,所以它有可能是定時發生,有可能是當出現系統空閒CPU資源時發生,也有可能是和原始的垃圾收集一樣,等到內存消耗出現極限時發生,這與垃圾收集器的選擇和具體的設置都有關系。
  (2)垃圾收集的精確性:主要包括2 個方面:(a)垃圾收集器能夠精確標記活著的對象;(b)垃圾收集器能夠精確地定位對象之間的引用關系。前者是完全地回收所有廢棄對象的前提,否則就可能造成內存洩漏。而後者則是實現歸並和復制等算法的必要條件。所有不可達對象都能夠可靠地得到回收,所有對象都能夠重新分配,允許對象的復制和對象內存的縮並,這樣就有效地防止內存的支離破碎。
  (3)現在有許多種不同的垃圾收集器,每種有其算法且其表現各異,既有當垃圾收集開始時就停止應用程序的運行,又有當垃圾收集開始時也允許應用程序的線程運行,還有在同一時間垃圾收集多線程運行。
  (4)垃圾收集的實現和具體的JVM 以及JVM的內存模型有非常緊密的關系。不同的JVM 可能采用不同的垃圾收集,而JVM 的內存模型決定著該JVM可以采用哪些類型垃圾收集。現在,HotSpot 系列JVM中的內存系統都采用先進的面向對象的框架設計,這使得該系列JVM都可以采用最先進的垃圾收集。
  (5)隨著技術的發展,現代垃圾收集技術提供許多可選的垃圾收集器,而且在配置每種收集器的時候又可以設置不同的參數,這就使得根據不同的應用環境獲得最優的應用性能成為可能。
  針對以上特點,我們在使用的時候要注意:
  (1)不要試圖去假定垃圾收集發生的時間,這一切都是未知的。比如,方法中的一個臨時對象在方法調用完畢後就變成了無用對象,這個時候它的內存就可以被釋放。
  (2)Java中提供了一些和垃圾收集打交道的類,而且提供了一種強行執行垃圾收集的方法--調用System.gc(),但這同樣是個不確定的方法。Java 中並不保證每次調用該方法就一定能夠啟動垃圾收集,它只不過會向JVM發出這樣一個申請,到底是否真正執行垃圾收集,一切都是個未知數。
  (3)挑選適合自己的垃圾收集器。一般來說,如果系統沒有特殊和苛刻的性能要求,可以采用JVM的缺省選項。否則可以考慮使用有針對性的垃圾收集器,比如增量收集器就比較適合實時性要求較高的系統之中。系統具有較高的配置,有比較多的閒置資源,可以考慮使用並行標記/清除收集器。
  (4)關鍵的也是難把握的問題是內存洩漏。良好的編程習慣和嚴謹的編程態度永遠是最重要的,不要讓自己的一個小錯誤導致內存出現大漏洞。
  (5)盡早釋放無用對象的引用。大多數程序員在使用臨時變量的時候,都是讓引用變量在退出活動域(scope)後,自動設置為null,暗示垃圾收集器來收集該對象,還必須注意該引用的對象是否被監聽,如果有,則要去掉監聽器,然後再賦空值。

 

參考:

http://blog.csdn.net/zsuguangh/article/details/6429592

http://blog.csdn.net/tayanxunhua/article/details/21752781

 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved