@XingdingCAO
2017-11-16T21:07:58.000000Z
字数 3306
阅读 1841
reference
Java
在从像C、C++等这样手动回收内存的语言,转到Java这样的自动回收内存的语言时,我们可能会感叹编程语言的人性化发展。但是,Java的GC(Garbage Collection)机制也还没有足够智能到让我们忘记内存管理这回事。
public class Stack{
private static int DEFAULT_INITIAL_CAPACITY=16;
private Object[] elements;
prvate int size=0;
public Stack(){elements=new Object[DEFAULT_INITIAL_CAPACITY];
public void push(Object obj){
ensureCapacity();
elements[size++]=obj;
}
public Object pop{
if(size==0)
throw new StackEmptyException();
return elements[--size];
}
private void ensureCapacity(){
if(size==elements.length)
elements=Array.copyOf(elements,2*size+1);
}
}
上面的例子中,犯了一个错误,导致使用这段代码时,可能会发生内存泄漏(定义如下)的问题。
- 内存泄露简单来讲就是,程序在运行时,没有释放不必要占用的内存。在面向对象的编程中,就是实际占用着内存的对象却无法被使用了。
- 详细定义(摘自Wikipedia中的Memory Leak条目):
—— In computer science, a memory leak is a type of resource leak that occurs when a computer program incorrectly manages memory allocations in such a way that memory which is no longer needed is not released.
—— In object-oriented programming, a memory leak may happen when an object is stored in memory but cannot be accessed by the running code.
—— A memory leak has symptoms similar to a number of other problems and generally can only be diagnosed by a programmer with access to the program's source code.
我们都知道Java的数据类型分为值类型与引用类型,而Object类及其子类(也就是Java中所有的类)都是引用类型。在上面例子中的pop()
方法执行时,仅仅使size
的值减小了1,而没有释放掉无法被用户获取却依然在数组中保留引用的elements[size-1]
。GC判定所有存在引用的对象均为有效对象,但是实际上已经无法取得它的引用。无法被使用的对象逐渐积累,导致内存占用不断提升。
上面的例子仅需要将数组中无用的引用置空即可使GC正确回收内存,解决内存泄露问题。这也是解决Java中内存泄露的方法——将过时的引用置空。
public Object pop(){
if(size==0)
throw new StackEmptyException();
Object ret = elements[size-1];
elements[--size] = null;//消除过时的引用,将引用置空
return ret;
}
将引用置空除了可以释放不必占用着的内存,还会在错误访问应被弃置的对象时抛出NullPointerException
,保证了程序的稳定性与正确性。
例如:在上例中Stack
类内方法假如错误地访问已经被pop出去的对象时,就会抛出异常。
消除过时的引用除了将引用置空,还可以通过指定变量的作用域来实现。变量在结束其生命周期后便被回收,整个回收过程都由GC完成,无需我们插手。因此。我们应该仔细考量变量的作用域,将其放置在最小的作用域。
必须置空引用的情形通常发生在自我维系内存池的程序中,例如:上例中Stack
类自己维系着若干个Object
对象的引用,极易造成无意的内存泄露。对于此种情况,程序编写者必须时刻提醒自己,在对象没有用处时,及时置空,防止内存泄漏。
还需要注意的就是在对Cache(缓存)
的使用时,我们时常在无需使用缓存时忘记释放对应内存。
key(键)
,比特流作为value(值)
,存入一个HashMap
中作为缓存。但是,这样做最大的问题就是,图片比特流通常占用着大量的内存,而HashMap
不会自动释放一对key
和value
,当我们进行多项操作后忘却内存的释放时,悲剧就发生了。(悲惨的教训——永远不要相信使用者,尽量交给代码去完成) 使用WeakHaspMap
这类使用了Weak Reference
的类,作为缓存的存储容器。
Weak Reference
在这里就是指:在将key
置空后,下一轮GC操作就会回收这个key
对应的“键-值对”。
有关
WeakHaspMap
的详细情况可参考:http://www.baeldung.com/java-weakhashmap
String key = "img_name";
String value = "img_bits";
Map<String,String> cache = new WeakHashMap<>();
cache.put(key,value);
//JUnit中的方法 —— assertTrue()
assertTrue(map.containsKey(key));//无输出,说明包含key
key = null;
System.gc();
//阻塞直到内存被GC回收
await().atMost(10, TimeUnit.SECONDS).until(() -> map.size() == 0);
assertTrue(map.containsKey(key));//有输出,说明不包含key
使用LinkedHaspMap
这类自动回收内存的类作为缓存的容器。
关于自动回收内存可以通过后台线程,如Timer
或ScheduledThreadPoolExcutor
;还可以在添加新的“键-值对”时进行。
//LinkedHaspMap源码 JDK8
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
此外,另外一个常见的情况就是,为Listerner
设置Callback
。
当你继承某个API,为某个Listener
注册了Callback
回调,但是却没有释放。这样会使Callback
的内存占用逐渐提升,造成内存泄漏。
解决方法:使用Weak Reference
的类来存储Callback
,如WeakHaspMap
。
内存泄漏并不能被显式地检测到,仅能依靠编码者的代码走查,然后配合工具检测是否发生了内存泄漏的现象。可悲的是,现在运行的程序就在发生着内存泄漏,却无法被用户感知,知道内存被挤爆。所以,我们应该培养足够的编程素养,尽量防止这种问题的发生。