Java之所以能够成为世界上最受欢迎的语言,与其垃圾回收机制分不开。我们Javaer能够在创建完对象后就不用管她的生死,确实是十分方便(真特么是个渣男)。可是有时候因为你创建了她,又对她爱答不理,就很有可能出大问题。
强哥在这给你看个代码:
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]; } private void ensureCapacity() { if (elements.length == size) elements = Arrays.copyOf(elements, 2 * size + 1); }}
一个简单的栈实现,代码规范,符合逻辑,本地测试也测不出什么问题。可是当你将这个栈类用到生产环境中,在一段时间过后,你很有可能就会发现随着垃圾回收活动不断增加或者内存占用不断增加,程序会开始越来越卡,而且垃圾回收慢慢的不管用了。
没错,上面的代码隐藏着一个内存泄漏的问题。
在极端情况下,这种内存泄漏可能导致磁盘分页,甚至出现 OutOfMemoryError 程序故障。
那么内存泄漏在哪里呢?罪魁祸首就是在pop方法中:如果堆栈增长,然后收缩,虽然减小了size,但是数组的总体长度是不会减小的,而被弹出的那个对象我们如果使用后没有手动做清除处理的话,由于栈中的elements数组还保持着对该对象的引用,那么弹出的对象即使在使用完后也不会被垃圾回收器收集。
这一点确实非常容易被人所忽略,但是如果发现了,要处理起来也很简单,方法如下:
public Object pop() { if (size == 0) throw new EmptyStackException(); Object result = elements[--size]; elements[size] = null; // Eliminate obsolete reference return result;}
没错,在pop将数组中将弹出位的引用设置成null就可以了。这样用完该弹出对象后,垃圾回收机制就能够发现该对象并将其回收。
由于问题隐蔽,且对性能影响比较大。所以发现这样的问题之后,我们往往心里就会很担心这种情况的再次发生,然后做出每一个对象,一旦程序不使用了,就手动将该对象设置成null。额,强哥只想说,这样的话,那Java语言的优势就彻底没有了,而且代码会很难看。Effective Java中便提到:清空对象引用应该是一种例外,而不是一种规范行为。
消除过时引用的最佳方法是让包含引用的变量脱离作用域。如果你在最狭窄的范围内定义每个变量,那么这种情况自然会发生。
那么,什么时候应该取消引用呢?Stack 类的哪些方面容易导致内存泄漏?简单地说,它管理自己的内存。elements存储池包含的元素是对象引用,而不是对象本身。数组的活动部分(小于size的部分)中的元素被分配,而数组其余部分中的元素是空闲的。但是垃圾收集器没有办法知道这一点;对于垃圾收集器,元素数组中的所有对象引用都同样有效。只有我们自己知道数组的非活动部分不重要。只要数组元素成为非活动部分的一部分,就可以通过手动清空数组元素,有效地将这个事实传递给垃圾收集器。
这就像你去图书馆里借书,图书管系统记录了书被你借走了,标记这本书已被借出。但是当你还回去的时候,图书管的人员没有把这个这本书的状态改成已归还,可以外借的时候,其他人就没办法再借到这本书了。如果这种情况一直都存在的话,很久以后,这个图书馆的书就可能都借不了了,也就是我们所谓的内存泄漏。
所以,一般来说,当类管理自己的内存时,咱们就应该警惕内存泄漏。每当释放一个元素时,元素中包含的任何对象引用都应该被取消。