4年前 (2020-12-12)  jvm |   抢沙发  736 
文章评分 0 次,平均分 0.0

在本文中,我们将全面概述Java世界中的内存泄漏,以及防止它们的主要方法。

与许多人的想法相反,用Java编写的应用程序确实会出现内存泄漏问题。不幸的是,大量java程序员认为内存泄漏是C++的一部分,java垃圾收集器完全解决了这个问题。在本文中,我打算说明虽然垃圾收集器工作得很好,但它不能发挥神奇的作用。

内存泄漏的意思正是它的名字所说的:内存泄漏。它可以有两种类型:

  1. 内存块:已分配并可供应用程序使用,但不可访问,因为应用程序没有指向此内存区域的指针*。因此,这些存储器块既不能用于应用程序,也不能用于其他处理。
  2. 内存块:具有可以释放的数据,因为它们不可访问,并且(因为它们被遗忘了)即使没有被使用,仍然在代码中被引用,并且不能被释放。

在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抛出一个java.lang.OutOfMemoryError和结束)。

正如我们看到的,垃圾收集器能够检测到非引用对象并销毁它们,从而释放内存。要创建内存泄漏,只需在代码中保留对一个或多个对象的引用,即使以后不使用它。因此,垃圾回收器永远无法销毁对象,它们将继续存在于内存中,即使不具有更高的可访问性。请看清单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中存在内存泄漏,这与许多人的想法相反。已经演示了如何创建内存泄漏情况,以及如何注意避免这种情况,以免损害应用程序。

如何防止内存泄露

 

除特别注明外,本站所有文章均为老K的Java博客原创,转载请注明出处来自https://javakk.com/1010.html

关于

发表评论

表情 格式

暂无评论

登录

忘记密码 ?

切换登录

注册