有了 GC 還會不會發生記憶體洩漏?
問題的發現
這個問題是我在寫C++時考慮到的,C++需要手動管理記憶體,雖然現在標準庫中提供了一些智慧指標,可以實現基於引用計數的自動記憶體管理,但現實環境是很複雜的,我們仍要注意迴圈引用的問題。還有一個容易被忽視的問題就是物件間關係的“佔有”和“非佔有”,這個問題其實在具有GC的C#和Java中也一樣存在。
目前.NET和Java的GC策略都屬於Tracing garbage collection
,基本原理是從一系列的root開始,沿著引用鏈進行遍歷,對遍歷過的物件進行標記(mark),表示其“可達(reachable)”,然後回收那些沒有標記的,即“不可達”物件所佔用的記憶體。如果你的程式碼中明明有的物件已經沒用了,但在某些地方仍然保持有對它的引用,就會造成這個物件長期處於“可達”狀態,以至其佔用的記憶體無法被及時回收。
物件關係的問題
佔有 與 非佔有
好吧,這兩個詞是我自己發明的。這兩個詞是針對“擁有”而言的,佔有 是表示強的擁有,宿主物件會影響被擁有物件的生命週期,宿主物件不死,被擁有的物件就不會死;非佔有 表示弱的擁有,宿主物件不影響被擁有物件的生命週期。
在處理物件間關係時,如果應該是非佔有關係,但卻實現成了佔有關係,則佔有關係就會妨礙GC對被佔有物件的回收,輕則造成記憶體回收的不及時,重則造成記憶體無法被回收。這裡我用C#實現觀察者模式作為示例:
public interface IPublisher { void Subscribe(ISubscriber sub); void UnSubscribe(ISubscriber sub); void Notify(); } public interface ISubscriber { void OnNotify(); } public class Subscriber : ISubscriber { public String Name { get; set; } public void OnNotify() { Console.WriteLine($"{this.Name} 收到通知"); } } public class Publisher : IPublisher { private List<ISubscriber> _subscribers = new List<ISubscriber>(); public void Notify() { foreach (var s in this._subscribers) s.OnNotify(); } public void Subscribe(ISubscriber sub) { this._subscribers.Add(sub); } public void UnSubscribe(ISubscriber sub) { this._subscribers.Remove(sub); } } class Program { static void Main(string[] args) { IPublisher pub = new Publisher(); AttachSubscribers(pub); pub.Notify(); GC.Collect(); Console.WriteLine("垃圾回收結束"); pub.Notify(); Console.ReadKey(); } static void AttachSubscribers(IPublisher pub) { var sub1 = new Subscriber { Name = "訂閱者 甲" }; var sub2 = new Subscriber { Name = "訂閱者 乙" }; pub.Subscribe(sub1); pub.Subscribe(sub2); // 這裡其實賦不賦null都一樣,只是為了突出效果 sub1 = null; sub2 = null; } }
這段程式碼有什麼問題嗎?
在AttachSubscribers方法裡,建立了兩個訂閱者,並進行了訂閱,這裡的兩個訂閱者都是在區域性建立的,也並沒有打算在外部引用它們,它們應該在不久的某個時刻被回收了,但是由於同時它們又存在於釋出者的訂閱者列表裡,釋出者“佔有”了訂閱者,雖然它們都沒用了,但暫時不會被銷燬,如果釋出者一直活著,則這些沒用的訂閱者也一直得不到回收,那為什麼不呼叫UnSubscribe呢?因為在實際中情況可能很複雜,有些時候UnSubscribe呼叫的時機會很難確定,而且釋出者的任務在於登記和通知訂閱者,不應該因此而“佔有”它們,不應干涉它們的死活,所以對於這種情況,可以使用“弱引用”實現“非佔用”。
弱引用
弱引用是一種包裝型別,用於間接訪問被包裝的物件,而又不會產生對此物件的實際引用。所以就不會妨礙被包裝的物件的回收。
給上面的例子加入弱引用:
class Program { static void Main(string[] args) { IPublisher pub = new Publisher(); AttachSubscribers(pub); pub.Notify(); GC.Collect(); Console.WriteLine("垃圾回收結束"); pub.Notify(); Console.WriteLine("============================================="); pub = new WeakPublisher(); AttachSubscribers(pub); pub.Notify(); GC.Collect(); Console.WriteLine("垃圾回收結束"); pub.Notify(); Console.ReadKey(); } static void AttachSubscribers(IPublisher pub) { var sub1 = new Subscriber { Name = "訂閱者 甲" }; var sub2 = new Subscriber { Name = "訂閱者 乙" }; pub.Subscribe(sub1); pub.Subscribe(sub2); // 這裡其實賦不賦null都一樣,只是為了突出效果 sub1 = null; sub2 = null; } } public interface IPublisher { void Subscribe(ISubscriber sub); void UnSubscribe(ISubscriber sub); void Notify(); } public interface ISubscriber { void OnNotify(); } public class Subscriber : ISubscriber { public String Name { get; set; } public void OnNotify() { Console.WriteLine($"{this.Name} 收到通知"); } } public class Publisher : IPublisher { private List<ISubscriber> _subscribers = new List<ISubscriber>(); public void Notify() { foreach (var s in this._subscribers) s.OnNotify(); } public void Subscribe(ISubscriber sub) { this._subscribers.Add(sub); } public void UnSubscribe(ISubscriber sub) { this._subscribers.Remove(sub); } } public class WeakPublisher : IPublisher { private List<WeakReference<ISubscriber>> _subscribers = new List<WeakReference<ISubscriber>>(); public void Notify() { for (var i = 0; i < this._subscribers.Count();) { ISubscriber s; if (this._subscribers[i].TryGetTarget(out s)) { s.OnNotify(); ++i; } else this._subscribers.RemoveAt(i); } } public void Subscribe(ISubscriber sub) { this._subscribers.Add(new WeakReference<ISubscriber>(sub)); } public void UnSubscribe(ISubscriber sub) { for (var i = 0; i < this._subscribers.Count(); ++i) { ISubscriber s; if (this._subscribers[i].TryGetTarget(out s) && Object.ReferenceEquals(s, sub)) { this._subscribers.RemoveAt(i); return; } } } }
其實弱引用也不是完美的解決方案,因為限制了API使用者的自由,當然這裡也沒打算實現一個通用的、完美的解決辦法,只是想通過個例子讓你知道,即使是在有GC的情況下,不注意程式碼設計的話,仍有可能會發生記憶體洩漏的問題。
非託管資源
GC不能釋放非託管資源嗎?
GC的作用在於清理託管物件,託管物件是可以定義析構方法(準確點說應該叫finalizer,C#中的~類名
,Java中的finalize)的,這個方法會在託管物件被GC回收前被呼叫,析構方法裡完全可以釋放非託管資源(實際上很多託管物件的實現也都這麼做了),也就是說GC是可以釋放非託管資源的。
但是GC的執行時間是不確定的,現在計算機的記憶體也都足夠大,記憶體遲點回收不會有什麼問題,但託管物件內部包裝的其它資源可能屬於“緊張的資源”,比如非託管記憶體、檔案控制程式碼、socket連線,這些資源是必須要被及時回收的,比如檔案控制程式碼不及時釋放會導致該檔案一直被佔用,影響其它程式對該檔案的讀寫、socket連線不及時釋放會導致埠號一直被佔用,那如何保證釋放的及時呢?
Dispose模式
方法很簡單,就是在物件中用一個方法來專門釋放這些非託管資源,比如叫close
, dispose
, free
, release
之類的,然後顯式呼叫這些方法。C#中的IDisposable介面和Java中的Closeable介面就是這個作用,因為大多數帶GC的語言都使用這種設計,所以這也算是一種模式。
虛擬碼示例:
File f = new File("data.txt"); f.writeBytes((new String("Hello, world!")).getBytes("ascii")); f.close();
這樣就夠了嗎?如果close前發生異常或直接return了怎麼辦? – finally語句塊
finally語句塊保證了其中的語句一定會被執行,配合close方法,就能確保非託管資源的及時釋放。(注:不呼叫close其實一般來講非託管資源也是會被釋放的,只是這種釋放不夠“及時”,因為要等到託管物件被回收)
C++中沒有finally語句結構,這並不奇怪,因為C++有RAII機制,物件的銷燬是確定的,而且確保解構函式的呼叫,所以不需要finally這種語法。
結語
其實以上所列舉的種種情況,大多數情況資源最終都會得到回收,只是回收不夠及時,但這種回收不及時在資源緊張或出現極端情況時,還是有可能會發生記憶體洩漏的,所以說不是有了GC就可以高枕無憂了。
相關文章
- ThreadLocal真會記憶體洩漏?thread記憶體
- 分析記憶體洩漏和goroutine洩漏記憶體Go
- js記憶體洩漏JS記憶體
- Android記憶體洩漏Android記憶體
- Android 記憶體洩漏Android記憶體
- jvm 記憶體洩漏JVM記憶體
- Java記憶體洩漏Java記憶體
- 造成記憶體洩漏的操作有哪些?記憶體
- 記憶體洩漏的原因記憶體
- valgrind 記憶體洩漏分析記憶體
- 【記憶體洩漏和記憶體溢位】JavaScript之深入淺出理解記憶體洩漏和記憶體溢位記憶體溢位JavaScript
- JVM——記憶體洩漏與記憶體溢位JVM記憶體溢位
- iOS檢測記憶體洩漏iOS記憶體
- Android記憶體洩漏場景Android記憶體
- ThreadLocal記憶體洩漏問題thread記憶體
- PHP 記憶體洩漏分析定位PHP記憶體
- 記憶體洩漏除錯工具記憶體除錯
- WebView引起的記憶體洩漏WebView記憶體
- Perfdog 玩轉記憶體洩漏記憶體
- JavaScript之記憶體洩漏【四】JavaScript記憶體
- .Net程式記憶體洩漏解析記憶體
- 記憶體的分配與釋放,記憶體洩漏記憶體
- 1.記憶體優化(一)記憶體洩漏記憶體優化
- JavaScript之記憶體溢位和記憶體洩漏JavaScript記憶體溢位
- 納尼,Java 存在記憶體洩洩洩洩洩洩漏嗎?Java記憶體
- hashCode竟然不是根據物件記憶體地址生成的?還對記憶體洩漏與偏向鎖有影響?物件記憶體
- 騰訊一面:記憶體滿了,會發生什麼?記憶體
- Bulk 異常引發的 Elasticsearch 記憶體洩漏Elasticsearch記憶體
- Java棧溢位|記憶體洩漏|記憶體溢位Java記憶體溢位
- [Java基礎]記憶體洩漏和記憶體溢位Java記憶體溢位
- 解決記憶體洩漏(1)-ApacheKylin InternalThreadLocalMap洩漏問題分析記憶體Apachethread
- Andriod專案記憶體洩漏流程記憶體
- Java記憶體洩漏解決之道Java記憶體
- Android備忘錄《記憶體洩漏》Android記憶體
- 小心遞迴中記憶體洩漏遞迴記憶體
- vue使用中的記憶體洩漏Vue記憶體
- Android中的記憶體洩漏模式Android記憶體模式
- [譯] Swift 中的記憶體洩漏Swift記憶體
- linux程式之記憶體洩漏分析Linux記憶體