Java 内存管理原理、内存泄漏实例及解决方案研究 java
在项目的最后阶段,就是要防止系统的内存泄漏了,顺便找了些资料,看了些java内存泄漏的实例及解决,总结一下: 程序员
Java是如何管理内存 算法
为了判断Java中是否有内存泄露,咱们首先必须了解Java是如何管理内存的。Java的内存管理就是对象的分配和释放问题。在Java中,程序员须要经过关键字new为每一个对象申请内存空间 (基本类型除外),全部的对象都在堆 (Heap)中分配空间。另外,对象的释放是由GC决定和执行的。在Java中,内存的分配是由程序完成的,而内存的释放是有GC完成的,这种收支两条线的方法确实简化了程序员的工做。但同时,它也加剧了JVM的工做。这也是Java程序运行速度较慢的缘由之一。由于,GC为了可以正确释放对象,GC必须监控每个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC都须要进行监控。 编程
监视对象状态是为了更加准确地、及时地释放对象,而释放对象的根本原则就是该对象再也不被引用。 数组
为了更好理解GC的工做原理,咱们能够将对象考虑为有向图的顶点,将引用关系考虑为图的有向边,有向边从引用者指向被引对象。另外,每一个线程对象能够做为一个图的起始顶点,例如大多程序从main进程开始执行,那么该图就是以main进程顶点开始的一棵根树。在这个有向图中,根顶点可达的对象都是有效对象,GC将不回收这些对象。若是某个对象 (连通子图)与这个根顶点不可达(注意,该图为有向图),那么咱们认为这个(这些)对象再也不被引用,能够被GC回收。 缓存
如下,咱们举一个例子说明如何用有向图表示内存管理。对于程序的每个时刻,咱们都有一个有向图表示JVM的内存分配状况。如下右图,就是左边程序运行到第6行的示意图。安全
Java使用有向图的方式进行内存管理,能够消除引用循环的问题,例若有三个对象,相互引用,只要它们和根进程不可达的,那么GC也是能够回收它们的。这种方式的优势是管理内存的精度很高,可是效率较低。另一种经常使用的内存管理技术是使用计数器,例如COM模型采用计数器方式管理构件,它与有向图相比,精度行低(很难处理循环引用的问题),但执行效率很高。 性能优化
什么是Java中的内存泄露 网络
下面,咱们就能够描述什么是内存泄漏。在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特色,首先,这些对象是可达的,即在有向图中,存在通路能够与其相连;其次,这些对象是无用的,即程序之后不会再使用这些对象。若是对象知足这两个条件,这些对象就能够断定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。 数据结构
在C++中,内存泄漏的范围更大一些。有些对象被分配了内存空间,而后却不可达,因为C++中没有GC,这些内存将永远收不回来。在Java中,这些不可达的对象都由GC负责回收,所以程序员不须要考虑这部分的内存泄露。
经过分析,咱们得知,对于C++,程序员须要本身管理边和顶点,而对于Java程序员只须要管理边就能够了(不须要管理顶点的释放)。经过这种方式,Java提升了编程的效率。
所以,经过以上分析,咱们知道在Java中也有内存泄漏,但范围比C++要小一些。由于Java从语言上保证,任何对象都是可达的,全部的不可达对象都由GC管理。
对于程序员来讲,GC基本是透明的,不可见的。虽然,咱们只有几个函数能够访问GC,例如运行GC的函数System.gc(),可是根据Java语言规范定义, 该函数不保证JVM的垃圾收集器必定会执行。由于,不一样的JVM实现者可能使用不一样的算法管理GC。一般,GC的线程的优先级别较低。JVM调用GC的策略也有不少种,有的是内存使用到达必定程度时,GC才开始工做,也有定时执行的,有的是平缓执行GC,有的是中断式执行GC。但一般来讲,咱们不须要关心这些。除非在一些特定的场合,GC的执行影响应用程序的性能,例如对于基于Web的实时系统,如网络游戏等,用户不但愿GC忽然中断应用程序执行而进行垃圾回收,那么咱们须要调整GC的参数,让GC可以经过平缓的方式释放内存,例如将垃圾回收分解为一系列的小步骤执行,Sun提供的HotSpot JVM就支持这一特性。
下面给出了一个简单的内存泄露的例子。在这个例子中,咱们循环申请Object对象,并将所申请的对象放入一个Vector中,若是咱们仅仅释放引用自己,那么Vector仍然引用该对象,因此这个对象对GC来讲是不可回收的。所以,若是对象加入到Vector后,还必须从Vector中删除,最简单的方法就是将Vector对象设置为null。Vector v=new Vector(10);
for (int i=1;i<100; i++)
{
Object o=new Object();
v.add(o);
o=null;
}
//此时,全部的Object对象都没有被释放,由于变量v引用这些对象。
Java内存泄漏的类型、实例及解决
1.对象游离
一种形式的内存泄漏有时候叫作对象游离(object loitering),是经过清单 1 中的 LeakyChecksum 类来讲明的,清单 1 中有一个 getFileChecksum() 方法用于计算文件内容的校验和。getFileChecksum() 方法将文件内容读取到缓冲区中以计算校验和。一种更加直观的实现简单地将缓冲区做为 getFileChecksum() 中的本地变量分配,可是该版本比那样的版本更加 “聪明”,不是将缓冲区缓存在实例字段中以减小内存 churn。该 “优化”一般不带来预期的好处;对象分配比不少人指望的更便宜。(还要注意,将缓冲区从本地变量提高到实例变量,使得类若不带有附加的同步,就再也不是线程安全的了。直观的实现不须要将 getFileChecksum() 声明为 synchronized,而且会在同时调用时提供更好的可伸缩性。)
清单 1. 展现 “对象游离” 的类
// BAD CODE - DO NOT EMULATE
public class LeakyChecksum {
private byte[] byteArray;
public synchronized int getFileChecksum(String fileName) {
int len = getFileSize(fileName);
if (byteArray == null || byteArray.length < len)
byteArray = new byte[len];
readFileContents(fileName, byteArray);
// calculate checksum and return it
}
}
这个类存在不少的问题,可是咱们着重来看内存泄漏。缓存缓冲区的决定极可能是根据这样的假设得出的,即该类将在一个程序中被调用许屡次,所以它应该更加有效,以重用缓冲区而不是从新分配它。可是结果是,缓冲区永远不会被释放,由于它对程序来讲老是可及的(除非 LeakyChecksum 对象被垃圾收集了)。更坏的是,它能够增加,却不能够缩小,因此 LeakyChecksum 将永久保持一个与所处理的最大文件同样大小的缓冲区。退一万步说,这也会给垃圾收集器带来压力,而且要求更频繁的收集;为计算将来的校验和而保持一个大型缓冲区并非可用内存的最有效利用。
LeakyChecksum 中问题的缘由是,缓冲区对于 getFileChecksum() 操做来讲逻辑上是本地的,可是它的生命周期已经被人为延长了,由于将它提高到了实例字段。所以,该类必须本身管理缓冲区的生命周期,而不是让 JVM 来管理。
软引用
弱引用如何能够给应用程序提供当对象被程序使用时另外一种到达该对象的方法,可是不会延长对象的生命周期。Reference 的另外一个子类 —— 软引用 —— 可知足一个不一样却相关的目的。其中弱引用容许应用程序建立不妨碍垃圾收集的引用,软引用容许应用程序经过将一些对象指定为 “expendable” 而利用垃圾收集器的帮助。尽管垃圾收集器在找出哪些内存在由应用程序使用哪些没在使用方面作得很好,可是肯定可用内存的最适当使用仍是取决于应用程序。若是应用程序作出了很差的决定,使得对象被保持,那么性能会受到影响,由于垃圾收集器必须更加辛勤地工做,以防止应用程序消耗掉全部内存。
高速缓存是一种常见的性能优化,容许应用程序重用之前的计算结果,而不是从新进行计算。高速缓存是 CPU 利用和内存使用之间的一种折衷,这种折衷理想的平衡状态取决于有多少内存可用。若高速缓存太少,则所要求的性能优点没法达到;若太多,则性能会受到影响,由于太多的内存被用于高速缓存上,致使其余用途没有足够的可用内存。由于垃圾收集器比应用程序更适合决定内存需求,因此应该利用垃圾收集器在作这些决定方面的帮助,这就是件引用所要作的。
若是一个对象唯一剩下的引用是弱引用或软引用,那么该对象是软可及的(softly reachable)。垃圾收集器并不像其收集弱可及的对象同样尽可能地收集软可及的对象,相反,它只在真正 “须要” 内存时才收集软可及的对象。软引用对于垃圾收集器来讲是这样一种方式,即 “只要内存不太紧张,我就会保留该对象。可是若是内存变得真正紧张了,我就会去收集并处理这个对象。” 垃圾收集器在能够抛出 OutOfMemoryError 以前须要清除全部的软引用。
经过使用一个软引用来管理高速缓存的缓冲区,能够解决 LeakyChecksum 中的问题,如清单 2 所示。如今,只要不是特别须要内存,缓冲区就会被保留,可是在须要时,也可被垃圾收集器回收:
清单 2. 用软引用修复 LeakyChecksum
public class CachingChecksum {
private SoftReferencebufferRef;
public synchronized int getFileChecksum(String fileName) {
int len = getFileSize(fileName);
byte[] byteArray = bufferRef.get();
if (byteArray == null || byteArray.length < len) {
byteArray = new byte[len];
bufferRef.set(byteArray);
}
readFileContents(fileName, byteArray);
// calculate checksum and return it
}
}
二、基于数组的集合
当数组用于实现诸如堆栈或环形缓冲区之类的数据结构时,会出现另外一种形式的对象游离。清单 3 中的 LeakyStack 类展现了用数组实现的堆栈的实现。在 pop() 方法中,在顶部指针递减以后,elements 仍然会保留对将弹出堆栈的对象的引用。这意味着,该对象的引用对程序来讲仍然可及(即便程序实际上不会再使用该引用),这会阻止该对象被垃圾收集,直到该位置被将来的 push() 重用。
清单 3. 基于数组的集合中的对象游离
public class LeakyStack {
private Object[] elements = new Object[MAX_ELEMENTS];
private int size = 0;
public void push(Object o) { elements[size++] = o; }
public Object pop() {
if (size == 0)
throw new EmptyStackException();
else {
Object result = elements[--size];
// elements[size+1] = null;
return result;
}
}
}
修复这种状况下的对象游离的方法是,当对象从堆栈弹出以后,就消除它的引用,如清单 3 中注释掉的行所示。可是这种状况 —— 由类管理其本身的内存 —— 是一种很是少见的状况,即显式地消除再也不须要的对象是一个好主意。大部分时候,认为不该该使用的强行消除引用根本不会带来性能或内存使用方面的收益,一般是致使更差的性能或者 NullPointerException。该算法的一个连接实现不会存在这个问题。在连接实现中,连接节点(以及所存储的对象的引用)的生命期将被自动与对象存储在集合中的期间绑定在一块儿。弱引用可用于解决这个问题 —— 维护弱引用而不是强引用的一个数组 —— 可是在实际中,LeakyStack 管理它本身的内存,所以负责确保对再也不须要的对象的引用被清除。使用数组来实现堆栈或缓冲区是一种优化,能够减小分配,可是会给实现者带来更大的负担,须要仔细地管理存储在数组中的引用的生命期。