陷阱内存泄漏

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 仅返回最后一个元素。