GC机制和内存泄漏

Java有了GC还会出现内存泄漏吗?

虽然Java有GC垃圾自动回收功能,但并不是说Java程序就不会内存泄漏。如果一个对象没有地方会使用到,但是却仍然有引用指向他,那么垃圾回收器就无法回收他,这种情况就属于内存泄漏。这种泄漏可能属于短暂的(即程序运行一段时间后引用消除进而出发GC)也可能是程序级别的(即程序退出时才会回收)。Java的内存泄漏和C/C++的内存泄漏不一样,C/C++的内存泄漏可能是系统级别的,即使程序退出也无法被回收,只能重启系统。

垃圾回收机制

在程序运行过程中,每创建一个对象都会被分配一定的内存用以存储对象数据。如果只是不停的分配内存,那么程序迟早面临内存不足的问题。
所以在任何语言中,都会有一个内存回收机制来释放过期对象的内存,以保证内存能够被重复利用。
内存回收机制按照实现角色的不同可以分为两种,一种是程序员手动实现内存的释放(比如C语言)另一种则是语言内建的内存回收机制比如本文将要介绍的java垃圾回收机制。

Java的垃圾回收机制

在程序的运行时环境中,java虚拟机提供了一个系统级的垃圾回收(GC,Carbage Collection)线程,它负责回收失去引用的对象占用的内存。理解GC的前提是理解一些和垃圾回收相关的概念。
在java中GC主要回收哪些对象呢?

Java对象的实例存储在jvm的堆区,对于GC线程来说,这些对象有三种状态。

  1. 可触及状态:程序中还有变量引用,那么此对象为可触及状态。
  2. 可复活状态:当程序中已经没有变量引用这个对象,那么此对象由可触及状态转为可复活状态。CG线程将在一定的时间准备调用此对象的finalize方法(finalize方法继承或重写子Object),finalize方法内的代码有可能将对象转为可触及状态,否则对象转化为不可触及状态。
  3. 不可触及状态:只有当对象处于不可触及状态时,GC线程才能回收此对象的内存。

GC为了能够正确释放对象,必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC都需要进行监控,所以无论一个对象处于上文中的任何状态GC都会知道。

内存泄露

内存泄漏指由于错误的设计造成程序未能释放已经不再使用的内存,造成资源浪费。GC会自动清理失去引用的对象所占用的内存。但是,由于程序设计错误而导致某些对象始终被引用,那么将会出现内存泄漏。

说到内存泄露,就不得不提到内存溢出,这两个比较容易混淆的概念,我们来分析一下。

  1. 内存泄露:程序在向系统申请分配内存空间后(new),在使用完毕后未释放。结果导致一直占据该内存单元,我们和程序都无法再使用该内存单元,直到程序结束,这是内存泄露。

  2. 内存溢出:程序向系统申请的内存空间超出了系统能给的。比如内存只能分配一个int类型,我却要塞给他一个long类型,系统就出现oom。又比如一车最多能坐5个人,你却非要塞下10个,车就挤爆了。

大量的内存泄露会导致内存溢出(oom)。

分析内存泄漏

分析:

A对象引用B对象,A对象的生命周期(t1-t4)比B对象的生命周期(t2-t3)长的多。当B对象没有被应用程序使用之后,A对象仍然在引用着B对象。这样,垃圾回收器就没办法将B对象从内存中移除,从而导致内存问题,因为如果A引用更多这样的对象,那将有更多的未被引用对象存在,并消耗内存空间。

B对象也可能会持有许多其他的对象,那这些对象同样也不会被垃圾回收器回收。所有这些没在使用的对象将持续的消耗之前分配的内存空间。

如果长生命周期的对象持有短生命周期的引用,就很可能会出现内存泄露

简单的列子说明内存泄漏:

1
2
3
4
5
6
7
public class Simple {
Object object;
public void method1(){
object = new Object();
//...其他代码
}
}

这里的object实例,其实我们期望它只作用于method1()方法中,且其他地方不会再用到它,但是,当method1()方法执行完成后,object对象所分配的内存不会马上被认为是可以被释放的对象,只有在Simple类创建的对象被释放后才会被释放,严格的说,这就是一种内存泄露。解决方法就是将object作为method1()方法中的局部变量。
当然,如果一定要这么写,可以改为这样:

1
2
3
4
5
6
7
8
public class Simple {
Object object;
public void method1(){
object = new Object();
//...其他代码
object = null;
}
}

这样,之前“new Object()”分配的内存,就可以被GC回收。

Java常见的内存泄漏

  1. 数组使用的时候内存泄漏。
  2. 数据库连接,网络连接,IO连接等没有显示调用close关闭,会导致内存泄露
  3. 监听器的使用,在释放对象的同时没有相应删除监听器的时候也可能导致内存泄露
  4. 内部类和外部模块的引用

例1

比如下面的例子。使用数组实现了一个栈,有入栈和出栈两个操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class MyStack {
private Object[] elements;
private int Increment = 10;
private int size = 0;
public MyStack(int size) {
elements = new Object[size];
}
//入栈
public void push(Object o) {
capacity();
elements[size++] = o;
}
//出栈
public Object pop() {
if (size == 0)
throw new EmptyStackException();
return elements[--size];
}
//增加栈的容量
private void capacity() {
if (elements.length != size)
return;
Object[] newArray = new Object[elements.length + Increment];
System.arraycopy(elements, 0, newArray, 0, size);
}
public static void main(String[] args) {
MyStack stack = new MyStack(100);
for (int i = 0; i < 100; i++)
stack.push(new Integer(i));
for (int i = 0; i < 100; i++) {
System.out.println(stack.pop().toString());
}
}
}

这个程序是可用的,支持常用的入栈和出栈操作。但是,有一个问题没有处理好,就是当出栈操作的时候,并没有释放数组中出栈元素的引用,这导致程序将一直保持对这个Object的引用(此object由数组引用),GC永远认为此对象是可触及的,也就更加谈不上释放其内存了。这就是内存泄漏的一个典型案例。

针对此,修改后的代码为:

1
2
3
4
5
6
7
8
//出栈
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object o = elements[--size];
elements[size] = null;
return o;
}

例2

比如数据库连接(dataSourse.getConnection()),网络连接(socket)和io连接,以及使用其他框架的时候,除非其显式的调用了其close()方法(或类似方法)将其连接关闭,否则是不会自动被GC回收的。其实原因依然是长生命周期对象持有短生命周期对象的引用。

可能很多人使用过hibernate,我们操作数据库时,通过SessionFactory获取一个session:

1
Session session=sessionFactory.openSession();

完成后我们必须调用close()方法关闭:

1
session.close();

SessionFactory就是一个长生命周期的对象,而session相对是个短生命周期的对象,但是框架这么设计是合理的:它并不清楚我们要使用session到多久,于是只能提供一个方法让我们自己决定何时不再使用。
因为在close()方法调用之前,可能会抛出异常而导致方法不能被调用,我们通常使用try语言,然后再finally语句中执行close()等清理工作:

1
2
3
4
5
6
try{
session=sessionFactory.openSession();
//...其他操作
}finally{
session.close();
}