陷阱記憶體洩漏

Java 自動管理記憶體。你不需要手動釋放記憶體。當活動執行緒無法再訪問物件時,垃圾收集器可以釋放堆上的物件記憶體。

但是,你可以通過允許不再需要的物件來防止釋放記憶體。無論你將此稱為記憶體洩漏還是記憶體打包,結果都是相同的 - 分配的記憶體不必要地增加。

Java 中的記憶體洩漏可能以各種方式發生,但最常見的原因是永久物件引用,因為垃圾收集器在仍然有對它們的引用時無法從堆中刪除物件。

靜態欄位

可以通過使用包含一些物件集合的 static 欄位定義類來建立這樣的引用,並且在不再需要集合之後忘記將 static 欄位設定為 nullstatic 欄位被認為是 GC 根,並且從未收集過。另一個問題是使用 JNI 時非堆記憶體中的洩漏。

Classloader 洩漏

但到目前為止,最陰險的記憶體洩漏型別是類載入器洩漏 。類載入器包含對已載入的每個類的引用,並且每個類都包含對其類載入器的引用。每個物件都包含對其類的引用。因此,即使類載入器載入的類的單個物件不是垃圾,也不能收集該類載入器載入的單個類。由於每個類也引用其靜態欄位,因此也無法收集它們。

累積洩漏累積洩漏示例可能如下所示:

final ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
final Deque<BigDecimal> numbers = new LinkedBlockingDeque<>();
final BigDecimal divisor = new BigDecimal(51);

scheduledExecutorService.scheduleAtFixedRate(() -> {
    BigDecimal number = numbers.peekLast();
    if (number != null && number.remainder(divisor).byteValue() == 0) {
        System.out.println("Number: " + number);
        System.out.println("Deque size: " + numbers.size());
    }
}, 10, 10, TimeUnit.MILLISECONDS);

scheduledExecutorService.scheduleAtFixedRate(() -> {
    numbers.add(new BigDecimal(System.currentTimeMillis()));
}, 10, 10, TimeUnit.MILLISECONDS);

try {
    scheduledExecutorService.awaitTermination(1, TimeUnit.DAYS);
} catch (InterruptedException e) {
    e.printStackTrace();
}

此示例建立兩個計劃任務。第一個任務從一個名為 numbers 的雙端佇列中獲取最後一個數字,如果該數字可被 51 整除,則會列印數字和雙端佇列的大小。第二項任務是將數字放入雙端佇列。這兩項任務都按固定速率安排,每 10 毫秒執行一次。

如果執行程式碼,你將看到雙端佇列的大小永久增加。這最終將導致 deque 填充消耗所有可用堆記憶體的物件。

為了在保留此程式的語義的同時防止這種情況,我們可以使用另一種方法從 deque 中獲取數字:pollLast。與方法 peekLast 相反,pollLast 返回元素並將其從雙端佇列中移除,而 peekLast 僅返回最後一個元素。