一、先了解下必備的知識前提
內存中的托管與非托管,可簡單理解為:
托管:可借助GC從內存中釋放的數據對象(以下要描述的內容點)
非托管:必須手工借助Dispose釋放資源(實現自IDisposable)的對象
內存中有棧和堆的概念區分,僅簡單說明:
棧:小型的,當前運行函數、值類型及指針等(這里不再詳細闡述)
堆:存放數據對象實例的內存空間,GC清理的區域(以下要描述的內容點)
作者:[Sol·wang] - 博客園,原文出處:https://www.cnblogs.com/Sol-wang/p/14792628.html
二、.Net GC的簡單描述
GC垃圾回收是對於內存堆的處理過程。
當一個應用程序進程創建時,會為此應用程序在物理內存堆中分配一塊虛擬的連續性內存空間,以供應用程序后續運行時存放產生的數據對象實例。
GC是一個獨立的進程,用來自動維護管理內存堆中的空間分配和釋放。它通過一個或多個線程進行垃圾回收,默認啟用后台線程垃圾回收。
(關於前台線程與后台線程,可參考其它)
三、.Net平台的GC垃圾回收,什么時候會被觸發呢?
1、當被分配的堆中虛擬內存空間不夠用時,系統會自動 壓縮/回收/擴大 被分配的虛擬內存塊,以適應新產生的數據對象存儲。
2、當整個物理內存不夠用時,系統會自動 壓縮/回收 各個進程占用的內存空間,以適應新產生的數據對象存儲。
3、當應用程序中手動觸發GC回收時,GC按照手動指定的方式進行垃圾回收。
四、從作用域上 去理解堆中的代
先這樣去理解吧
假設一個實例變量聲明時的作用域較大,那它就不會馬上被回收,因為作用域大的因素,有可能后續程序還會經常用到。
假設一個實例變量聲明時的作用域較小,那它就有可能被優先回收,因為生存周期較短,過了作用域范圍,此變量不會再被使用。
假設一個靜態的或全局的作用域變量,那它通常不會被回收,因為這樣的全局聲明會在任意代碼段長期被使用。
所以,為了更好的回收,結合上面的三個假設案例,堆中將各數據對象實例歸納為:0代、1代、2代
0代:存放臨時或最新創建的數據對象實例。最常被回收的對象實例。
1代:存放一段時間內再次使用的數據對象實例,生命周期較長的數據對象實例。較少被回收的對象實例。
2代:存放常住內存的對象實例,如:靜態類型,全局作用域等的對象實例。通常為應用程序退出后回收。
五、堆中對象 在代之間的轉移:幸存者的提升
應用程序持續運行中,
新創建的對象首先被放在0代中,當運行一段時間后,有些變量超出了自己所在的作用域,不會再被使用,會被GC清理;
由於有些變量作用域大,當前還未超出自己所在的作用域,接下來可能還會被使用,所以GC不會清理;
0代中,有些數據對象實例會被GC清理,有些數據實例對象未被GC清理,那么,未被GC清理的數據對象實例,我們稱它為幸存者。
此時,0代中的幸存者會被轉移到1代中(想想上面提到1代存放的是哪類對象實例...);
那么,以此類推,長期/處處被使用的對象實例,就會從1代中轉移到2代中;
因此,2代中存放的通常為靜態或全局作用域或長期被使用到的對象實例。
幸存和提升:
垃圾回收中未回收的對象也稱為幸存者,並會被提升到下一代:
- 第 0 代垃圾回收中未被回收的對象將會升級至第 1 代。
- 第 1 代垃圾回收中未被回收的對象將會升級至第 2 代。
- 第 2 代垃圾回收中未被回收的對象將仍保留在第 2 代。
六、GC是如何去確定要清理的對象實例?
GC在堆中生成各對象間的結構圖,作為回收對象的依據,找出非活動的對象。
所有數據對象實例之間的關聯引用關系,都會生成一個完整的結構圖,一些不在結構圖中的 或超出所在作用域的 或不再被繼續使用的對象實例,被稱為非活動對象。被視為GC要清理的對象。
准確的說,依據:
- 堆棧根
- 垃圾回收句柄
- 靜態數據
七、手動GC垃圾回收
在某些不常見的情況下,強制回收可提高應用程序的性能。在此,可使用 GC.Collect
方法強制執行垃圾回收,從而誘導垃圾回收。
注意,是誘導,而不是即刻回收。
為了考慮到應用程序當前的穩定運行,執行GC.Collect
並不一定及時產生效果,它需要一個過程,這里僅僅是一個觸發,會去收集將要回收的對象,回收動作會在未來某個合適的時間段進行。(當然,也可以強制阻塞式回收,這里略過)
(思考一下:無用的實例=null,造成堆中無主的廢數據,再執行GC.Collect()后的效果?)
關於 GC.Collect 方法的參數,會用到上面提到的概念及場景:
- 對指定的代進行回收
- 指定回收次數
- 強制回收 或 擇機回收
- 阻塞式回收 或 后台線程回收
- 壓縮 或 回收
(阻塞式回收方式:都先停一停,先讓我回收完)
當然,通常建議:0代,擇機,后台回收(阻塞式風險太大,通常選擇擇機方式,具體自我考量)
至此,關於GC垃圾回收的敘述,基本已結束,下面是關於優化層面的內容。
八、內存堆中的弱引用
當應用程序正在執行使用的對象,GC是不可能回收的,那么,就認為應用程序對該對象具有強引用。
強引用:應用程序正在使用的對象實例,不能被GC回收。
弱引用:應用程序暫時沒使用的對象實例,暫時可被GC定義為可回收的實例,在回收之前,也可被應用程序再次使用后變為強引用。
假設一個對象實例被GC清理后,后續又被再次用到的場景,就會重新創建對象實例;那如果這個對象實例又比較大,創建過程又比較繁瑣耗時,這樣的頻繁創建... ...
當然還有優化的空間,所以,弱引用優化了以上場景。
弱引用的優點:對於頻繁創建的大實例,弱引用可以做到一次創建多次使用,避免大對象實例多次創建的性能消耗。
(對於小對象使用弱引用,所帶來的對對象管理上的性能消耗,是否值得?)
若要對某對象建立弱引用,使用要跟蹤的對象實例創建 WeakReference
。 然后將 Target 屬性設置為該對象,將該對象的原始引用設置為 null
。
(參考官方文檔)
也就是說:我們可以自定義控制哪些對象實例要不要暫時不被GC垃圾回收。
九、多應用共享內存時的垃圾回收
當多個應用程序在一台主機同時運行時,對內存空間大小的分配,建議是靈活可變的,以達到各應用程序對內存利用的平衡及穩定性。
假設一台主機,其中部署着 應用A、應用B、應用C。應用A 占用內存空間為 20%;應用B 占用內存空間為 20%;應用C 占用內存空間為 30%;(剩余內存空間為系統本身所需占用)
一個場景:應用A 突然有一個顯著的並發量的提升,原本分配給應用A的內存空間已經無法滿足當前的應用負載開銷,需要繼續擴大內存空間;此時的應用B、應用C 處於比較空間的狀態,對於內存的空間使用比較小;那么 系統可以自動根據實際狀況來調整各應用在內存空間中的占比,壓縮應用A與應用B的使用空間,之后產生的空閑空間再分配給應用A,以使得應用A能處理更多的並發量。
正如以下操作,如果啟用 gcTrimCommitOnLowMemory 設置,垃圾回收器會計算系統內存負載,並在負載達到 90% 時進入修整模式。除非負載下降到不到 85%,否則會一直處於修整模式。
如果條件允許,垃圾回收器可以決定 gcTrimCommitOnLowMemory 設置對當前應用沒有幫助並忽略它。
如下啟用 gcTrimCommitOnLowMemory 的設置:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<runtime>
<gcTrimCommitOnLowMemory enabled="true"/>
</runtime>
</configuration>