中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

如何降低Java垃圾回收開銷

發布時間:2022-03-15 17:32:59 來源:億速云 閱讀:140 作者:iii 欄目:web開發

這篇文章主要介紹“如何降低Java垃圾回收開銷”的相關知識,小編通過實際案例向大家展示操作過程,操作方法簡單快捷,實用性強,希望這篇“如何降低Java垃圾回收開銷”文章能幫助大家解決問題。

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 的集合類則更進一步,允許初始化集合時明確指定期望元素的個數或者指定一個預測值。

1

2List result = Lists.newArrayListWithCapacity(list.size());

List result = Lists.newArrayListWithExpectedSize(list.size());

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

Tip #2:直接處理數據流

當處理數據流時,比如從一個文件讀取數據或者從網絡中下載數據,下面的代碼是非常常見的:

1byte[] 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<Pair, List>這樣的 map,這種處理方式是非常棒的。

真正的問題源于當我們想要使用一個 list 存儲 int 類型,或者一個 map 存儲 double 類型作為 value。因為泛型不支持原生數據類型,因此另外的一種選擇是使用包裝類型來進行替換,這里我們使用 List 。

這種處理方式是非常浪費的,因為一個 Integer 是一個完全的對象,一個對象的頭部占用12個字節以及其內部的所維護的 int 屬性,每個Integer 對象總共占用16個字節。這比起存儲相同個數的 int 類型的 list 而言,其消耗的空間是它的四倍!比這個更加嚴重的問題在于,事實上因為 Integer 是真正的對象實例,因此它需要垃圾收集階段被垃圾收集器所考慮是否要回收。

為了處理這個問題,我們在 Takipi 中使用非常棒的 Trove 集合庫。Trove 摒棄了部分泛型的特定來支持特定的使用內存更高效的原生類型的集合。比如,我們使用非常消耗性能的 Map<Integer, Double>,在 Trove 中有另一種特別的選擇方案,其形式為 TIntDoubleMap

TIntDoubleMap map = new TIntDoubleHashMap();

map.put(5, 7.0);

map.put(-1, 9.999);

...

Trove 的底層實現使用了原生類型的數組,所以當操作集合的時候不會發生元素的裝箱(int->Integer)或者拆箱(Integer->int), 沒有存儲對象,因為底層使用原生數據類型存儲。

關于“如何降低Java垃圾回收開銷”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識,可以關注億速云行業資訊頻道,小編每天都會為大家更新不同的知識點。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

承德县| 布尔津县| 林甸县| 昭通市| 静宁县| 石景山区| 河北省| 庆阳市| 全南县| 蚌埠市| 裕民县| 华安县| 青冈县| 资中县| 龙泉市| 昭平县| 沿河| 新野县| 西乌| 临泉县| 凤凰县| 瓮安县| 江孜县| 沈阳市| 肥西县| 博罗县| 卓尼县| 昌平区| 金平| 金秀| 班戈县| 东乡族自治县| 海林市| 吉安县| 开鲁县| 图们市| 平陆县| 盐池县| 石城县| 乌审旗| 文化|