D語言中的垃圾收集機制

垃圾收集

D 是一種全面採用垃圾收集的語言。這意味着它從來不用釋放內存。只需要按需分配,然後由垃圾收集程序週期性的將所有未使用的內存返回給可用內存池。

C 和 C++ 程序員習慣於顯式的管理內存分配和釋放,很可能會懷疑垃圾收集的好處和功效。對一開始就採用垃圾收集設計的新項目和用垃圾收集改良的現有項目的經驗表明:

  • 採用垃圾收集的程序更快。這有些違反直覺,但是其原理有:
    • 引用計數是解決顯式內存分配問題的常用解決方案。實現賦值時遞增和遞減操作的代碼通常是程序緩慢的原因之一。將其隱藏在智能指針類之後並不能提高速度。 (無論如何,引用計數也不是全面的解決方案,因爲循環引用從不會被刪除。)
    • 析構函數用來釋放對象獲得的資源。對於大多數類來說,這個資源就是內存。採用垃圾收集,大多數析構函數都可以完全拋棄。
    • 當對象在堆棧上分配時,那些負責釋放內存的析構函數就變得十分重要。對於這些函數來說,必須確立一種機制以使異常發生時,每一個函數幀中的析構函數都會被調用以釋放對象持有的內存。如果析構函數變得不相關,就沒有設置特殊的堆棧幀用於處理異常,這樣運行也會更快。
    • 內存管理代碼總量會變少。程序越大,它在cache中的部分的越少;它的調頁越多,它運行的越慢。
    • 垃圾收集只會在內存變得緊張時纔會運行。當內存尚且寬裕時,程序將全速運行,不會在釋放內存上花費任何時間。
    • 相對於過去的緩慢的垃圾收集程序,現代的垃圾收集程序要先進得多。分代,複製收集程序在很大程度上克服了早期的標記&清除算法的低效。
    • 現代垃圾收集程序進行堆緊縮。堆緊縮將減少程序引用的頁的數量,這意味着內存訪問命中率將更高,交換將更少。
    • 採用垃圾收集的程序不會因爲內存泄漏的累積而崩潰。
  • 垃圾收集程序回收不被使用的內存,因此不會有“內存泄露”問題。內存泄露會使長期運行的應用程序逐漸耗盡內存直至使系統崩潰。採用 GC 的程序擁有更長期的穩定性。
  • 採用垃圾收集的程序 有更少的難以發現的指針bug。這是因爲沒有指向已經釋放的內存的懸掛指針。因爲沒有顯式的內存管理代碼,也就不可能有相應的bug。
  • 採用垃圾收集的程序的開發和調試更快,因爲不用開發、調試、測試或維護顯式的釋放代碼。
  • 採用垃圾收集的程序會明顯更小,因爲沒用用於管理內存釋放的代碼,也不需要處理內存釋放異常的代碼。
垃圾收集並不是萬金油。它有以下缺點:
  • 內存收集何時運行是不可預測的,所以程序可能在任何位置暫停。
  • 運行內存收集的時間是沒有上界的。儘管在實踐中它的運行通常很快,但無法保證這一點。
  • 除了收集程序以外的所有線程在回收進行時都會停止運行。
  • 垃圾收集程序也許會留下一些本該回收的內存。在實踐中,這不是什麼大問題,因爲顯式內存回收程序通常會泄露一些內存,這致使它們最終耗盡所有內存,另一個理由就是顯式內存回收程序通常會把內存放回自己的內部內存池中而不是把內存交還給操作系統。
  • 垃圾收集應該被實現爲一個基本的操作系統內核服務。但是因爲現實並非如此,就造成了採用垃圾收集的程序被迫帶着它們的垃圾收集實現到處跑。儘管這個實現可以被做成一個共享 DLL ,它也還是程序的一部分。
這些限制可以通過採用 內存管理 中介紹的技術來緩解。

垃圾收集如何工作

尚未完成。

外部代碼同垃圾收集對象的協作

垃圾收集程序在靜態數據段和每個線程的堆棧和寄存器中尋找根。如果對象唯一的根不在它們中,收集程序將會釋放它佔有的內存。

如果要避免這種行爲,需要

  • 爲對象維護一個根,該根要位於收集程序掃描的區域之內。
  • 使用外部代碼的存儲分配程序重新分配一個對象或者使用 C 運行時庫的 malloc/free 。

指針和垃圾收集程序

D 中的指針可以被粗略地分爲兩類:指向垃圾收集內存的指針和其他的指針。後者如由 C 的 malloc() 得到的指針,從 C 庫中得到的指針,指向靜態數據的指針,指向堆棧上對象的指針,等等。對於這些指針,所有 C 中的合法指針的操作都可以應用於其上。

但是,對於垃圾收集指針和引用,對指針的操作就有一些限制了。這些限制是不會造成太大的缺陷,但卻會使垃圾收集程序的設計靈活許多。

未定義行爲:

  • 將指針同其他的值進行異或,如同在 C 中在鏈表中用異或保存指針那種小技巧。不要使用異或技巧交換兩個指針的值。
  • 使用類型轉換和其他的小技巧將指針存儲在非指針變量內。
    	* p;
    	...
    	 x = ()p;	
    	
    垃圾收集程序構建根集時不會掃描非指針類型。

  • 利用特定的指針對齊的特點在低位或者高位存儲 flag :
    	p = (*)(()p | 1);	
    	
  • 將可能指向垃圾收集堆的值轉換爲指針:
    	p = (*)12345678;	
    	
  • 不要將不同於 null 的“魔數”存入指針。

  • 將指針值寫入到磁盤然後再從磁盤讀入到內存。

  • 使用指針值計算散列值。採用複製技術的垃圾收集程序可能會任意地在內存中移動對象,這會使散列值失效。

  • 依賴指針的順序:
    	 (p1 < p2)		
    	    ...
    	
    因此,再一次提醒,垃圾收集程序可能會在內存中移動對象。

  • 給指針加上或者減去一個偏移量是結果指向垃圾收集程序原來爲對象分配的範圍之外。
    	* p = [10];
    	* q = p + 6;		
    	q = p + 11;		
    	q = p - 1;		
    	
可以依賴的行爲:
  • 使用聯合共享指針的存儲空間:
    	 U { * ptr;  value }
    	
    但是,使用這樣的聯合來替代 cast(int) 的結果是未定義的。

  • 如果存在指向垃圾收集對象對象內部的指針,就不必維護指向對象開始處的指針。
    	[] p = [10];
    	[] q = p[3..6];
    	
    	
程序員可以不用指針完成大部分任務。D 提供的各種特徵使得人們在決大多數情況下都不需要顯式地使用指針,這些特徵包括:飲用對象、動態數組和垃圾收集。提供指針的目的是成功地同 C API 銜接並完成一些低級的工作。

與垃圾收集程序協作

垃圾收集程序並不能解決所有的內存釋放問題。例如,如果保留了一個指向一大塊數據的指針,那麼就算它再也用不到了,垃圾收集程序也無法回收這塊空間。爲了解決這個問題,一個不錯的實踐是在不再使用一個對象時將指向它的引用或指針設爲 null 。

這個建議只適用於靜態引用或者嵌入到其他對象內的引用。它對於那些存儲在堆棧上的引用沒什麼意義,因爲收集程序並不掃描棧頂之上的部分,並且新的堆棧幀總會被初始化。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章