無論何種程式語言,記憶體分配的核心皆為返回分配記憶體的真實位址(即指標)。在 Java 中,物件透過 new 關鍵字或反射機制建立,均分配於堆積(Heap)區域。所有物件的回收工作全權委由 Java 虛擬機(JVM)的垃圾回收器(Garbage Collector, GC)負責。
GC 為了精準釋放物件,會持續監控每個物件的運行狀態,包含申請、引用、被引用、賦值等操作。Java 採用有向圖管理記憶體,即時追蹤物件是否可達;若物件不可達,則判定為垃圾進行回收,此機制亦能有效消除循環引用問題。
判斷記憶體空間是否符合垃圾回收標準,主要有兩種情況:一是物件被賦值為 null 且後續未再被呼叫;二是物件被賦予新值,導致原記憶體空間重新分配。
什麼是記憶體洩漏? 指無用物件(不再被使用的物件)持續佔用記憶體,或其記憶體無法及時釋放,造成記憶體空間浪費。輕微洩漏難以察覺,嚴重時則會拋出 OutOfMemoryError。
根本原因: 長生命週期物件持有短生命週期物件的引用。即便短生命週期物件已無用,因長生命週期物件仍引用它,導致 GC 無法回收,這便是 Java 記憶體洩漏的典型場景。以下歸納六大常見類別:
HashMap、Vector 等靜態集合最易導致洩漏。靜態變數生命週期與應用程式同在,其引用的所有物件亦無法釋放。
Static Vector v = new Vector(10);
for (int i = 1; i < 100; i++) {
Object o = new Object();
v.add(o);
o = null; // 僅釋放區域引用,Vector 仍持有物件引用
}
解決之道:物件加入集合後,若不再需要,必須從集合中移除(如 v.remove(o)),或將集合本身設為 null。
remove() 失效若集合元素(如 HashSet)的屬性被修改,導致 hashCode() 改變,則 remove() 無法找到原物件,造成洩漏並重複添加。
Set<Person> set = new HashSet<>();
Person p3 = new Person("豬八戒", "pwd3", 27);
set.add(p3);
p3.setAge(2); // 修改屬性,hashCode 改變
set.remove(p3); // 移除失敗
set.add(p3); // 重複添加成功,size 變 4
對策:作為 Set 元素或 Map 鍵值的物件,一旦放入集合,嚴禁修改參與 hashCode()/equals() 計算的屬性。
Java 程式常使用監聽器(addXXXListener()),若釋放物件時未呼叫對應的 removeXXXListener(),監聽器仍持有物件引用,造成洩漏。
資料庫連接(Connection)、網路連接(Socket)、I/O 流等,若未顯式呼叫 close(),GC 無法自動回收。特別注意:Connection 必須顯式關閉;使用連接池時,ResultSet、Statement 亦需顯式關閉。最佳實踐:在 try 區塊獲取連接,finally 區塊釋放。
內部類隱含持有外部類引用,若不釋放易導致一連串物件無法回收。跨模組呼叫(如模組 A 傳遞物件給模組 B 的 registerMsg(Object b))時,需確認模組 B 是否提供移除引用的操作,防止非預期持有引用。
單例物件生命週期贯穿 JVM 整個運行期。若單例持有外部物件引用,該外部物件將無法被回收。
class A {
public A() { B.getInstance().setA(this); }
}
class B { // 單例
private A a;
private static B instance = new B();
public void setA(A a) { this.a = a; }
}
此例中 B 持有 A 引用,導致 A 及其關聯物件永駐記憶體。若 A 為大型集合或複雜物件,後果嚴重。