动态

详情 返回 返回

為什麼Java/Python程序無需關心內存釋放?揭秘垃圾回收(GC)的核心概念 - 动态 详情

在Java的編程世界裏,開發者既無需也無法像C/C++那樣手動調用malloc/free來管理內存的分配與回收,這一核心任務完全由Java虛擬機在幕後自動完成。這種自動化設計極大地簡化了編碼,將開發者從繁瑣且極易出錯的內存管理中解放出來。然而,這種便利性的背後隱藏着一個經典且複雜的難題:一個動態運行的程序,其對象創建和消亡的模式千變萬化,Java虛擬機如何高效地追蹤這些對象的生命週期,在正確的時間回收不再使用的內存,同時又不能過度影響程序的正常運行?這不僅是一個純粹的技術挑戰,更是一門關於平衡與取捨的系統設計藝術。本文將深入剖析Java虛擬機垃圾回收(Garbage Collection,GC)的核心邏輯,從底層的標記-清除算法到現代回收器的動態分區與併發策略,揭示自動化內存管理如何在程序響應速度(延遲)、內存空間利用率和計算資源吞吐量這三大核心指標之間實現精妙的平衡。

垃圾回收:為何需要自動大掃除
垃圾回收是一種自動化的內存管理機制。它的核心任務是自動追蹤並回收那些在程序中已經不再被任何活動部分引用的內存空間,即“垃圾”,從而將這些寶貴的內存資源釋放出來,以便後續的內存分配可以重新利用它們。
在許多高級編程語言(如Java、Python、C#、Golang等)中,開發者不需要(通常也不能)直接操作內存地址。內存的分配(創建對象時)和回收(對象不再使用時)都由語言的運行時系統(Runtime System)全權負責。這種自動化機制的初衷是為了從根本上避免一系列因手動內存管理而臭名昭著的嚴重問題:
1)內存泄漏(Memory Leak):程序員分配了內存後,忘記在不再需要時釋放它,導致可用內存隨程序運行不斷減少,最終耗盡系統資源,引發程序崩潰。
2)懸掛指針(Dangling Pointer):一個指針繼續指向一塊已經被釋放的內存區域。後續對該指針的任何讀寫操作都可能導致數據損壞、程序崩潰,甚至是嚴重的安全漏洞。
3)雙重釋放(Double Free):程序試圖對同一塊內存區域執行兩次釋放操作。這會破壞內存管理器的內部數據結構,導致不可預測的後果。
雖然垃圾回收帶來了巨大的編程便利性和系統穩定性,但它並非沒有代價。其主要的挑戰在於垃圾回收過程本身需要消耗計算資源,並且可能會導致應用程序的短暫暫停(Stop-the-world, STW),即所有業務線程被凍結。此外,垃圾回收觸發的時機和持續時間在某種程度上是不可預測的,這為實時性和低延遲應用帶來了挑戰。

垃圾回收的概念:對象、堆、根與分配

對象
對象(Object),在不同的使用場合其意思各不相同。例如,在面向對象編程(Object-Oriented Programming,OOP)中,對象被定義為具有屬性(也稱為狀態或字段)和行為(也稱為方法或函數)的實體。然而,在垃圾回收中,對象通常指的是應用程序動態創建並使用的數據集合。
image

通常,對象由兩部分組成:頭(Header)和域(Field)。
頭是對象中存儲對象自身信息的部分,主要包含對象的大小和類型。如果沒有這些信息,那麼將無法確定內存中對象的邊界,這對垃圾回收至關重要。
此外,頭部還預先存儲了執行垃圾回收所需的信息,這些信息會根據垃圾回收算法的不同而不同。例如,在對象的頭部設置一個標誌位(flag)來記錄對象是否已被標記,以便確定該對象是否可以被回收。
通常,垃圾回收算法中都會用到對象大小和類型信息。
域是對象中可供用户訪問的部分,類似於C語言中的結構體成員。用户可以引用或修改對象的域值,但通常無法直接更改頭部信息。
域中的數據類型主要分為兩類:非指針和指針。非指針類型是指直接使用的值,如數字、字符和布爾值。指針類型則是指向內存空間中某個區域的值。對於使用過C或C++的讀者來説,對指針應該非常熟悉。即使在像Java這樣的編程語言中,用户並未明確使用指針,但在Java虛擬機內部,指針仍然被使用。
在大多數語言的運行程序中,指針默認指向對象的首地址。這個約束條件簡化了垃圾回收以及語言處理程序的其他各種處理過程。
image


堆(Heap)是一種動態內存分配的數據結構。它允許程序在運行時請求並釋放內存。這與棧(Stack)不同,棧是在程序編譯時就已經分配好的內存空間。
當一個對象被創建(如通過new關鍵字或其他構造函數),系統會在堆內存中為其分配空間。這個對象將一直存在,直到沒有引用指向它,此時,它將被視為垃圾。垃圾回收的目標是識別並釋放這些無引用的對象所佔用的內存,以便這部分內存可以被重新分配。
當堆被所有活動對象佔滿時,就算運行垃圾回收也無法分配可用空間。通常,有以下兩種選擇:
1)中斷當前程序運行,輸出錯誤信息(例如OutOfMemoryError Exception);
2)擴大堆,分配可用空間。
在實際運行環境中,應儘量避免因內存不足導致的程序中斷。在沒有特殊內存限制的情況下,應優先考慮擴展堆。
在垃圾回收中,分塊(Chunk)指的是預先準備的用於有效分配對象的空間。初始狀態下,堆被一個大的分塊佔據。然後,程序會根據運行環境的需求將這個分塊劃分為適當的大小。對象在一段時間後會變為垃圾並被回收。此時,這部分被回收的內存空間再次成為分塊,為下次使用做好準備。換句話説,內存中的各個區塊都在重複着分塊->對象創建->垃圾回收->分塊的循環過程。

分配
分配(Allocation)通常是指在堆內存中為對象分配空間的過程,主要有兩種方式。
1)空閒鏈表(Free List):在這種方法中,所有的空閒內存塊通過鏈表連接在一起,每個空閒塊包含指向下一空閒塊的指針和大小信息。當需要分配內存時,系統遍歷這個鏈表尋找合適的空閒塊,並從鏈表中移除它;當內存塊被釋放時,它會被重新添加到鏈表中。這種方法可以處理任意大小的內存請求,但由於需要遍歷鏈表,操作可能較慢。
image

2)碰撞指針(Bump Pointer):在這種方法中,系統維護一個指針,指向堆內存中的當前位置。當需要分配內存時,系統只需將碰撞指針向上移動相應的大小,然後返回原來的指針值即可。這個過程非常快,因為它只需要一次簡單的指針加法操作。然而,碰撞指針的缺點是它不能直接處理內存釋放。當內存塊被釋放時,除非它恰好位於堆的頂部,否則系統無法將其空間重新添加到可用內存中。因此,碰撞指針通常與其他內存管理技術(如垃圾回收)結合使用。
image


根(Root)這個詞的意思是根基或根底。在垃圾回收中,根是指向對象的指針的起點部分。

obj = Object.new
obj.field1 = Object.new

在如上偽代碼中,obj是全局變量。首先,分配一個對象 (對象A),然後把obj代入指向這個對象的指針。然後,再分配一個對象 (對象B)。然後把obj.field1代入指向這個對象的指針。此時,全局變量空間及堆如圖所示。
image

因為可以使用obj直接從偽代碼中引用對象A,也就是説A是存活對象(活動對象)。此外,因為可以通過obj經由對象A引用對象B,所以對象B也是存活對象。因此垃圾回收必須保護這些對象。
垃圾回收把上述這樣可以直接或間接從全局變量空間中引用的對象視為存活對象(活動對象),與之對應的是死亡對象(非活動對象)。

Mutator
在垃圾回收中,Mutator是指能夠修改堆內存的代碼部分。這些代碼通常是應用程序的一部分,可以創建新對象、改變對象引用關係或釋放對象。
Mutator在運行時會改變堆中的數據結構,這可能會影響哪些對象是存活的,哪些是死亡對象,可以被垃圾回收。
在垃圾回收過程中,需要暫停或監控Mutator的行為。這是因為如果在回收過程中,Mutator繼續修改堆數據結構,可能導致內存處於不一致狀態,例如將不再需要的對象誤認為仍在使用。
因此,垃圾回收器和Mutator之間需要協調機制,以確保在回收過程中堆的數據結構保持一致。這通常通過暫停整個程序的方式或使用讀寫屏障(Read/Write Barrier)來實現。
image

未完待續

很高興與你相遇!如果你喜歡本文內容,記得關注哦

Add a new 评论

Some HTML is okay.