减少Java垃圾收集开销的5条提示
保持較低的GC開銷的一些最有用的技巧是什么?
隨著Java 9的一次再次延遲發布,G1(“ Garbage First”)垃圾收集器將設置為HotSpot JVM的默認收集器。 從串行垃圾收集器一直到CMS收集器,JVM在其整個生命周期中都見證了許多GC實現,而G1收集器緊隨其后。
隨著垃圾收集器的發展,每一代(沒有雙關語)都會帶來比以前更高的進步和改進。 串行收集器之后的并行GC利用多核計算機的計算功能使垃圾收集成為多線程。 隨后的CMS(“并發標記掃描”)收集器將收集分為多個階段,從而使許多收集工作可以在應用程序線程運行時同時完成-從而減少了“停止世界”的頻率。 G1在堆非常大的JVM上增加了更好的性能,并且具有更加可預測的統一暫停。
無論高級GC收到什么,其致命弱點仍然是:冗余且不可預測的對象分配。 無論您選擇使用哪種垃圾收集器,這些快速,適用,永恒的技巧將幫助您避免GC開銷。
提示1:預測收集容量
所有標準Java集合以及大多數自定義和擴展的實現(例如Trove和Google的Guava )都使用基礎數組(基于原始或對象的數組)。 由于數組一旦分配就不會改變大小,因此在許多情況下向集合中添加項目可能會導致丟棄舊的基礎數組,而使用較大的新分配的數組。
即使未提供預期的集合大小,大多數集合實現都嘗試優化此重新分配過程并將其保持在攤銷后的最小值。 但是,通過在構造時為集合提供預期的大小可以達到最佳效果。
讓我們以以下代碼為簡單示例:
public static List reverse(List<? extends T> list) {List result = new ArrayList();for (int i = list.size() - 1; i >= 0; i--) {result.add(list.get(i));}return result; }此方法分配一個新數組,然后以相反的順序填充另一個列表中的項目。
可能會很痛苦并且可以優化的一點是將項目添加到新列表的行。 對于每個添加項,列表都需要確保其基礎數組中具有足夠的可用插槽以容納新項。 如果是這樣,它將簡單地將新項目存儲在下一個空閑插槽中。 如果不是,它將分配一個新的基礎數組,將舊數組的內容復制到新數組中,然后添加新項。 這將導致陣列的多個分配,這些分配將保留在那里,以供GC最終收集。
我們可以通過在構造數組時讓數組知道預計要保留多少個項來避免這些多余的分配:
public static List reverse(List<? extends T> list) {List result = new ArrayList(list.size());for (int i = list.size() - 1; i >= 0; i--) {result.add(list.get(i));}return result;}這使得ArrayList構造函數執行的初始分配足夠大,可以容納list.size()項,這意味著在迭代期間不必重新分配內存。
Guava的集合類更進一步,使我們可以使用預期項目的確切數量或估計值來初始化集合。
List result = Lists.newArrayListWithCapacity(list.size()); List result = Lists.newArrayListWithExpectedSize(list.size());前者用于以下情況:我們確切知道集合將要容納多少項,而后者則分配一些填充以解決估計誤差。
提示2:直接處理流
例如,在處理數據流(例如從文件讀取的數據或通過網絡下載的數據)時,通常會看到以下內容:
byte[] fileData = readFileToByteArray(new File("myfile.txt"));然后,可以將結果字節數組解析為XML文檔,JSON對象或協議緩沖區消息,以列舉一些常用的選項。
當處理大文件或大小無法預測的文件時,這顯然不是一個好主意,因為在JVM無法實際分配整個文件大小的緩沖區的情況下,這會使我們面臨OutOfMemoryErrors。
但是,即使數據的大小似乎是可管理的,使用上述模式在進行垃圾回收時也會導致大量開銷,因為它會在堆上分配一個較大的blob來保存文件數據。
解決此問題的更好方法是使用適當的InputStream(在這種情況下為FileInputStream),將其直接輸入解析器,而無需先將整個內容讀取到字節數組中。 所有主要庫都公開了API以直接解析流,例如:
FileInputStream fis = new FileInputStream(fileName); MyProtoBufMessage msg = MyProtoBufMessage.parseFrom(fis);提示3:使用不可變對象
不變性具有很多優勢。 甚至不讓我開始。 但是,很少受到關注的一個優點是它對垃圾回收的影響。
不變對象是指在構造對象之后其字段(在我們的情況下尤其是非原始字段)無法修改的對象。 例如:
public class ObjectPair {private final Object first;private final Object second;public ObjectPair(Object first, Object second) {this.first = first;this.second = second;}public Object getFirst() {return first;}public Object getSecond() {return second;}}實例化以上類會導致一個不可變的對象—它的所有字段都標記為final,并且不能在構造后進行修改。
不變性意味著不變容器引用的所有對象都是在容器構造完成之前創建的。 用GC的術語來說:容器至少與所保存的最小引用一樣年輕 。 這意味著,在年輕一代執行垃圾回收周期時,GC可以跳過位于老一代中的不可變對象,因為它可以確定它們不能引用正在收集的一代中的任何對象。
要掃描的對象越少,意味著要掃描的內存頁面越少,而要掃描的內存頁面就越少,意味著GC周期越短,這意味著GC暫停時間越短,總體吞吐量就越高。
提示4:警惕字符串連接
在任何基于JVM的應用程序中,字符串可能是最普遍的非原始數據結構。 但是,它們隱含的重量和易于使用的特性使它們很容易成為導致應用程序占用大量內存的罪魁禍首。
問題顯然不在于文字字符串,因為它們是內聯和插入的,而是在于在運行時分配和構造的字符串。 讓我們看一下動態字符串構造的快速示例:
public static String toString(T[] array) {String result = "[";for (int i = 0; i < array.length; i++) {result += (array[i] == array ? "this" : array[i]);if (i < array.length - 1) {result += ", ";}}result += "]";return result; }這是一個不錯的方法,它接受一個數組并為其返回字符串表示形式。 在對象分配方面也是如此。
很難理解所有這些語法糖,但是幕后的實際情況是:
public static String toString(T[] array) {String result = "[";for (int i = 0; i < array.length; i++) {StringBuilder sb1 = new StringBuilder(result);sb1.append(array[i] == array ? "this" : array[i]);result = sb1.toString();if (i < array.length - 1) {StringBuilder sb2 = new StringBuilder(result);sb2.append(", ");result = sb2.toString();}}StringBuilder sb3 = new StringBuilder(result);sb3.append("]");result = sb3.toString();return result; }字符串是不可變的,這意味著在進行串聯時它們本身不會被修改,而是依次分配新的字符串。 另外,編譯器利用標準的StringBuilder類來實際執行這些串聯。 這導致了雙重麻煩,因為在循環的每次迭代中,我們同時獲得(1)臨時字符串的隱式分配和(2)臨時StringBuilder對象的隱式分配,以幫助我們構造最終結果。
避免這種情況的最佳方法是顯式使用StringBuilder并將其直接附加到其上,而不是使用有些天真的串聯運算符(“ +”)。 可能是這樣的:
public static String toString(T[] array) {StringBuilder sb = new StringBuilder("[");for (int i = 0; i < array.length; i++) {sb.append(array[i] == array ? "this" : array[i]);if (i < array.length - 1) {sb.append(", ");}}sb.append("]");return sb.toString(); }在此方法的開頭,我們僅分配了一個StringBuilder。 從那時起,所有字符串和列表項都附加到唯一的StringBuilder上,該字符串最終僅使用其toString方法轉換成字符串,然后返回。
提示5:使用專門的原始集合
Java的標準集合庫既方便又通用,允許我們使用具有半靜態類型綁定的集合。 如果我們想使用例如一組字符串(Set <String>),或一對和一組字符串之間的映射(Map <Pair,List <String >>),這是很棒的。
真正的問題始于我們要保存一個int列表或一個double類型值的映射。 由于泛型類型不能與基元一起使用,因此替代方法是使用裝箱的類型,因此我們需要使用List <Integer>來代替List <int>。
這是非常浪費的,因為Integer是一個完整的Object,充斥著12字節的對象標頭和一個內部4字節的int字段來保存其值。 每個Integer項的總和為16個字節。 這是相同大小的原始整數列表的大小的4倍! 但是,更大的問題是所有這些Integer實際上都是對象實例,在垃圾回收期間需要考慮這些實例。
為了解決這個問題,我們在塔基皮(Takipi)使用了出色的Trove收藏庫。 Trove放棄了一些(但不是全部)泛型,轉而使用專門的內存有效的原始集合。 例如,代替浪費的Map <Integer,Double>,還有TIntDoubleMap形式的專門替代方法:
TIntDoubleMap map = new TIntDoubleHashMap(); map.put(5, 7.0); map.put(-1, 9.999); ...Trove的基礎實現使用基本數組,因此在操作集合時不會進行裝箱(int-> Integer)或拆箱(Integer-> int),并且不會存儲任何對象來代替基元。
最后的想法
隨著垃圾收集器的不斷發展,以及運行時優化和JIT編譯器變得越來越智能,我們作為開發人員將發現自己越來越不關心如何編寫與GC友好的代碼。 但是,就目前而言,無論G1多么先進,我們仍然可以做很多工作來幫助JVM。
翻譯自: https://www.javacodegeeks.com/2015/12/5-tips-reducing-java-garbage-collection-overhead.html
總結
以上是生活随笔為你收集整理的减少Java垃圾收集开销的5条提示的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 白酒出海,新周期下觅新增量
- 下一篇: 杭州亚运会发布总赛程 3.0 版:电竞项