構建高效能ASP.NET網站 第七章 如何解決記憶體的問題(前篇)—託管資源最佳化—記憶體回收機制剖析
前言:本章主要詳細的講述如何因記憶體問題而導致的效能問題,很多的時候都是深入.NET核心進行分析,然後給出解決方案,同時,本系列的其他文章,也爭取做到:深入淺出。
本篇是為後面的做個鋪墊,而且比較的精彩。只有真正的理解了本篇,後面才可以順利的走下去。
本篇的議題如下:
記憶體問題概述(前篇)
託管資源最佳化(前篇)
對象的生命週期(前篇)
對象的”代“(前篇)
大對象堆(LOH) (前篇)
CLR計數器的使用 (前篇)
CLR Profiler的使用(中篇)
記憶體回收行程的不同版本(中篇)
對象使用注意事項(中篇)
常用最佳化措施(後篇)
非託管資源最佳化
Session會話的最佳化
系列文章連結:
構建高效能ASP.NET網站 開篇
構建高效能ASP.NET網站之一 剖析頁面的處理過程(前端)
構建高效能ASP.NET網站之二 最佳化HTTP請求(前端)
構建高效能ASP.NET網站之三 細節決定成敗
構建高效能ASP.NET網站 第五章—效能調優綜述(前篇)
大型高效能ASP.NET系統架構設計
構建高效能ASP.NET網站 第五章—效能調優綜述(中篇)
構建高效能ASP.NET網站 第五章—效能調優綜述(後篇)
構建高效能ASP.NET網站 第六章—效能瓶頸診斷與初步調優(上篇)—識別效能瓶頸
構建高效能ASP.NET網站 第六章—效能瓶頸診斷與初步調優(下前篇)—簡單的最佳化措施
構建高效能ASP.NET網站 第六章—效能瓶頸診斷與初步調優(下後篇)—減少不必要的請求
構建高效能ASP.NET網站 第七章 如何解決記憶體的問題(前篇)—託管資源最佳化—記憶體回收機制深度剖析
構建高效能ASP.NET網站 第七章 如何解決記憶體的問題(前中篇)—託管資源最佳化—監測CLR效能
記憶體問題概述
和CPU一樣,記憶體也是一個直接影響服務端效能的重要的硬體資源。
一般來說,如果服務端記憶體不足,從導致以下兩個問題產生:
1. 導致服務端把一些原本要寫到記憶體中的資料,寫到硬碟上面。這樣不僅僅加大了CPU和磁碟的I/O操作,同時也延長了讀取這些資料的時間。
2. 阻止了一些緩衝策略的使用。
對於記憶體不足,一直最快最直接的方式就是去買記憶體條加在伺服器上面。但是這樣存在一個隱患的問題就是:如果加了新的記憶體之後,服務端又面臨記憶體不足的問題,我們不可能無止境的加記憶體條,那麼我們就必須從網站本身來解決這個問題,例如從服務端的配置,對網站的代碼進行分析,最佳化。
託管資源最佳化
對於託管資源,相信大家並不陌生了,簡單的說就是:在C#的託管堆上面建立的資源,或者說通過new產生的對象。
在深入講解之前,我們首先來看看”對象的生命週期”
對象的生命週期
當我們用new關鍵字建立了一個對象的時候,這個對象就被分配到CRL託管堆上面。這個託管堆是在記憶體中的。而且這個指派至空間的速度是非常的快的,因為每次都是在託管堆的最後面划出一定的空間來給這個對象,不用去堆上面需找合適大小的空間。
如果當託管堆準備為一個對象分配空間的時候,發現託管堆上面的空間太小了,不足以分配給這個新的對象,那麼CLR就開始運行記憶體回收機制了。我們知道:記憶體回收機制會把那些在託管堆上面沒有了引用指向的那些對象都清理掉,同時也會把託管堆上面現存的對象進行壓縮。
但是有一點需要清楚:如果此時進行了記憶體回收的時候,清除了一些沒有用的對象,但是只有在下一次來回收進行的時候,上次記憶體回收清除的對象才真正的從記憶體中消除(此時,還有一些“對象複蘇“等話題就不在贅述)。
下面就來講述一些記憶體回收的話題。
對象的”代“
在CLR進行記憶體回收的時候,記憶體回收行程回去託管堆上面去檢查對象是否可以被回收,這個檢查過程是非常消耗資源的。為了避免每次記憶體回收都要便利託管堆上面的所有對象,CLR給把託管堆上面的對象用”代”來劃分,例如,第一代,第二代。然後每次便利掃描託管堆的時候,就去掃描某一個”代”中的對象,這樣效能就好點。
在託管堆上面,可以把對象分為三個”代”:0代,1代,2代,僅此這三個代。每個對象都是從0代開始的。一個對象每經曆一次記憶體回收,並且這個對象還在使用中,那麼這個對象的“代“就會增加1代。例如,如果在0代的對象,經曆了一次記憶體回收之後,他的代就是1代,如果是1代的對象,最後就會變為2代。如果對象本身已經是2代了,不管經曆多少次記憶體回收(如果對象一直在使用),那麼這個對象還是2代。
在CLR記憶體回收中有句話要記得:” ’代’數越大,被回收的可能性就越小”。而且一些效能最佳化就是根據這個進行的。
每次CLR在進行記憶體回收的時候,都會優先的去掃描第0代的對象,所以,一些新的,臨時使用的對象可以被立刻的清除。相比而言,記憶體回收行程掃描第1代對象的頻率就沒有第0代強,掃描第2代對象的頻率就更低了。所以說:對象存活的時間越長,就越難被回收,而且一直佔據CLR的記憶體資源。
還有有點需要注意的就是:如果CLR決定要掃描了第1代了,同時也用掃描第0代的對象,同時如果,CLR掃描第2代對象,那麼第0代,第1代對象都會被掃描。
所以,從這裡可以得出:我們盡量避免把原本需要立刻回收的的對象變為長期存活的對象。通俗點說就是:如果一個對象本來已經存活在0代的,然後用完就回收的,我們不要讓這個對象一直存活到第1代,甚至第2代。在編程上面基本就是這樣的實現思路:儘可能晚的執行個體化對象,儘可能早的釋放對象。
大對象堆(Large Objecet Heap)
我們之前講述了”堆”的一些話題,CLR除了上面的一般的堆(一般的new對象分配空間的那個堆),CLR中還存在另外的一個堆:專門用來放置那些大於了85k的對象的堆,大對象堆。
如果new一個對象的時候,這個對象的大小超過了85k,那麼CLR就會把這個對象放在LOH上面。如果此時LOH的空間不足了,那麼CLR就會啟動記憶體回收行程去掃描LOH堆和那個一般堆上面的第2代對象,我們之前說過,如果掃描第2代對象,就同時掃描第1代,第0代,那麼實際相當於掃描了整個託管堆,效能影響可想而知。
而且不想之前那個一般堆,在LOH上面的對象被記憶體回收行程回收之後,上面的大對象是不會被壓縮的,那麼LOH這個堆上面就可能存在一些”空間片段”,然後分配新的大對象的時候,就要找空間,甚至進行片段的整理,大家可以聯想一下我們電腦的磁碟磁碟重組。
OK,今天就講到這裡,理論有點多,但是都是基本要清楚和掌握的,希望多多理解。