因为 JVM 提供了自动管理内存的能力,当我们用完了对象之后,它们会被自动回收,这也容易让我们产生“开发者不再需要考虑内存管理”的错觉了,其实不然。
上面提到,即使 JVM 为我们提供了垃圾回收器,将没用的对象回收以节省内存使用。下面我们通过一个例子,意识到内存泄露的存在:
public class Stack { private Object[] elements; private int size = 0; private static final int DEFAULT_INITIAL_CAPACITY = 16;
// 初始化数组长度为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]; }
/** * <p>扩容</p> */ private void ensureCapacity() { if (elements.length == size){ elements = Arrays.copyOf(elements, 2 * size + 1); } }}
复制代码
上面程序段隐藏着一个“内存泄露”的问题:随着垃圾回收器活动的增加,或者由于内存占用的不断增加,程序性能的降低会逐渐表现出来。
代码分析:
这个内存泄露的情况就是 pop() 方法,从栈弹出的对象将不会被当做垃圾回收,即使使用栈的程序不再引用这些对象,他们也不会被回收。原因就是,我们声明的栈内部(对象数组)维护着这些对象的过期引用(obsolete reference)。
Arrays.copyOf(elements, 2 * size + 1):扩容方法,底层声明一个两倍的内存空间,然后将原有的数组引用拷贝到新的内存空间里。这样导致引用永远保持存活。而弹出栈顶也仅仅是返回指针指向的元素地址,并未删除对象引用。
过期引用:指的是永远不会再被解除的引用。
在极端情况下,这种内存泄露会导致磁盘交换(Disk Paging),甚至程序失败(OutOfMemoryError 错误),即使这种情况非常少。
Java 语言的内存泄露是非常隐蔽的(无意识的对象保持,unintentional object retention)。
一个对象被无意识的保留起来,可能会导致潜在的重大影响:
-
垃圾回收机制不再处理这个对象
-
垃圾回收机制不再处理这个对象所引用的所以其它对象
因此,对 pop()方法的有了下面的优化:
/** * 弹出栈顶元素 */public Object pop(){ if (size == 0){ throw new EmptyStackException(); } Object result = elements[--size]; elements[size] = null; //释放对象引用 return result; }
复制代码
清空过期引用的好处之一是,可以尽快检测出程序中的错误,如果不清理导致往后继续被错误解除引用,程序会立即抛出 NullPointException 异常。
第一个内存泄露的常见原因是自行管理内存(例如,开头的 Stack 类):
-
自己管理内存(manage is own memory),存储池(storage pool)包含了 elements 数组(对象引用单元,而不是对象本身)的元素。
-
数组活动区域是已分配的(allocated),其余部分则是自由的(free),但是 GC 并不知道这一点,所以需要程序员自行将这个情况告知 GC。
解决方法:警惕类内存管理的场景,手动清空这些数组元素。
第二个内存泄漏的常见原因是缓存:一旦将对象引用放到缓存中,它很容易被遗忘掉,从而使得它不再有用并长期停留在缓存。
解决方法:使用 WeakHashMap 代表缓存,当缓存过期后会被自动删除。 参考《弱引用是什么》
在 Java 集合中有一种特殊的 Map 类型:WeakHashMap。WeakHashMap 继承于 AbstractMap,实现了 Map 接口。 和 HashMap 一样,WeakHashMap 也是一个散列表,它存储的内容也是键值对(key-value)映射,而且键和值都可以是 null。 不过 WeakHashMap 的键是“弱键”,里面存放了键对象的弱引用,当某个键不再正常使用时,会从 WeakHashMap 中被自动移除。当一个键对象被垃圾回收,那么相应的值对象的引用会从 Map 中删除。WeakHashMap 能够节约存储空间,可用来缓存那些非必须存在的数据。
第三个内存泄漏的常见原因是监听器与回调:如果你实现了某个 API,客户端在这个 API 中注册回调(例如,流程上需要调用其他服务接口),却没有显式取消注册,这样会导致这类回调请求会积聚。
解决方法:同样将它们的服务调用对象保存为弱引用(weak reference),例如 WeakHashMap 的键。
上文总结了 3 种常见的 Java 内存泄露场景和对应的解决办法。
我们虽然可以依赖于 GC,让软件系统不会表现为明显的失败,但如果开发者不注意内存泄露,那么风险依旧长期存在。
而我们往往只有通过仔细检查代码,或者借助 Heap 剖析工具(Heap Profiler)才能定位发现内存泄露问题。
因此,如果在问题发生前,有意识的阻止发生便是最好不过了。
《源码系列》
《经典书籍》
《Java并发编程实战:第2章 影响线程安全性的原子性和加锁机制》
《Java并发编程实战:第3章 助于线程安全的三剑客:final & volatile & 线程封闭》
《服务端技术栈》
《算法系列》
《读懂排序算法(六):二分查找算法》
《设计模式》