必威电竞|足球世界杯竞猜平台

內存泄漏
來源:互聯網

內存泄漏(Memory Leak),是指由于疏忽或錯誤,造成程序未能釋放已經不再使用的內存的情況。程序在申請獲得動態內存塊并使用完畢后,沒有釋放所申請的動態內存就將保存動態內存地址的變量用于其他用途,使得這些動態內存不可能再被程序使用,也無法被操作系統回收。

內存泄漏以發生的方式來分類可分為常發性內存泄漏、偶發性內存泄漏、一次性內存泄漏和隱式內存泄漏。產生內存泄漏原因主要包括忘記釋放內存、存在內存泄漏的析構函數以及未釋放指針指向的對象等。起初,內存泄漏的變化常常被忽視,但是由于系統內存耗盡,反復泄漏會導致應用程序的性能下降,進而影響程序和系統運行。如果被有意者對其恢復應用,也就會出現信息泄漏,導致發生系統安全問題。

通常在內存泄漏檢測過程中,可以將其分成動態檢測方法以及靜態檢測方法兩種,常用的動態檢測工具有Mtrace,Memwatch,Purify等,靜態檢測工具有BEAM、PC-Lint等。內存泄漏檢測采用的關鍵技術包括垃圾回收、智能指針和虛擬技術等,可通過監控內存、清理棧等方法進行解決。

產生原因

未釋放分配的內存

導致內存泄漏最常見的一個原因是分配了內存后,忘記了釋放內存。一個進程運行時會占據一塊內存空間,進程結束后若不釋放內存,則該進程下次啟動時,將會占據新的內存空間。進程頻繁的啟動、停止,導致該進程占據的內存空間越來越大。一般來說,在軟件中若有某個功能頻繁啟動關閉,則容易產生內存泄漏。

存在內存泄漏的析構函數

當人們使用C/C++、delphi或java等開發工具編寫應用程序時,一般在構造函數中使用malloc、realloc、new等函數從堆中分配到一塊內存,該內存空間使用結束后,程序必須相應地調用free或delete等函數釋放該內存塊,以便操作系統重新分配與再次使用此內存。若在使用結束后,沒有及時在程序的全部執行路徑或析構函數中釋放此內存塊,并且無合適的指針指向該塊內存,使得該部分內存失去重用性,則會造成內存泄漏。

指針操作

循環引用

在某些語言中,兩個對象相互引用可能會導致兩個對象都無法被垃圾回收的情況,即使程序的其他部分沒有引用它們。例如,一個簡單的循環引用問題:有對象A和對象B,對象A中含有對象B的引用,對象B中含有對象A的引用。此時,對象A和B的引用計數器都不為0。但是,在系統中卻不存在任何第3個對象引用了A或B。也就是說,A和B是應該被回收的垃圾對象,但由于垃圾對象間的相互引用,從而使垃圾回收器無法識別,引起內存泄漏。

靜態集合

使用隨時間增長而從未被清除的靜態數據結構可能會導致內存泄漏。例如,向靜態列表添加元素而不刪除它們可能會導致列表無限增長。

外部類引用內部類

這個場景主要出現在非靜態內部類中,在類初始化時,內部類總是需要外部類的一個實例。每個非靜態內部類默認都持有外部類的隱式引用。如果在應用程序中使用該內部類的對象,即使外部類使用完畢,也不會對其進行垃圾回收。前端閉包與外部類引用內部類的情況非常類似,所以前端閉包運用得不好也會造成內存泄漏。

未關閉連接

在Java中,在對數據庫進行操作的過程時,首先需要建立與數據庫的連接,當不再使用時,需要調用close方法來釋放與數據庫的連接。只有連接被關閉后,垃圾回收器才會回收對應的對象。否則,如果在訪問數據庫的過程中,對Connection、Statement 或ResultSet不顯式地關閉,將會造成大量的對象無法被回收,從而引起內存泄漏。

事件偵聽器

不分離事件偵聽器或回調可能會導致內存泄漏,尤其是在Web瀏覽器等環境中。如果一個對象附加到某個事件但不再使用,則它不會被垃圾收集,因為該事件仍然保留對其的引用。

中間件和第三方庫

有時造成內存泄漏的原因可能不是因為應用程序代碼,而是在其使用的中間件或第三方庫中。這些組件中的錯誤或低效代碼可能會導致內存泄漏。

內存碎片

內存碎片雖然不是傳統意義上的泄漏,但仍可能會導致內存使用效率低下。隨著時間的推移,內存分配之間的小間隙會累積,從而難以分配更大的內存塊。

孤立線程

產生但未正確終止的線程可能會消耗內存資源。這些孤立線程會隨著時間的推移而積累,尤其是在長時間運行的應用程序中。

緩存過度使用

如果沒有合適的逐出策略,實現緩存機制可能會導致內存無限期地消耗,特別是在緩存不斷無限制增長的情況下,可能會導致內存泄漏。

主要類型

以發生的方式來分類,內存泄漏可以分為以下4種類型:

常發性內存泄漏

常發性內存泄漏指的是每次執行特定操作時都會發生的內存泄漏。這類泄漏通常更容易被發現和修復,因為它們的發生模式一致,通過監測內存使用情況可以較容易地定位到問題源頭。例如,每次調用某個函數時分配內存而忘記釋放,或者循環中創建對象而沒有適當的清理機制則會造成常發性內存泄漏。

偶發性內存泄漏

偶發性內存泄漏是指發生內存泄漏的代碼只有在某些特定環境下或操作過程中才會發生。例如在funB()函數中,如果funB()函數只有在特定環境下才返回True,那么pB指向的HBITMAP對象并不總是發生泄漏。常發性和偶發性是相對的,對于特定的環境,偶發性的也許就變成了常發性的。

一次性內存泄漏

一次性內存泄漏是指發生內存泄漏的代碼只會被執行一次,或者由于算法上的缺陷,導致總會有一塊且僅一塊內存發生泄漏。從用戶使用程序的角度來看,內存泄漏本身不會產生什么危害,作為一般的用戶,根本感覺不到內存泄漏的存在。真正有危害的是內存泄漏的堆積,這會最終消耗盡系統所有的內存。從這個角度來說,一次性內存泄漏并沒有什么危害,因為它不會堆積。比如,在類的構造數中分配內存,在析構函數中卻沒有釋放該內存,但是因為這個類是一個Singleton,所以內存泄漏只會發生一次。

隱式內存泄漏

程序在運行過程中不停地分配內存,但是直到結束的時候才釋放內存,哪怕最終程序釋放了所有申請的內存,但是對于一個服務器程序,需要運行幾天、幾周甚至幾個月,然而隱式內存泄漏很難被檢測和定位,內存泄漏一旦暴露,可能會造成難以預估的損失。例如對于一個服務器程序,這類泄漏同樣會堆積,造成系統內存資源的耗盡。因此,稱這類內存泄漏為隱式內存泄漏。

危害

內存資源浪費:內存泄漏在軟件程序設計中屬于是缺陷之一,在用戶程序運行中,如果設計中存在缺陷,就會導致在內存應用后程序沒有主動通知操作系統,從而喪失其使用權,那么這一堆內存塊就會無法控制也不能重新應用,致使系統無法釋放不需要的內存空間,也就會出現內存資源浪費。

內存使用量增加:隨著更多內存泄漏且未釋放,整個系統內存使用量會增加,使得可用于其他進程和應用程序的內存減少。

應用程序不穩定:隨著內存使用量隨著時間的推移而增加,存在內存泄漏的應用程序可能會遇到崩潰、意外行為和間歇性故障。這會導致不穩定和可靠性問題。

性能下降:內存泄漏通常不會直接產生可觀察的錯誤癥狀,起初其變化可能相當離散并常被忽視,隨后逐漸積累導致系統整體性能下降。隨后,系統可能由于所謂的“過度分配”或“虛擬內存耗盡”而無法正常工作或大幅減慢。

資源爭用:當系統試圖管理有限的資源時,較高的內存使用量還會導致對緩存和 CPU 時間等資源的更多爭用。這進一步降低了性能。

響應速度降低:當單個進程消耗大量內存時,它往往會占用越來越多的主內存,迫使其他程序移至輔助存儲器,并大大降低系統的響應速度。即使泄漏的程序被終止,其他程序可能需要一些時間才能切換回主內存,性能可能需要一些時間才能恢復到正常水平。

系統崩潰:最終,內存泄漏會使得程序因內存耗盡而崩潰,對于內存受限系統(如嵌入式系統),或程序執行時間較長的系統(如服務器系統)而言,這種問題更加嚴重。例如1992年在英國倫敦發生的救護服務系統崩潰事件,就是因系統中存在內存泄漏,在程序連續運行三周后最終引發了危險事故。隨著計算機內存容量的不斷增大和虛擬內存技術的發展,內存泄漏缺陷越來越難以覺察。

安全風險:內存泄漏導致數據在內存中停留的時間比預期的要長。這些數據可能包含密碼、密鑰或其他敏感信息,如果被惡意軟件或攻擊者訪問,這些信息會帶來安全風險。

檢測方法

通常在內存泄漏檢測過程中,可以將其分成動態檢測方法以及靜態檢測方法兩種。

動態檢測方法

動態檢測方法在應用中,是針對應用程序實施動態內存分配過程中,實現對堆內存的標記。如果程序退出,同時將已經分配內存進行釋放過程中,分析堆上殘留對象,這些對象即為應用程序所泄漏的內存。在這一方法應用中可以對應用中存在的程序缺陷及時發現,然而動態特性要求在程序實際執行過程中需要有較大性能以及時間成本,且在執行路徑覆蓋過程中也存在死角,導致檢測過程中存在有不完備性,存在較高的漏報率。

檢測工具

靜態檢測方法

靜態檢測方法是指對程序源代碼的分析,對于可能會出現的執行路徑均模擬分析,以此實現對程序執行路徑中可能出現安全問題的判定。在此檢測過程中,不需要實際執行程序,可以改善動態分析中存在較大性能開銷問題,但是這一方法在應用中對于程序輸入、環境變量等信息無法實現精確判斷,在執行路徑模擬中可能會出現不可行路徑。

檢測工具

診斷過程

內存分配成功性驗證:在進行內存分配后,應驗證操作是否成功。常規方法包括:

內存的初始化:分配的內存可能包含隨機數據。立即初始化這些內存區域是防止使用未定義數據的關鍵步驟。

邊界控制:處理數據結構如數組時,必須確保所有訪問都在有效邊界內。應通過適當的循環條件和索引檢查來避免越界訪問。

內存需求計算:準確計算所需內存量,以確保為數據結構分配足夠空間,防止溢出。

動態分配內存的釋放:維護對動態分配內存的引用,并在不再需要時適時釋放,以避免內存泄漏。

錯誤處理與資源管理:在出現錯誤時,確保釋放任何已分配的資源,防止內存泄漏。

處理已釋放內存的引用:釋放內存后,應立即更新任何相關指針,以避免懸掛指針或未定義行為。

解決方法與技術

方法

內存監控

內存監控的一個選擇是通過管理虛擬機的影子頁表實現。在虛擬機運行時,內存管理模塊載入影子頁表,試圖完成從GVA到HPA的映射。如果影子頁表中已經存在某個GVA到其HPA的映射,那么這個轉換過程會自動完成;否則,虛擬機會陷入到虛擬機管理器中,由后者完善GVA到HPA的映射。但是影子頁表可能會產生額外的內存訪問延遲,從而導致嚴重的性能損失?;谟白禹摫淼倪@種特征,通過如下步驟即可完成對內存的監控:

首先,計算出內存片斷所跨越的所有頁面,例如在頁面大小為4KB的虛擬機中,首地址為0x80a8400,長度為10KB的內存片斷,跨越了3個頁面;其中占第1個和第3個頁面的部分,完全占用第2個頁面。隨后,在影子頁表中消除對這些頁面的GVA到HPA的映射關系。由于這些頁面的映射關系被消除,只要虛擬機試圖訪問該內存片斷,都會導致虛擬機的陷入;最后,在虛擬機陷入時,分析陷入指令所訪問的內存地址是否屬于該內存片斷。

此外,英特爾 VT的VMX架構通過引入擴展頁表(Extended Page Tabe,EPT)機制實現對物理內存的訪問控制。EPT是Intel在VT-x技術基礎上增加的一種硬件輔助內存虛擬化技術。在處理器端,VMX架構通過引入EPT機制來實現VM物理地址空間的隔離。當客戶機通過指令訪問內存時,首先,客戶機操作系統通過分頁機制將線性地址(linear address)轉換為客戶機物理地址GPA(Guest-Physical Address),然后,通過定義在VMM中EPT頁表將GPA轉換為主機物理地址HPA(Host-PhysicalAddress),從而訪問真正的物理地址。

清理棧

異常處理

在編程中,異常處理是一種結構化的方法,用于處理程序運行時可能遇到的錯誤和異常情況。它主要依賴于幾個核心概念:拋出異常、捕獲異常、以及確保資源被適當管理。雖然具體的實現細節可能因編程語言的不同而有所差異,但基本原理是通用的。以Java為例,異常處理的方法有兩種:一是通過throws和throw拋出異常,二是使用try-catch-finally結構對異常進行捕獲和處理。異常處理也稱捕捉異常。異常處理用到5個關鍵字:try、catch、fnally、throw、throws。

拋出異常

程序在執行過程中,當遇到錯誤或異常情況時,會創建并拋出一個異常對象。這個異常對象包含了異常的類型和發生時的狀態信息。如果當前作用域無法處理該異常,它將被傳遞到調用棧的上一層,直至找到適當的異常處理代碼進行處理。

捕獲異常

為了防止異常導致程序崩潰,編程語言提供了結構化的異常捕獲機制,如try-catch-finally結構(或等效的語言結構),允許開發者有效地捕獲并處理異常。

Try:try是為了保證出現異常后程序不至于崩潰,可以正常運行。當函數內部出現try語句,就需要寫另一個語句保護,直到所有的try語句將出現的異常處理完。就不再生成新的try語句。通常情況下,try塊不應該過大,過大的try塊可能導致代碼難以理解和維護。為了使代碼更加清晰和易于理解,應該對try塊進行細分和拆分,一遍處理出現異常情況的代碼塊,同時也方便后續代碼的迭代和維護。

catch:捕捉try語句塊內發生異常。此外,在捕獲異常時,應該使用最具體的異常類型來捕獲異常,而不是使用異?;悂聿东@所有類型的異常。其次,處理異常時提供有意義的錯誤消息。除了捕獲和處理異常外,還應該為捕獲的異常提供有意義的錯誤消息。

Finally:不管出現任何情況都會被執行。如果try中所有語句被執行完畢,則進人finally階段。如果finally階段沒有異常,則整個try-catch-finally完成。如果finally階段出現異常,那么將重新被catch捕獲,return會try語句塊內,重新執行直至不再生成新的try語句,完成語句的執行,直接終止。

技術

垃圾回收

GC,全稱Garbage Collection,即垃圾回收,是一種自動內存管理的機制。當程序向操作系統申請的內存不再需要時,垃圾回收主動將其回收并供其他代碼進行內存申請時候復用,或者將其歸還給操作系統,這種針對內存級別資源的自動回收過程,即為垃圾回收。而負責垃圾回收的程序組件,即為垃圾回收器。通常,垃圾回收器的執行過程被劃分為兩個半獨立的組件:

(1)賦值器(Mutator):這一名稱本質上是在指代用戶態的代碼。因為對垃圾回收器而言,用戶態的代碼僅只修改對象之間的引用關系,即在對象圖(對象之間引用關系的一個有向圖)上進行操作;

(2)回收器(Collector):負責執行垃圾回收的代碼。

垃圾回收的算法

垃圾回收主要依賴以下兩種算法來識別不再需要的內存:

垃圾回收的限制

盡管垃圾回收提高了內存管理的效率,但在特定環境下,它也面臨一些限制:

智能指針

為了簡化動態內存管理,C++11引入了標準的智能指針解決方案,通過頭文件中的模板庫提供。智能指針采用代理設計模式,自動管理動態內存的生命周期?;舅枷胧菍ew操作返回的指針封裝到一個局部對象中,這個對象成為動態內存的所有者。當所有者對象離開作用域被銷毀時,其析構函數會自動釋放動態內存。

C++11的智能指針類型
代碼示例

“unique_ptr”是“獨占式智能指針”。使用它管理前面的O類指針:

例中p是一個智能指針。其中的“”指明它所指向的數據類型是“O”。除了創建方法不太一樣,以及不用手工釋放之外,智能指針使用上和它所管理的裸指針基本一樣。

“std::shared_ptr”是“共享式智能指針”。shared_ptr可以被復制很多次,并且指針指向的對象直到最后一個shared_ptr被銷毀,都會保持有效。

在該示例中,如果有一個指向節點的shared_ptr實例,就可以確保節點存在,但是當刪除共享指針后,并不關心節點的存在:它可能被刪除;如果有另一個節點連接到它,則也可能被保留。

虛擬存儲技術

Storage Virtualization(譯為:虛擬存儲)是利用硬、軟件技術,將分別獨立存儲的、種類各異的物理存儲體集合成一個可以供網絡用戶共同使用的綜合邏輯虛擬存儲空間。這個虛擬存儲空間的容量等于存儲共享信息的所有物理存儲體的容量之和,虛擬存儲空間的訪問帶寬也是約等于存儲共享信息的所有物理存儲體訪問帶寬的總和。

虛擬存儲器不同于一般物理存儲體,它是一種利用邏輯方法對物理存儲體進行編輯、并將處理后的圖像向用戶顯示的邏輯存儲器。所以,用戶在查閱共享信息時,使用的是虛擬設備而不是實物,這樣能夠合理使用存儲空間,并使利用率最大化、合理化。

具體案例

案例1

假設在Client從Server端斷開后,Server并沒有呼叫Disconnected()函數,那么代表那次連接的Connection對象就不會被及時地刪除,在Server程序退出的時候,所有Connection對象會在ConnectionManager的析構函數里被刪除。當不斷地有連接建立或斷開時,隱式內存泄漏就發生了。

案例2

在這個例子里,如果函數做了某些導致異常的操作,異常處理函數就不會釋放pObject,從而導致了內存泄漏。

參考資料 >

How to Identify Memory Leaks.atatus.2024-02-05

What is a Memory Leak?.codereliant.2024-02-29

What Are Memory Leaks and How To Detect Them?.codete.2024-02-05

生活家百科家居網