如果使用的是C或C++語言,開發人員需要自己管理memory,但當轉換到有garbage collection的程式語言時,會突然覺得工作變輕鬆。但是有garbage collection機制,真的就可以完全不注意memory的使用嗎?不會有memory leak的問題嗎?
以下面使用array實作stack的程式碼為例,並用for迴圈push和pop 100個物件,當物件被pop出stack之後,在沒有做額外的處理情況下,因為每個物件被array reference,所以garbage collector以為它還有被使用,就沒有做garbage collection,結果這些已經被pop出去的物件,被遺留在記憶體中。簡單地用Runtime API計算佔用的記憶體容量,大約是26674776 bytes。
import java.lang.Exception;
import java.util.Arrays;
public class Stack {
private Object[] items;
private int size = 0;
private static final int DEFAULT_SIZE = 16;
public Stack() {
items = new Object[DEFAULT_SIZE];
}
public void push(Object item) {
ensureCapacity();
items[size++] = item;
}
public Object pop() throws Exception {
if (size == 0) {
throw new Exception("Stack is empty");
}
Object result = items[--size];
return result;
}
private void ensureCapacity() {
if (size == items.length) {
items = Arrays.copyOf(items, 2 * size + 1);
}
}
public static void main(String[] args) throws Exception {
long beforeUsedMem=Runtime.getRuntime().totalMemory()-Runtime.getRuntime().freeMemory();
Stack example = new Stack();
for (int i = 0; i < 100; i++) {
example.push(new byte[1024 * 1024]);
example.pop();
}
long afterUsedMem=Runtime.getRuntime().totalMemory()-Runtime.getRuntime().freeMemory();
long actualMemUsed=afterUsedMem-beforeUsedMem;
System.out.println(actualMemUsed);
}
}
而已經pop出去的物件,可以做什麼處理,避免記憶體的佔用呢?可以在pop的時候,把物件設為null ,這樣就可以消滅遺留的reference,解放記憶體。把物件設為null之後,再重新計算佔用的記憶體容量,大約是4637976 bytes,是沒有消滅遺留的reference的1/5左右。
import java.lang.Exception;
import java.util.Arrays;
public class Stack {
private Object[] items;
private int size = 0;
private static final int DEFAULT_SIZE = 16;
public Stack() {
items = new Object[DEFAULT_SIZE];
}
public void push(Object item) {
ensureCapacity();
items[size++] = item;
}
public Object pop() throws Exception {
if (size == 0) {
throw new Exception("Stack is empty");
}
Object result = items[--size];
items[size] = null;
return result;
}
private void ensureCapacity() {
if (size == items.length) {
items = Arrays.copyOf(items, 2 * size + 1);
}
}
public static void main(String[] args) throws Exception {
long beforeUsedMem=Runtime.getRuntime().totalMemory()-Runtime.getRuntime().freeMemory();
Stack example = new Stack();
for (int i = 0; i < 100; i++) {
example.push(new byte[1024 * 1024]);
example.pop();
}
long afterUsedMem=Runtime.getRuntime().totalMemory()-Runtime.getRuntime().freeMemory();
long actualMemUsed=afterUsedMem-beforeUsedMem;
System.out.println("Memory used: " + actualMemUsed + " bytes");
}
}
當然,使用null 清除reference這個方法,只能當不得已使用的workaround,因為如果不小心把正在被使用的reference設為null,很容易造成NullPointerException。
除了上面的範例會有memory leak的風險,cache也常常會有memory leak的問題,因為使用cache時,裡面的record是由開發人員管理,不是由JVM管理,所以有一些沒有被使用的record,很有可能被一直遺忘在cache裡面。所以在實作cache的時候,可以使用WeakHashMap ,這樣當裡面的資料沒有被reference的時,就會自動被清理,當然,也可以設置expire time,讓tool(ex: redis)自動去清理過期的record。