当你从手工管理内存的语言(比如 C 或 C++)转换到具有垃圾回收功能的比如 Java 语言时,我们的工作会变得更容易,因为当你用完对象后,会进行自动的垃圾回收。它很容易给你留下这样的印象,认为自己不再需要考虑内存管理的事情,其实不然
内存泄漏来源1-无意识的对象保持
先提供书中案例:
/**
* 简单的栈实现
*
* @author misury
**/
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0)
throw new EmptyStackException();
return elements[--size];
}
/**
* 修改后的版本
* @return
*/
public Object popVersion2(){
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null;
return result;
}
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
书中阐述这段程序并没有很明显的错误。无论如何测试,都会成功通过每一项测试,但是这个程序隐藏着一个**“内存泄漏”**问题。随着垃圾回收器活动的增加,或由于内存占用不断增加,程序性能降低逐渐表现出来。
程序中哪里发生内存泄漏?如果一个栈先是增长,然后再收缩,那么,**栈中弹出来的对象将不会被当作垃圾回收,即使使用栈的程序不再引用这些对象,它们也不会被回收。**因为栈内部维护着这些对象的过期引用(永远也不会再被解除的引用)。本例中,凡是在 elements 数组的“活动部分”之外的任何引用都是过期的。活动部分是指 elements 中下标小于 size 的那些元素。优化后的版本也在上面程序中:popVersion2()方法
本人对前面这段话并没有很理解,有明白的大佬求指点。所以找了下面这一段我认为和上面这段代码想表达意思相同的程序:
/**
* 运行的JVM参数:-Xmx64M -Xms64M
*
* @author misury
**/
public class MemoryDemo {
public static void main(String[] args) {
Runtime runtime = Runtime.getRuntime();
// 1. 获取现在空闲内存
System.out.println("Now JVM total free memory: " + runtime.freeMemory());
ArrayList<String> list = new ArrayList<>(1000000);
// 2. 获取创建了一个100w容量的list后的空闲内存
System.out.println(runtime.freeMemory());
for (int i = 0; i < 1000000; i++) {
list.add(new String("Rubbish"));
}
// 3. 获取list里面装了100w个元素后的空闲内存
System.out.println("Free memory after create 1000000 rubbish :" + runtime.freeMemory());
// 4. 触发GC
System.gc();
try {
Thread.sleep(100);
} catch (Exception e) {
e.printStackTrace();
}
// 5. 获取触发GC后的空闲内存
System.out.println("使用list.clear方法前进行垃圾回收的效果: " +runtime.freeMemory());
System.out.println("------------------");
list.clear();
System.gc();
try {
Thread.sleep(100);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("使用list.clear方法后进行垃圾回收的效果: " + runtime.freeMemory());
}
}
运行结果:
Now JVM total free memory: 64519120
60324816
Free memory after create 1000000 rubbish :37701288
使用list.clear方法前进行垃圾回收的效果: 37786368
------------------
使用list.clear方法后进行垃圾回收的效果: 61661576
与书中案例不同,通过控制台输出的内容可以看出,在没有调用list.clear()时,进行垃圾回收,List添加进去的元素没有办法回收掉,从clear()入手可以看到,清除的时候会将list的数组元素置空,清除该引用就会被垃圾回收器回收掉
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
在支持垃圾回收的语言中,内存泄漏是很隐蔽的(称这类内存泄漏为“无意识的对象保持”更为恰当)。如果一个对象引用被无意识地保留起来,垃圾回收机制不仅不会处理这个对象,而且也不会处理被这个对象引用的所有其他对象。修改这类问题的方法:一旦对象引用已经过期,只需清空这些引用
清空过期引用的另一好处是,如果它们以后又被错误的接触引用,程序就会立即抛出NPE异常,而不是悄悄地错误运行下去。尽快地检测出程序中的错误总是有益的
当我们第一次被类似这样的问题困扰时,就会变得过分小心:对于每一个对象引用,一旦程序不再用到它,就把它清空。**一朝被蛇咬,十年怕井绳。**其实没必要,清空对象引用应该是一种例外,而不是一种规范行为。
一般来说,**只要类是自己管理内存,我们就应该警惕内存泄漏问题。**一旦元素被释放掉,则该元素中包含的任何对象引用都应该被清空
内存泄漏来源2 - 缓存
内存泄漏的另一个常见来源是缓存。 一旦把对象引用放到缓存中,就很容易被遗忘掉,从而使得它不再有用之后很长一段时间仍然留在缓存中。如果你正好要实现这样的缓存:**只要在缓存之外存在某个项的键的引用,该项就有意义,那么就可以用WeakHashMap代表缓存;**缓存中的项过期后,它们就会被自动删除
更为常见的情况是“缓存项的生命周期是否有意义”不容易确定,随着时间推移,其中项会变得越来越没价值。这种情况,缓存应该时不时清除掉没用的内容。清除工作可以由一个后台线程来完成,或也可以在给缓存添加新条目的时候顺便清理。LinkedHashMap利用它的removeEldestEntry方法可以很容易实现后一种方案
内存泄漏来源3 - 监听器和其他回调
如果你实现了一个 API,客户端在这个 API 中注册回调,却没有显式地取消注册。那么除非你采取某些动作,否则它们会不断堆积。确保回调立即被当作垃圾回收的最佳方法是只保存它们的弱引用。例如:只讲它们保存成WeakHashMap中的键。
由于内存泄漏通常不会表现成明显的失败,所以它们可以在一个系统中存在很多年。往往只有通过仔细检查代码,或借助于Heap分析工具才能发现内存泄漏问题。因此,如果能在内存泄漏发生之前就知道如何预测此类问题,阻止它们发生就最好不过了。