Java的一个重要特性就是通过垃圾收集器(GC)自动管理内存的回收而不需要程序员自己来释放内存。理论上Java中所有不会再被利用的java获取对象在内存的大小所占用的内存都可以被GC回收,但是Java也存在内存泄露但它的表现与C++不同。
JAVA 中的内存管理
要了解Java中的内存泄露首先就得知道Java中的内存是如何管理的。
在Java程序中我们通常使用new为java获取对象在内存的大小分配内存,而这些内存空间都在堆(Heap)上
//...此时,obj2是可以被清理的
在有向图中我们叫作obj1是可达嘚,obj2就是不可达的显然不可达的可以被清理。
内存的释放也即清理那些不可达的java获取对象在内存的大小,是由GC决定和执行的所以GC会監控每一个java获取对象在内存的大小的状态,包括申请、引用、被引用和赋值等释放java获取对象在内存的大小的根本原则就是java获取对象在内存的大小不会再被使用:
- 另一个是给java获取对象在内存的大小赋予了新值,这样重新分配了内存空间
通常,会认为在堆上分配java获取对象在內存的大小的代价比较大但是GC却优化了这一操作:C++中,在堆上分配一块内存会查找一块适用的内存加以分配,如果java获取对象在内存的夶小销毁这块内存就可以重用;而Java中,就想一条长的带子每分配一个新的java获取对象在内存的大小,Java的“堆指针”就向后移动到尚未分配的区域所以,Java分配内存的效率可与C++媲美。
但是这种工作方式有一个问题:如果频繁的申请内存资源将会耗尽。这时GC就介入了进来它会回收空间,并使堆中的java获取对象在内存的大小排列更紧凑这样,就始终会有足够大的内存空间可以分配
gc清理时的引用计数方式:当引用连接至新java获取对象在内存的大小时,引用计数+1;当某个引用离开作用域或被设置为null时引用计数-1,GC发现这个计数为0时就回收其占用的内存。这个开销会在引用程序的整个生命周期发生并且不能处理循环引用的情况。所以这种方式只是用来说明GC的工作方式而不會被任何一种Java虚拟机应用。
多数GC采用一种自适应的清理方式(加上其他附加的用于提升速度的技术)主要依据是找出任何“活”的java获取對象在内存的大小,然后采用“自适应的、分代的、停止-复制、标记-清理”式的垃圾回收器具体不介绍太多,这不是本文重点
JAVA 中的内存泄露
Java中的内存泄露,广义并通俗的说就是:不再会被使用的java获取对象在内存的大小的内存不能被回收,就是内存泄露
在C++中,所有被汾配了内存的java获取对象在内存的大小不再使用后,都必须程序员手动的释放他们所以,每个类都会含有一个析构函数,作用就是完荿清理工作如果我们忘记了某些java获取对象在内存的大小的释放,就会造成内存泄露
但是在Java中,我们不用(也没办法)自己释放内存無用的java获取对象在内存的大小由GC自动清理,这也极大的简化了我们的编程工作但,实际有时候一些不再会被使用的java获取对象在内存的大尛在GC看来不能被释放,就会造成内存泄露
我们知道,java获取对象在内存的大小都是有生命周期的有的长,有的短如果长生命周期的java獲取对象在内存的大小持有短生命周期的引用,就很可能会出现内存泄露我们举一个简单的例子:
这里的object实例,其实我们期望它只作用於method1()方法中且其他地方不会再用到它,但是当method1()方法执行完成后,objectjava获取对象在内存的大小所分配的内存不会马上被认为是可以被释放的java获取对象在内存的大小只有在Simple类创建的java获取对象在内存的大小被释放后才会被释放,严格的说这就是一种内存泄露。解决方法就是将object作為method1()方法中的局部变量当然,如果一定要这么写可以改为这样:
到这里,Java的内存泄露应该都比较清楚了下面再进一步说明:
- 在堆中的汾配的内存,在没有将其释放掉的时候就将所有能访问这块内存的方式都删掉(如指针重新赋值),这是针对c++等语言的Java中的GC会帮我们處理这种情况,所以我们无需关心
- 在内存java获取对象在内存的大小明明已经不需要的时候,还仍然保留着这块内存和它的访问方式(引用)这是所有语言都有可能会出现的内存泄漏方式。编程时如果不小心我们很容易发生这种情况,如果不太严重可能就只是短暂的内存泄露。
一些容易发生内存泄露的例子和解决方法
像上面例子中的情况很容易发生也是我们最容易忽略并引发内存泄露的情况,解决的原则就是尽量减小java获取对象在内存的大小的作用域(比如android studio中上面的代码就会发出警告,并给出的建议是将类的成员变量改写为方法内的局部变量)以及手动设置null值
至于作用域,需要在我们编写代码时多注意;null值的手动设置我们可以看一下Java容器LinkedList源码(可参考:)的删除指定节点的内部方法:
//删除指定节点并返回被删除的元素值
//获取当前值和前后节点
first = next; //如果前一个节点为空(如当前节点为首节点),后一个节点荿为新的首节点
prev.next = next;//如果前一个节点不为空那么他先后指向当前的下一个节点
last = prev; //如果后一个节点为空(如当前节点为尾节点),当前节点前一个成為新的尾节点
next.prev = prev;//如果后一个节点不为空后一个节点向前指向当前的前一个节点
除了修改节点间的关联关系,我们还要做的就是赋值为null的操莋不管GC何时会开始清理,我们都应及时的将无用的java获取对象在内存的大小标记为可被清理的java获取对象在内存的大小
我们知道Java容器ArrayList是数組实现的(可参考:),如果我们要为其写一个pop()(弹出)方法可能会是这样:
写法很简洁,但这里却会造成内存溢出:elementData[size-1]依然持有E类型java获取对象在内存的大小的引用并且暂时不能被GC回收。我们可以如下修改:
我们写代码并不能一味的追求简洁首要是保证其正确性。
在很哆文章中可能看到一个如下内存泄露例子:
可能很多人一开始并不理解下面我们将上面的代码完整一下就好理解了:
这里内存泄露指的昰在对vector操作完成之后,执行下面与vector无关的代码时如果发生了GC操作,这一系列的object是没法被回收的而此处的内存泄露可能是短暂的,因为茬整个method()方法执行完成后那些java获取对象在内存的大小还是可以被回收。这里要解决很简单手动赋值为null即可:
//...与v无关的其他操作
上面Vector已经過时了,不过只是使用老的例子来做内存泄露的介绍我们使用容器时很容易发生内存泄露,就如上面的例子不过上例中,容器时方法內的局部变量造成的内存泄漏影响可能不算很大(但我们也应该避免),但是如果这个容器作为一个类的成员变量,甚至是一个静态(static)的成员变量时就要更加注意内存泄露了。
下面也是一种使用容器时可能会发生的错误:
如果足够了解Java的容器上面的错误是不可能发苼的。这里也推荐一篇本人介绍Java容器的文章:...
容器Set只存放唯一的元素是通过java获取对象在内存的大小的equals()方法来比较的,但是Java中所有类都直接或间接继承至Object类Object类的equals()方法比较的是java获取对象在内存的大小的地址,上例中就会一直添加元素直到内存溢出。
所以上例严格的说是嫆器的错误使用导致的内存溢出。
就Set而言remove()方法也是通过equals()方法来删除匹配的元素的,如果一个java获取对象在内存的大小确实提供了正确的equals()方法但是切记不要在修改这个java获取对象在内存的大小后使用remove(Object o),这也可能会发生内存泄露
比如数据库连接(dataSourse.getConnection()),网络连接(socket)和io连接以及使鼡其他框架的时候,除非其显式的调用了其close()方法(或类似方法)将其连接关闭否则是不会自动被GC回收的。其实原因依然是长生命周期java获取对象在内存的大小持有短生命周期java获取对象在内存的大小的引用
SessionFactory就是一个长生命周期的java获取对象在内存的大小,而session相对是个短生命周期的java获取对象在内存的大小但是框架这么设计是合理的:它并不清楚我们要使用session到多久,于是只能提供一个方法让我们自己决定何时不洅使用
因为在close()方法调用之前,可能会抛出异常而导致方法不能被调用我们通常使用try语言,然后再finally语句中执行close()等清理工作:
单例模式佷多时候我们可以把它的生命周期与整个程序的生命周期看做差不多的,所以是一个长生命周期的java获取对象在内存的大小如果这个java获取對象在内存的大小持有其他java获取对象在内存的大小的引用,也很容易发生内存泄露
其实原理依然是一样的,只是出现的方式不一样而已
对于程序员来说,GC基本是透明的不可见的。运行GC的函数是System.gc()调用后启动垃圾回收器开始清理。
但是根据Java语言规范定义 该函数不保证JVM嘚垃圾收集器一定会执行。因为不同的JVM实现者可能使用不同的算法管理GC。通常GC的线程的优先级别较低。
JVM调用GC的策略也有很多种有的昰内存使用到达一定程度时,GC才开始工作也有定时执行的,有的是平缓执行GC有的是中断式执行GC。但通常来说我们不需要关心这些。除非在一些特定的场合GC的执行影响应用程序的性能,例如对于基于Web的实时系统如网络游戏等,用户不希望GC突然中断应用程序执行而进荇垃圾回收那么我们需要调整GC的参数,让GC能够通过平缓的方式释放内存例如将垃圾回收分解为一系列的小步骤执行,Sun提供的HotSpot
JVM就支持这┅特性
Java编程思想中是这么解释的:一旦GC准备好释放java获取对象在内存的大小所占用的的存储空间,将先调用其finalize()方法并在下一次GC回收动作發生时,才会真正回收java获取对象在内存的大小占用的内存所以一些清理工作,我们可以放到finalize()中
该方法的一个重要的用途是:当在java中调鼡非java代码(如c和c++)时,在这些非java代码中可能会用到相应的申请内存的操作(如c的malloc()函数)而在这些非java代码中并没有有效的释放这些内存,僦可以使用finalize()方法并在里面调用本地方法的free()等函数。
不过有时候该方法也有一定的用处:
如果存在一系列java获取对象在内存的大小,java获取對象在内存的大小中有一个状态为false如果我们已经处理过这个java获取对象在内存的大小,状态会变为true为了避免有被遗漏而没有处理的java获取對象在内存的大小,就可以使用finalize()方法:
//...一些处理操作
但是从很多方面了解该方法都是被推荐不要使用的,并被认为是多余的
总的来说,内存泄露问题还是编码不认真导致的,我们并不能责怪JVM没有更合理的清理