Java内存泄漏与内存溢出问题分析:从实例代码学习内存管理技巧
网上看到一个关于内存泄漏处理的例子,其原网址为:。接下来,笔者将对这篇文章中的代码进行具体分析,并且从中学习 JAVA 的内存管理。

有读者指出此处并非内存泄露问题,对此表示赞同,此处应该是内存溢出问题。
Q:在Java中怎么可以产生内存泄露?
在 Java 中,导致内存泄露的原因存在多种。其中一个典型的例子是,当一个 Key 类未实现 hasCode 和 equals 方法时,它在 HashMap 中被保存。这样最终会生成大量重复的对象。所有的内存泄露最终都会引发 OutOfMemoryError 异常。接下来通过一段简短的示例来进行说明,该示例通过无限循环来模拟内存泄露的情况。
import java.util.HashMap;
import java.util.Map;
public class Test {
主函数的入口点,用于启动 Java 应用程序的执行。在这个方法中,通常会进行一些初始化操作,然后调用其他方法来完成具体的业务逻辑。参数 args 是一个字符串数组,它可以接收命令行参数,这些参数在程序运行时传递给程序。通过遍历 args 数组,可以获取每个命令行参数的值,并根据需要进行相应的处理。在 main 方法结束时,程序的执行也会结束。
Map map = new HashMap(1000);
int counter = 0;
while (true) {
由于 Key 类的问题,会创建重复的对象。
map 把一个新的键值对放入其中,这个键是一个名为 "dummyKey" 的 Key 对象,值是 "value" 。
counter++;
if (counter % 1000 == 0) {
输出“map 的大小:”加上 map 的大小。
System.out.println("在计数 " + counter 之后的空闲内存");
获取可用内存的大小为 getFreeMemory()MB 。
sleep(100);
}
}
}
内部类的键没有哈希码()或等于()方法 -- 糟糕的实现
static class Key {
private String key;
public Key(String key) {
this.key = key;
}
}
在毫秒级的给定时间段内进行延迟
静态的 void 方法 sleep,其参数为 long 类型的 sleepFor,该方法用于使当前线程暂停执行指定的毫秒数。
try {
Thread.sleep(sleepFor);
尝试捕获 InterruptedException 异常,如果发生这种异常,则执行以下操作。
e.printStackTrace();
}
}
// get available memory in MB
获取空闲内存的方法是通过以下方式实现的:使用特定的代码来获取系统中当前可用的内存量,这个内存量以字节为单位进行表示,最终返回的是一个表示空闲内存大小的长整型数值。
获取运行时对象,即 Runtime.getRuntime(),接着获取其空闲内存,即 freeMemory(),然后将空闲内存除以 1024 再除以 1024,最后返回结果。
}
}
笔者运行时,其结果与那篇文中作者的运行结果不一致。之后配置了虚拟机的参数,发现虚拟机的运行内存是 2 的次方。对于不会设置参数的读者,可以查看网络资源。这里给出一种 Eclipse 的配置方法:右击项目,然后选择 Run As,再选择 Run Configuration。接着配置 JVM 参数,如图所示。
图1:JVM参数配置
接下来,运行该程序,我们得到这样的结果:
map size: 1000
map size: 2000
map size: 3000
........
map size: 24000
map size: 25000
map size: 49000
在主线程中出现异常,具体为 Java 堆空间溢出(Java.lang.OutOfMemoryError: Java heap space)。
在 java.util.HashMap 的 resize 方法处(来源未知)
在 java.util.HashMap 的 putVal 方法处(来源未知)
在 java.util.HashMap 的 put 方法处(未知来源)
在 test.Test 的 main 方法中,位于 Test.java 文件的第 13 行。
引起错误的原因较为明显,key 的类未实现 hashcode()和 equals()。这致使 hashmap 存储了大量相同的 entry。明明插入了 2 个 Key 相同的键值对,然而其个数却出现暴涨。好的,我们不使用 Key 这个类作为 Key,而是用 String 类作为 Key。具体情况如下:
Map map = new HashMap(1000);
再次运行后,发现之前出现的内存泄漏问题不再出现了。原因很简单,是因为没有覆写 hashcode()和 equal()方法,所以每次都能成功插入到 Map 中。要解决这个问题,需要覆写 hashcode()和 equal()。具体如下:
static class Key {
private String key;
public Key(String key) {
this.key = key;
}
@Override
如果 obj 为 null,那么返回 false;如果 obj 与当前对象不是同一个实例,那么返回 false;如果 obj 的类与当前对象的类不相同,那么返回 false;如果 obj 的类是当前对象的类,并且进行了强制类型转换后,各个属性的值都相等,那么返回 true。
if (obj instanceof Key)
最后将取出的 key 与传入的 key 进行比较。
else
return false;
}
@Override
public int hashCode() {
return key.hashCode();
}
}
再次运行后,内存泄漏问题得到了解决。另外要说明的是,Key 是从 Object 继承而来的。看完该代码,觉得自己很有必要对内存管理方面的知识进行回顾和加强,总结如下。
有读者指出上面所述并非内存泄露问题,对此表示赞同。此处应为内存溢出问题。
JAVA 中的垃圾收集器有哪些优势呢?它相对于以前的语言而言,优势具体体现在哪些方面?
过去的语言要求程序员要去显式地分配内存以及回收内存。然而这种通过手工进行的回收常常会引发“内存泄漏”这种情况(有过 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)条件,那么这些对象就能被判定为 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、。
JAVA 程序员面试宝典(第二版),其作者为欧立奇、刘洋和段韬。