Java中的垃圾回收和内存泄露
自计算机程序的第一天以来,一个重要的性能问题是可以使用的内存量。尽管今天硬件技术提供了大量的随机访问内存,但软件开发人员必须注意他们如何管理应用程序内存,因为他们可能实现错误的解决方案,这些解决方案会因为内存不足而粉碎进程或计算机。在其他实际应用程序中,我们可能有一些硬件限制,可用内存的最大量可以是严格的技术规范。
垃圾回收(Garbage collection)直接连接到使用堆内存区域,因为这是来自 RAM 的部分,操作系统以动态方式为运行时需要它的进程提供了额外的内存空间。
为了在堆中获取空间并存储值,我们需要通过一个 2 步过程:
申请内存并存储地址在引用/指针中;在C语言中,使用 malloc,在Java和c++中都使用 new;
使用引用/指针访问该区域,并使用值进行初始化;
与垃圾回收相关的另一个概念是 [内存泄漏] memory leaks。在这些情况下,通过请求内存区域并在使用完堆内存区域后不释放它们来减少堆内存量。在C/C++这是一个真正的问题,因为它们很容易导致内存泄露。下面这个C++个示例(即使我们没有C++知识,也很容易阅读该示例;只需假设 int * 指针 和 int[]引用 是一样的):
void main() { while(true) // 申请一个4000字节,造成内存泄漏 int* array = new int[1000]; }
这个程序运行了一个无限循环,在每一个迭代中,它请求 1000个整数值数组的空间(在堆中)。
很明显,它从未被使用过,我们在下一次迭代中释放了堆地址。这是产生内存泄漏的简单示例。如果运行该示例并在任务管理器(对于 Windows )中检查其进程,则将看到进程内存在增加,直到虚拟内存警告不足或进程作系统停止(取决于操作系统)。
示例的正确版本(无内存泄漏)如下所示:
void main() { while(true) { // 申请一个4000字节,造成内存泄漏 int* array = new int[1000]; // 释放未使用的内存 delete[] array; } }
同一个例子,用Java写的看起来像这样
public class Main{ public static void main(String[] args) { while(true) { int[] intArray = new int[1000]; // 由垃圾收集器释放的空间 } } }
令你惊讶的是,它会产生内存泄漏,但进程内存仍然处于稳定和低水平。原因是 Java 进程在 Java 虚拟机中运行,它将使用内部例程(称为垃圾收集器 Garbage Collector
)清理未使用的堆内存。
前面的示例很简单。尽管你有一个垃圾收集器,它会注意内存泄露的情况下,但是[你仍然需要注意内存的使用,因为你编写的Java应用程序依然有可能将内存耗尽] 。
由于 Java 是 OOP 编程语言,并且所有对象都存储在 Heap 中(请记住,它们是使用 new 创建的),因此我们可以说 JVM 垃圾回收器会查找无法访问的对象,并回收其空间回可用资源池。无法访问的对象(它是内存泄漏的)是那些无法再使用引用
找到/到达的堆区。
如何使用垃圾收集器
垃圾收集器已经发展得如此之多,对于Java1.6,你肯定知道:
- 它使用不同的方法来管理引用和相应的堆地址;
- 最琐碎的方法是使用表来计算用于到达堆内存区域的引用数;当引用数到达 0 时,将释放内存区域;
- 它是一个复杂且非常有效的 JVM 例程,不会干扰 Java 进程性能;
- 它无法保存任何内存不足的情况(异常内存例外);请记住,任何活动对象(通过实时引用可访问)不会收集;
- 我们可以通过调用 [System.gc()] 方法请求垃圾回收器显式清理内存;
- 不建议通过调用 [System.gc()] 方法来干扰垃圾回收器,因为我们没有任何保证它的行为方式;
如何生成无法访问的对象或内存泄漏
为了生成无法访问的对象或内存泄漏,并感受拥有垃圾回收器的好处,我们必须释放或删除该对象的所有引用。
- 最简单的方法是 [空引用] :
public class Main{ public static void main(String[] args) { int[] intArray = null; //在堆中的请求空间 intArray = new int[10000]; //处理数据 //... //将引用设置为null //数组的值符合GC(垃圾回收)的条件 intArray = null; } }
这样,对象就有资格使用垃圾回收器,因为我们没有用于访问该对象的其他引用。
- [重新给引用赋值] 我们使上一个对象无法访问:
public class Main{ public static void main(String[] args) { int[] intArray = null; // 在堆中申请内存空间 intArray = new int[10000]; // 处理数据 //... //创建一个新对象,并使用相同的引用 // 旧的对象数据将符合垃圾回收条件 intArray = new int[99]; } }
这样,第一个对象将有资格使用垃圾回收器,因为我们没有用于访问该对象的其他引用。
- [隔离引用] 这是一个更隐蔽的场景,因为乍一看,你可能会说对象仍然处于活动状态。如果我们考虑下面这个类:
class Student{ int age; //实例变量 int[] marks; // 实例变量 public Student() { this.age = 0; marks = new int[10]; } }
我们可以看到每个学生对象都包含一个数组引用作为实例变量。如果我们这样测试类
public class Main{ public static void main(String[] args) { Student s = null; //request space in Heap s = new Student(); //process your data //... //remove the reference s = null; } }
很明显,student对象将被 GC 收集。但是,对象内的整数数组将会发生什么。如果我们分析堆,我们可以看到数组对象marks的值仍然可以通过marks引用。
但是,如何访问marks引用?因为,它是无法访问对象的一部分,因此该引用被视为隔离的,在这种情况下,引用的对象被标记为垃圾收集。