现在的位置: 首页 > 综合 > 正文

[读透代码]第二篇(内存管理,HashMap内存泄漏解决办法)

2018年02月15日 ⁄ 综合 ⁄ 共 4401字 ⁄ 字号 评论关闭

网上看到一个关于内存泄漏处理的例子,原网址:http://www.jb51.net/article/49428.htm,下面笔者将具体分析下这篇文章中的代码,并从中学习JAVA的内存管理。

Q:在Java中怎么可以产生内存泄露?
A:Java中,造成内存泄露的原因有很多种。典型的例子是一个没有实现hasCode和equals方法的Key类在HashMap中保存的情况。最后会生成很多重复的对象。所有的内存泄露最后都会抛出OutOfMemoryError异常,下面通过一段简短的通过无限循环模拟内存泄露的例子说明一下。

import java.util.HashMap;
import java.util.Map;

public class Test {
	public static void main(String[] args) {
		Map<Key, String> map = new HashMap<Key, String>(1000);

		int counter = 0;
		while (true) {
			// creates duplicate objects due to bad Key class
			map.put(new Key("dummyKey"), "value");
			counter++;
			if (counter % 1000 == 0) {
				System.out.println("map size: " + map.size());
				System.out.println("Free memory after count " + counter
						+ " is " + getFreeMemory() + "MB");

				sleep(100);
			}

		}
	}

	// inner class key without hashcode() or equals() -- bad implementation
	static class Key {
		private String key;

		public Key(String key) {
			this.key = key;
		}
	}

	// delay for a given period in milli seconds
	public static void sleep(long sleepFor) {
		try {
			Thread.sleep(sleepFor);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	// get available memory in MB
	public static long getFreeMemory() {
		return Runtime.getRuntime().freeMemory() / (1024 * 1024);
	}
}

笔者运行的时候,和那篇文中作者运行结果并不一致,后来配置了下虚拟机的参数,发现虚拟机的运行内存为2的次方,不会设置参数的读者可以参看下网络的资源。我这里给出一种Eclipse的配置方法。右击项目->Run As->Run Configuration。配置JVM参数如图。


图1:JVM参数配置

接下来,运行该程序,我们得到这样的结果:

map size: 1000
Free memory after count 1000 is 2MB
map size: 2000
Free memory after count 2000 is 2MB
map size: 3000

........

Free memory after count 23000 is 1MB
map size: 24000
Free memory after count 24000 is 1MB
map size: 25000

Free memory after count 48000 is 0MB
map size: 49000
Free memory after count 49000 is 0MB
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.HashMap.resize(Unknown Source)
at java.util.HashMap.putVal(Unknown Source)
at java.util.HashMap.put(Unknown Source)
at test.Test.main(Test.java:13)

引起错误的原因很明显,key的类没有实现hashcode()和equals(),导致hashmap存储了大量的相同的entry。明明插入了2个Key一样的键值对,可是为什么其中个数暴涨?Fine,我们不用Key这个类来作为Key,而是使用String类来作为Key。具体如下:

Map<String, String> map = new HashMap<String, String>(1000);

再次运行,发现刚刚出现的内存泄漏问题不再出现了,原因很简单,没有覆写hashcode()和equal()方法导致每次都可以成功插入到Map中。覆写hashcode()和equal()来解决这个问题。如下:

static class Key {
		private String key;

		public Key(String key) {
			this.key = key;
		}

		@Override
		public boolean equals(Object obj) {

			if (obj instanceof Key)
				return key.equals(((Key) obj).key);
			else
				return false;

		}

		@Override
		public int hashCode() {
			return key.hashCode();
		}
	}

再次运行,内存泄漏问题得以解决,另外说下,Key是继承自Object。看完该代码觉得自己很有必要回顾并加强下对于内存管理方面的知识总结如下。

1、JAVA中的垃圾收集器相对于以前的语言的优势是什么?[Source:SAP公司面试题]

过去的语言要求程序员显式的分配内存、回收内存。但是这种手工的回收往往会造成“内存泄漏”(有过C++开发经验的读者应该清楚这个),即由于某种原因使分配的内存始终没有得到释放。如果该任务不断的被重复执行,将会导致大量的内存得不到释放,后果很明显。相比之下,JAVA提供了垃圾收集器来回收内存,避免了很多潜在的危险。JAVA在创建对象时会自动分配内存,并当该对象的引用不存在时释放这块内存。

JAVA中使用被称为垃圾收集器的技术来监视JAVA程序的运行,当对象不再使用时,就自动释放对象所使用的内存。JAVA使用一系列软指针来跟踪对象的各个引用,并用一个对象表将这些软指针映射为对象的引用。之所以称为软指针,是因为这些指针并不直接指向对象,而是指向对象的引用。使用软指针,JAVA的垃圾收集器能够以单独的线程在后台运行,并依次检查每个对象。通过更改对象表项,垃圾收集器可以标记对象、移除对象、移动对象或检查对象。

垃圾收集器是自动运行的,一般情况下,无须显式地请求垃圾收集器。程序运行时,垃圾收集器会不时检查对象的各个引用,并回收无引用对象所占用的内存。调用System类中的静态gc()方法可以运行垃圾收集器,但这些并不能保证立即回收指定对象。

2、JAVA是如何管理内存的?

JAVA的内存管理就是对象的分配和释放问题。在JAVA中,程序员需要通过关键字new为每个对象申请内存空间(基本类型除外),所有的对象都在堆(Heap)中分配空间。另外,对象的释放是由GC决定和执行的。在JAVA中,内存的分配是由程序完成的,而内存的释放是由GC完成,这种收支两条线的方法确实简化了一定的工作。但同时,也加重了JVM的工作。这就是JAVA程序运行速度较慢的原因之一。因为GC为了能够正确释放对象,必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC都需要进行监控。

为了更好的理解GC的工作原理,我们可以将对象考虑为有向图的顶点,将引用关系考虑为图的有向边,有向边从引用者指向被引用对象。另外,每个线程对象可以作为一个图的起始顶点,例如,大多程序从main进程开始执行,那么该图就是以main进行顶点开始的一棵根树。在这个有向图中,根顶点可达的对象都是有效对象,GC将不回收这些对象。如果某个对象(连通子图)对象不再被引用,可以被GC回收。

用有向图表示内存管理。对象程序的每一个时刻,都有一个有向图表示JVM的内存分配情况。下图就是左边程序运行到第六行的示意图。


图1:有向图表示内存示例

我们来看一个例子,观察如下代码,判定对象是否被回收。

		Vector v = new Vector();
		for (int i = 0; i < 100; i++) {
			Object o = new Object();
			v.add(o);
			o = null;
		}
	

用有向图的形式可以表示,如图2。

图2:示意图(应该是有100个o,大概示意下,读者明确的就是v到这100个Obj内存空间是可达的)

我们发现,虽然o=null了,可是依旧没有被回收(原因很明显,该对象依旧被引用),这也正是JAVA中同样存在内存泄漏的原因所在!下面我们来解释下什么是JAVA中的内存泄漏。


3、什么是JAVA中的内存泄漏?

在JAVA中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点:(1)对象是可达的,即在有向图中,存在有向路可以与其相连;(2)对象是无用的,即程序以后不会再使用这些对象。

如果对象满足上面的(1)(2)条件,这些对象就可以判定为JAVA中的内存泄漏,这些对象不会被GC所回收,然而他们却占用内存。

在C++中,内存泄漏的范围更大一些。有些对象被分配了内存空间,然后却不可达,由于C++中没有GC,这些内存将永远收不回来。在JAVA中,这些不可达的对象都由GC负责回收,因此,程序员不需要考虑这部分内存泄漏。对于C++,程序员需要自己管理边和顶点,而对于JAVA程序员,只需要管理边就可以了,通过这种方式,JAVA提高了编程效率。

对于程序员而言,GC基本是透明的、不可见的。虽然我们只有几个函数可以访问GC,例如,运行GC的函数System.gc(),但是根据JAVA语言规范定义,该函数不保证JVM的垃圾收集器一定会执行。因为不同的JVM实现者可能不同的算法管理GC。通常,GC线程的优先级别较低。JVM调用GC的策略也有很多种,有的是内存使用到达一定程度时,GC才开始工作,也有定时执行的,有的是平缓执行GC,有的是中段式执行GC。但通常来说,我们不需要关心这些,除非在一些特定的场合,GC的执行影响应用程序的性能,例如,对于基于Web的实时系统,如网络游戏等,用户不希望GC突然中断应用程序的执行而进行垃圾回收,那么需要调整GC的参数,让GC能够通过平缓的方式释放内存,例如,将垃圾回收分解为一系列的小步骤执行。Sun提供的HotSpotJVM就支持这一特性。

参考文献:

1、http://www.iteye.com/topic/838030。

2、JAVA程序员面试宝典(第二版)(欧立奇。刘洋,段韬)。

抱歉!评论已关闭.