在本文中,我们将全面概述Java世界中的内存泄漏,以及防止它们的主要方法。
与许多人的想法相反,用Java编写的应用程序确实会出现内存泄漏问题。不幸的是,大量java程序员认为内存泄漏是C++的一部分,java垃圾收集器完全解决了这个问题。在本文中,我打算说明虽然垃圾收集器工作得很好,但它不能发挥神奇的作用。
内存泄漏的意思正是它的名字所说的:内存泄漏。它可以有两种类型:
内存块:已分配并可供应用程序使用,但不可访问,因为应用程序没有指向此内存区域的指针*。因此,这些存储器块既不能用于应用程序,也不能用于其他处理。
内存块:具有可以释放的数据,因为它们不可访问,并且(因为它们被遗忘了)即使没有被使用,仍然在代码中被引用,并且不能被释放。
在C/C++中,情况1是很常见的。例如,使用malloc*函数分配一个内存量,在这个序列中,程序员使指向该区域的指针pass memory指向另一个位置,从而丢失初始引用。在Java中,这种类型的内存泄漏不会发生,因为垃圾收集器能够检测已分配和未引用的内存块,并释放它们以供以后使用。
问题出在情况2。但是,在解释这个问题是如何发生以及如何防止它之前,我们必须对Java垃圾收集器的工作原理有一点了解。
*1指针是指向内存地址的变量。它是C/C++中非常常见的术语,认为java没有指针是错误的。使用new创建对象时,接收该对象的变量实际上是指向包含该对象的内存地址的指针。
*2 malloc()函数是C API的一部分。它的功能是分配所需的内存量(作为参数提供)。此函数的返回是指向新创建的内存区域的指针。
垃圾收集器的角色
Java编程语言在较低级别上的一大优点是存在垃圾收集器。它的功能是清理已分配块后面的内存,这些块不再需要被应用程序引用。当应用程序再次使用垃圾回收器时,它将面临这种情况。
看看为什么Java中垃圾收集器的工作方式非常简单。注意清单1中的代码。
清单1。主方法示例。
public static void main(String[] args) throws Exception {
int[] array = null;
while(true) {
array = new int[1000];
System.out.println("Free Bytes : " + Runtime.getRuntime().freeMemory());
Thread.sleep(200);
}
}
这段代码正在无休止的循环中运行。循环的每一次迭代,都会创建一个包含1000个整数位置的数组。每次执行循环时,都会显示空闲字节的数量。一开始您可能会想:这个应用程序最终会得到JVM的内存吗?事实上,情况并非如此。当您在任何给定的时间运行该程序时,您将看到清单2中所示的以下输出。
清单2。程序输出。
Free Bytes: 1530520
Free Bytes: 1530520
Free Bytes: 1530520
Free Bytes: 1526504
Free Bytes: 1522488
Free Bytes: 1518472
Free Bytes: 1514456
Free Bytes: 1510440
Free Bytes: 1912792
Free Bytes: 1912792
Free Bytes: 1908776
Free Bytes: 1904760
Free Bytes: 1900744
Free Bytes: 1896728
Free Bytes: 1892712
内存在运行时突然增加了。对此的解释很简单。执行循环时,将创建数组。在循环的下一次迭代中,上一次迭代中创建的数组不会被其他任何人引用(请注意,数组变量停止指向旧数组,并继续指向新数组,从而使旧数组无法访问)。在某一点上,JVM会看到可用内存的减少,并让垃圾收集器运行。然后,垃圾回收器发现在循环中分配的内存不可访问并释放它。在这一点上,我们可以看到可用内存的增加。
在看到这段代码运行之后,您一定在想:我怎么知道垃圾回收器什么时候会运行?你不知道。垃圾回收器的执行控制来自JVM。JVM决定何时运行。请注意,不建议一直运行垃圾收集器,因为它会占用计算机资源。
关于垃圾收集器的另一个重要点是,由于它由JVM控制,所以不可能以编程方式强制执行它。最能做的就是调用System.gc() 方法(或Runtime.getRuntime().gc())。此方法通知JVM应用程序希望执行垃圾回收器,但并不保证它将在所需的时间实际执行。
finalize方法
Java对象类有一个名为finalize()的方法,它可以被继承自Object的类(即任何类)重写。当垃圾回收器决定某个特定的对象由于没有被更多引用而被销毁时,它会在销毁该对象之前对该对象调用finalize()方法。
尽管finalize()方法是程序员在销毁对象时释放与该对象相关联的资源的机会,但完全不建议重写finalize()。原因很简单:由于不能保证对象被销毁,也不能保证finalize()将运行。Oracle本身不建议重写finalize()方法。资源的释放可以通过其他方式实现,例如使用块try/catch/finally和/或为此目的建立特定的方法(例如close()方法)。
导致Java内存泄漏
在理解了什么是内存泄漏以及Java垃圾收集器的工作原理之后,我们现在将了解如何在Java中引起内存泄漏以及如何避免它。首先,我想强调内存泄漏很难发现(有时需要借助外部工具),并且总是由编程错误引起的。通常程序员不太关心它们,直到他们开始消耗过多的内存,甚至颠覆JVM(当内存用完时,JVM抛出一个
正如我们看到的,垃圾收集器能够检测到非引用对象并销毁它们,从而释放内存。要创建内存泄漏,只需在代码中保留对一个或多个对象的引用,即使以后不使用它。因此,垃圾回收器永远无法销毁对象,它们将继续存在于内存中,即使不具有更高的可访问性。请看清单3中堆栈的这个简单示例实现。
清单3。堆栈的示例。
public class Stack {
private List stack = new ArrayList();
int count = 0;
public void push(Object obj) {
pilha.add(count++, obj);
}
public Object pop() {
if(count == 0) {
return null;
}
return pilha.get(--count);
}
}
每次在堆栈中放置或移除元素时,计数器都会控制最后一个元素的位置。这是一个明显的记忆泄露。堆栈已注册对其中所有对象的引用。但是,由于对象都从堆栈中移除,堆栈将继续引用移除的对象,这使得Gargabe收集器无法恢复此内存。
要解决这种情况,只需在从堆栈中移除对象时删除对该对象的引用。
清单4。解决问题。
public class Stack {
private List stack = new ArrayList();
int count = 0;
public void push(Object obj) {
pilha.add(count++, obj);
}
public Object pop() {
if(count == 0) {
return null;
}
Object obj = stack.get(--count);
stack.set(count, null);
return obj;
}
}
将对象引用赋值为null将导致battery不再引用该对象,从而允许垃圾回收器销毁该对象。
这个例子旨在说明Java中内存泄漏是如何可能的。任何人都很难以这种方式实现堆栈(甚至更多的Java已经为此目的提供了一个stack类)。无论如何,想象一个类似的情况,成千上万甚至数百万的对象是不可访问的,但却被引用了。例如,在应用程序服务器中不停地运行的应用程序中,这可能导致JVM在内存累积泄漏几天后终止。在这种情况下,找出错误可能是一项极其复杂和费力的任务。
避免内存泄漏的建议
遵循一些避免内存泄漏的建议比在代码完成后检测要容易得多。由于垃圾回收器极大地方便了程序员的工作,只需注意一些常见的导致内存泄漏的关键点:
注意对象集合(数组、列表、映射、向量等)。有时,它们可以保存对不再需要的对象的引用;
另外,对于对象集合,如果它们是静态的或在整个应用程序生命周期中持续存在,则应小心;
在编程需要将事件侦听器注册到对象的情况下,如果不再需要这些对象的记录,请注意删除这些对象的记录。底线:删除对不必要对象的所有引用。通过这样做,垃圾回收器将能够彻底地完成它的工作,并且您将不会在应用程序中出现内存泄漏。
本文试图证明Java中存在内存泄漏,这与许多人的想法相反。已经演示了如何创建内存泄漏情况,以及如何注意避免这种情况,以免损害应用程序。