首頁 Java java教程 降低java垃圾回收開銷的5個建議

降低java垃圾回收開銷的5個建議

Dec 05, 2016 am 10:59 AM
java

保持GC低開銷的竅門有哪些?

隨著一再拖延而即將發布的 Java9,G1(“Garbage First”)垃圾回收器將被成為 HotSpot 虛擬機默認的垃圾回收器。從 serial 垃圾回收器到CMS 收集器, JVM 見證了許多 GC 實現,而 G1 將成為其下一代垃圾回收器。

隨著垃圾收集器的發展,每一代 GC 與其上一代相比,都帶來了巨大的進步和改善。 parallel GC 與 serial GC 相比,它讓垃圾收集器以多執行緒的方式運作,充分利用了多核心電腦的運算能力。 CMS(“Concurrent Mark-Sweep”)收集器與parallel GC 相比,它將回收過程分成了多個階段,使得應用線程正在運行的時候,收集工作可以並發地完成,大大改善了頻繁執行“stop- the-world” 的情況。 G1 對於擁有大量堆疊記憶體的 JVM 表現出更好的性能,並且具有更好的可預測和統一的暫停過程。

Tip #1: 預測集合的容量

所有標準的 Java 集合,包括定制和擴展的實現(比如 Trove 和 Google 的 Guava),底層都使用了數組(原生資料類型或基於對象的類型)。因為數組一旦被分配,其大小就不可變,因此當添加元素到集合時,大多數情況下都會導致需要重新申請一個新的大容量數組替換老的數組(指集合底層實現使用的數組)。

即使沒有提供集合初始化的大小,大多數集合的實作都盡量優化重新分配數組的處理並且將其開銷平攤到最低。不過,在構造集合的時候就提供大小可以得到最佳的效果。

讓我們將下面的程式碼作為一個簡單的例子分析一下:

public static List reverse(List & lt; ? extends T & gt; list) {    List result = new ArrayList();
    for (int i = list.size() - 1; i & gt; = 0; i--) {
        result.add(list.get(i));
    }    return result;
}
登入後複製

This method allocates a new array, then fills it up with items from another list, only in reverse order. 這個方法分配了一個新的數組,然後用另一個list 中元素對該數組進行填充,只是元素的數序改變了。

這個處理方式可能會付出慘痛的效能代價,其最佳化的點在新增元素到新的 list 中這行程式碼。 隨著每一次添加元素,list 都需要確保其底層數組擁有足夠的位置來容納新的元素。如果有空閒的位置,那麼只是簡單地將新元素儲存到下一個空閒的插槽。如果沒有的話,將分配一個新的底層數組,將其拷貝舊的數組內容分配到新的數組中,然後添加新的元素。這將導致多次分配數組,那些剩餘的舊數組最終被 GC 回收。

我們可以透過在建構集合時讓其底層的陣列知道它將儲存多少元素,從而避免這些多餘的分配

public static List reverse(List & lt; ? extends T & gt; list) {    List result = new ArrayList(list.size());
    for (int i = list.size() - 1; i & gt; = 0; i--) {
        result.add(list.get(i));
    }    return result;
}
登入後複製

上面的程式碼透過ArrayList 的建構器指定足夠大的空間來儲存list.size()個元素,在初始化時完成分配的執行,這意味著List 在迭代的過程中無需再次分配記憶體。

Guava 的集合類別則更進一步,允許初始化集合時明確指定期望元素的個數或指定一個預測值。

List result = Lists.newArrayListWithCapacity(list.size());List result = Lists.newArrayListWithExpectedSize(list.size());
登入後複製

上面的程式碼中,前者用於我們已經準確地知道集合將要儲存多少元素,而後者的分配方式考慮了錯誤預估的情況。

Tip #2:直接處理資料流

當處理資料流時,例如從一個檔案讀取資料或從網路下載數據,下面的程式碼是非常常見的:

byte[] fileData = readFileToByteArray(new File("myfile.txt"));
登入後複製

所產生的位元組數組可能被解析XML 文件、JSON 物件或協定緩衝訊息,以及一些常見的可選項。

當處理大檔案或檔案的大小無法預測時,上面的做法很是不明智的,因為當 JVM 無法分配一個緩衝區來處理真正檔案時,就會導致OutOfMemeoryErrors。

即使資料的大小是可管理的,當到垃圾回收時,使用上面的模式依然會造成巨大的開銷,因為它在堆中分配了一塊非常大的區域來儲存檔案資料。

一種更好的處理方式是使用合適的 InputStream (例如在這個例子中使用 FileInputStream)直接傳遞給解析器,不再一次性將整個文件讀取到一個位元組數組中。所有主流的開源函式庫都提供對應的 API 來直接接受一個輸入流進行處理,例如:

FileInputStream fis = new FileInputStream(fileName);
MyProtoBufMessage msg = MyProtoBufMessage.parseFrom(fis);
登入後複製

Tip #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 暂停和更好的总吞吐量。

Tip #4: 小心字符串拼接

字符串可能是在所有基于 JVM 应用程序中最常用的非原生数据结构。然而,由于其隐式地开销负担和简便的使用,非常容易成为占用大量内存的罪归祸首。

这个问题很明显不在于字符串字面值,而是在运行时分配内存初始化产生的。让我们快速看一下动态构建字符串的例子:

public static String toString(T[] array) {    
   String result = "[";    
   for (int i = 0; i & lt; array.length; i++) {
        result += (array[i] == array ? "this" : array[i]);        
        if (i & lt; array.length - 1) {
            result += ", ";
        }
    }
    result += "]";
    return result;
}
登入後複製

这是个看似不错的方法,接收一个字符数组然后返回一个字符串。但是这对于对象内存分配却是灾难性的。

很难看清这语法糖的背后,但是幕后的实际情况是这样的:

public static String toString(T[] array) {    
     String result = "[";    
     for (int i = 0; i & lt; array.length; i++) {
        StringBuilder sb1 = new StringBuilder(result);
        sb1.append(array[i] == array ? "this" : array[i]);
        result = sb1.toString();        
        
        if (i & lt; 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 类来执行这些拼接操作。这就会有问题了,因为每一次迭代,既隐式地分配了一个临时字符串,又隐式分配了一个临时的 StringBuilder 对象来帮助构建最终的结果。

最佳的方式是避免上面的情况,使用 StringBuilder 和直接的追加,以取代本地拼接操作符(“+”)。下面是一个例子:

public static String toString(T[] array) {
    StringBuilder sb = new StringBuilder("[");    
    for (int i = 0; i & lt; array.length; i++) {
        sb.append(array[i] == array ? "this" : array[i]);        
        if (i & lt; array.length - 1) {
            sb.append(", ");
        }
    }
    sb.append("]");    
    return sb.toString();
}
登入後複製

这里,我们只在方法开始的时候分配了唯一的一个 StringBuilder。至此,所有的字符串和 list 中的元素都被追加到单独的一个StringBuilder中。最终使用 toString() 方法一次性将其转成成字符串返回。

Tip #5: 使用特定的原生类型的集合

Java 标准的集合库简单且支持泛型,允许在使用集合时对类型进行半静态地绑定。比如想要创建一个只存放字符串的 Set 或者存储 Map

TIntDoubleMap map = new TIntDoubleHashMap();
map.put(5, 7.0);
map.put(-1, 9.999);...
登入後複製

Trove 的底层实现使用了原生类型的数组,所以当操作集合的时候不会发生元素的装箱(int->Integer)或者拆箱(Integer->int), 没有存储对象,因为底层使用原生数据类型存储。

最后

随着垃圾收集器持续的改进,以及运行时的优化和 JIT 编译器也变得越来越智能。我们作为开发者将会发现越来越少地考虑如何编写 GC 友好的代码。然而,就目前阶段,不论 G1 如何改进,我们仍然有很多可以做的事来帮 JVM 提升性能。


本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
3 週前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳圖形設置
3 週前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您聽不到任何人,如何修復音頻
3 週前 By 尊渡假赌尊渡假赌尊渡假赌
WWE 2K25:如何解鎖Myrise中的所有內容
4 週前 By 尊渡假赌尊渡假赌尊渡假赌

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

Java 中的完美數 Java 中的完美數 Aug 30, 2024 pm 04:28 PM

Java 完美數指南。這裡我們討論定義,如何在 Java 中檢查完美數?

Java 中的隨機數產生器 Java 中的隨機數產生器 Aug 30, 2024 pm 04:27 PM

Java 隨機數產生器指南。在這裡,我們透過範例討論 Java 中的函數,並透過範例討論兩個不同的生成器。

Java中的Weka Java中的Weka Aug 30, 2024 pm 04:28 PM

Java 版 Weka 指南。這裡我們透過範例討論簡介、如何使用 weka java、平台類型和優點。

Java 中的史密斯數 Java 中的史密斯數 Aug 30, 2024 pm 04:28 PM

Java 史密斯數指南。這裡我們討論定義,如何在Java中檢查史密斯號?帶有程式碼實現的範例。

Java Spring 面試題 Java Spring 面試題 Aug 30, 2024 pm 04:29 PM

在本文中,我們保留了最常被問到的 Java Spring 面試問題及其詳細答案。這樣你就可以順利通過面試。

突破或從Java 8流返回? 突破或從Java 8流返回? Feb 07, 2025 pm 12:09 PM

Java 8引入了Stream API,提供了一種強大且表達力豐富的處理數據集合的方式。然而,使用Stream時,一個常見問題是:如何從forEach操作中中斷或返回? 傳統循環允許提前中斷或返回,但Stream的forEach方法並不直接支持這種方式。本文將解釋原因,並探討在Stream處理系統中實現提前終止的替代方法。 延伸閱讀: Java Stream API改進 理解Stream forEach forEach方法是一個終端操作,它對Stream中的每個元素執行一個操作。它的設計意圖是處

Java 中的時間戳至今 Java 中的時間戳至今 Aug 30, 2024 pm 04:28 PM

Java 中的時間戳記到日期指南。這裡我們也結合範例討論了介紹以及如何在java中將時間戳記轉換為日期。

創造未來:零基礎的 Java 編程 創造未來:零基礎的 Java 編程 Oct 13, 2024 pm 01:32 PM

Java是熱門程式語言,適合初學者和經驗豐富的開發者學習。本教學從基礎概念出發,逐步深入解說進階主題。安裝Java開發工具包後,可透過建立簡單的「Hello,World!」程式來實踐程式設計。理解程式碼後,使用命令提示字元編譯並執行程序,控制台上將輸出「Hello,World!」。學習Java開啟了程式設計之旅,隨著掌握程度加深,可創建更複雜的應用程式。

See all articles