这是一个老生常谈的面试题,本文就系统讲解一下吧
虽然Java有GC垃圾⾃动回收功能,但并不是说Java程序就不会内存泄漏。如果一个对象没有地⽅会使⽤到,但是却仍然有引用指向他,那么垃圾回收器就无法回收他,这种情况就属于内存泄漏。这种泄漏可能属于短暂的(即程序运⾏一段时间后引用消除进⽽触发GC)也可能是程序级别的(即程序退出时才会回收)。Java的内存泄漏和C/C++的内存泄漏不一样,C/C++的内存泄漏可能是系统级别的,即使程序退出也无法被回收,只能重启系统。
-
垃圾回收机制
在程序运行过程中,每创建一个对象都会被分配一定的内存用以存储对象数据。如果只是不停的分配内存, 那么程序迟早⾯面临内存不足的问题。所以在任何语言中,都会有一个内存回收机制来释放过期对象的内存,以保证内存能够被重复利用。内存回收机制按照实现⻆色的不同可以分为两种,⼀种是程序员⼿动实现内存的释放(比如C语言)另⼀种则是语
言内建的内存回收机制,比如本文将要介绍的java垃圾回收机制。 -
Java的垃圾回收机制
在程序的运行时环境中,java虚拟机提供了了⼀个系统级的垃圾回收线程(GC,Carbage Collection),它负责回收失去引⽤的对象占⽤用的内存。理解GC的前提是理解⼀些和垃圾回收相关的概念,下⽂⼀一介绍这些概念。
Java对象的实例例存储在jvm的堆区,对于GC线程来说,这些对象有三种状态。
- 可触及状态:程序中还有变量量引⽤,那么此对象为可触及状态。
- 可复活状态:当程序中已经没有变量引用这个对象,那么此对象由可触及状态转为可复活状态。CG线程 将在一定的时间准备调⽤此对象的finalize()方法,finalize()方法内的代码有可能将对象转为可触及状态,否则对象转化为不可触及状态。
- 不可触及状态:只有当对象处于不可触及状态时,GC线程才能回收此对象的内存。
GC为了了能够正确释放对象,必须监控每一个对象的运行状态,包括对象的申请、引⽤、被引⽤、赋值等, GC都需要进行监控,所以无论一个对象处于上文中的任何状态GC都会知道。
内存泄露
内存泄漏指由于错误的设计造成程序未能释放已经不再使⽤用的内存,造成资源浪费。GC会⾃自动清理理失去引用的对象所占用的内存。但是,由于程序设计错误而导致某些对象始终被引⽤,那么将会出现内存泄漏漏。
说到内存泄露,就不得不提到内存溢出,这两个比较容易混淆的概念,我们来分析⼀下。
-
内存泄露:程序在向系统申请分配内存空间后(new),在使用完毕后未释放。结果导致一直占据该内存单元,我们和程序都⽆法再使⽤该内存单元,直到程序结束,这是内存泄露。
-
内存溢出:程序向系统申请的内存空间超出了系统能给的。⽐如内存只能分配⼀个int类型,我却要塞给他⼀个long类型,系统就出现oom。⼜比如一车最多能坐5个人,你却⾮要塞下10个,⻋就挤爆了。⼤量的内存泄露会导致内存溢出(oom)。
分析内存泄漏
分析:
- A对象引用B对象,A对象的⽣命周期(t1-t4)比B对象的生命周期(t2-t3)⻓的多。当B对象没有被应⽤程序使用之后,A对象仍然在引用着B对象。这样,垃圾回收器就没办法将B对象从内存中移除,从⽽导致内存问题,因为如果A引用更多这样的对象,那将有更多的未被引用对象存在,并消耗内存空间。
- B对象也可能会持有许多其他的对象,那这些对象同样也不会被垃圾回收器回收。所有这些没在使用的对象将持续的消耗之前分配的内存空间。
- 如果⻓生命周期的对象持有短⽣命周期的引用,就很可能会出现内存泄露
Java常见的内存泄漏
- 数组使用的时候内存泄漏。
- 数据库连接,网络连接,IO连接等没有显示调⽤close关闭,会导致内存泄露
- 监听器的使⽤,在释放对象的同时没有相应删除监听器的时候也可能导致内存泄露
比如下面的例子。使⽤数组实现了了一个栈,有⼊栈和出栈两个操作:
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永远认为此对象是可触及的,也就更加谈不上释放其内存了。这就是内存泄漏的⼀个典型案例。