在物理内存中观察CLR托管内存及GC行为
雖然看了一些書,還網(wǎng)絡(luò)上的一些博文,不過對CLR托管內(nèi)存細(xì)節(jié)依然比較模糊。而且因為工作原因總會有很多質(zhì)疑,想要親眼看到內(nèi)存里二進(jìn)制數(shù)據(jù)的變化。
所以借助winhex直接查看內(nèi)存以證實書上的描述或更進(jìn)一步揣摩CLR托管內(nèi)存的運作方式,這里寫下來跟大家一起分享(由于自己這方面知識儲備不太充足,下面的好多內(nèi)容也是猜測,肯定有很對錯誤,希望了解的網(wǎng)友可以幫忙指正)
?
測試環(huán)境: windowsXP win10 win7 (dotnet4.0 Releases編譯 ,下文截圖為win7上的運行結(jié)果)
內(nèi)存查看工具: winhex 7.5
?
雖然重點是監(jiān)測二進(jìn)制的內(nèi)存,不過基本的測試代碼還是要有的(測試是直接運行編譯好的exe,沒有使用調(diào)試模式,編譯時要使用Releases,因為debug跟Releases在GC回收時對象是否可達(dá)的判斷是不一樣的)
下面對內(nèi)存的查找部分看起來可能有點跳躍,因為是借助了反復(fù)測試得到的規(guī)律,很多過程沒有贅述
?
進(jìn)行之前需要先簡單了解CLR對象分配(類型對象指針要知道),GC的基本過程(G0,G1,G2需要簡單了解),二進(jìn)制數(shù)據(jù)的存儲(主要是大小端)
?
下圖是測試中用到的引用對象的結(jié)構(gòu)
?
?下圖為測試的主要步驟,會分8步進(jìn)行,每一步也都標(biāo)注出來了
?
?
?
?第1監(jiān)測點
?
通過TestForGC_3對象里的值類型87564023523 (hex?146338F6E3 ,windows為小端存儲,所以最后搜索E3 F6 38 63 14)
前面的78 41 9B 92 為32位類型對象指針 后面接著的是同步塊索引 (如果是64位程序這2個數(shù)據(jù)則都將是64位)
?
?
根據(jù)前面TestForGC_3的地址70354802,我們在內(nèi)存里搜索到了唯一的一個匹配項,說明這里一定是張表,這個指針指向了TestForGC_3的地址
?
?
這個就是 NextObjPrt 了 ?(這個也只是推測,根據(jù)后面的操作發(fā)現(xiàn)新增對象末尾地址總是002BFB
48里面的數(shù)據(jù),以及之前的反復(fù)測試前面的類型指針也是匹配的,但測試結(jié)果并不是每次這個類型指針都是這個值,并且在不同系統(tǒng)版本下差距非常大)
后面的操作大家可以看到它的確就是NextObjPrt?,整個內(nèi)存塊里存著這個地址的位置也只有這里
?
第2監(jiān)測點
?
?
按照順序我們通過內(nèi)存搜索先找到了a1的地址
這里順便解析下對象(引用類型)在內(nèi)存里的存儲
最前面8字節(jié)為類型對象指針及同步塊索引(每個32位,如果是64位應(yīng)用則每個64位)
類型對象指針不是一成不變的,就是dotnet內(nèi)置的類型也不能保證,這次運行是一個值(地址),另外一個實例運行起來可能是另外一個(地址) (這里的地址全部使用偏移地址)
后面接著的3個8字節(jié)數(shù)據(jù)發(fā)布是TypeA里3個引用類型變量的地址,可以看到第2個地址就緊接在下面(因為是一起分配的)
順便看下string類型在內(nèi)存里的存儲
?
根據(jù)a1里面的存儲的地址02483588,輕松定位到了“testtypea”字符串
同樣與其他對象一樣擁有類型對象指針,同步索引塊,后面有4個字節(jié)的數(shù)據(jù)長度,然后后面跟數(shù)據(jù)
這也的確說明了string千真萬確引用類型,毫無爭議
最后TypeA里面還有一個引用對象TypeB,是一樣的就不重復(fù)說了,不過TypeB的指針只存在a1里面(即他的回收確實也只能靠根搜索)
?
?
現(xiàn)在我們通過a1的位置,查找內(nèi)存中含有其地址的內(nèi)存,居然搜索到了5塊內(nèi)存,而且都靠的非常近
?
?
?
?
同樣的方法搜索到bytesStart在內(nèi)存里的地址
同樣的結(jié)構(gòu)類型指針,同步索引,后面跟8字節(jié)的長度,再后面就是數(shù)據(jù)
?
?
根據(jù)地址搜索bytesStart在內(nèi)存里的指針,也只有1個(這種結(jié)果在同樣環(huán)境下運行每次的表現(xiàn)都是已有的,不過在更換運行環(huán)境后就會有明顯差異),而且也緊靠著a1的指針(可以推斷他們確實是在一張“表”上)
?
?
現(xiàn)在看下剛剛找到的NextObjPrt里面的數(shù)據(jù)是多少(02486988),下面我們看下這個地址里是什么
?
?
可以看到就指向了最后分配的bytesStart地址的后面
每個引用對象后面都有8個全0的字節(jié)(多次測試,反復(fù)分析數(shù)據(jù)都是這樣)
?
監(jiān)測點3 (為了驗證剛剛的NextObjPrt的確是那塊內(nèi)存)
?
到第3步,可以直觀的看到bytesThen就直接使用了剛剛NextObjPrt后面指向的內(nèi)存,
?
?
?
同時也看到NextObjPrt指示下的一片內(nèi)存(這個時候?qū)0 6D 48 02 的搜索也證明內(nèi)存里只有這么一塊存的是這個數(shù)據(jù))
而且可以看到這個地址確實就是bytesThen后面的內(nèi)存地址
?
第4監(jiān)測點 (重復(fù)創(chuàng)建10分份的typeA)
?
?
這次直接使用類型對象指針?biāo)阉餍聞?chuàng)建的10份TypeA (可能會搜索出其他數(shù)據(jù),因為內(nèi)存里有其他程序及測試前幾次運行殘留的數(shù)據(jù))
可以看到這些TypeA直接分配在了bytesThen的后面(測試中盡量少使用終端打印。終端打印雖然1行代碼,不過clr會創(chuàng)建很多對象去完成打印,不方便觀察)
?
?
?
現(xiàn)在想知道這些TypeA的指針,卻發(fā)現(xiàn)內(nèi)存里根本沒有這個地址(后面9個的結(jié)果一樣)
甚至連里面的TypeB的指針也搜索不到
?
其實這些TypeA從一創(chuàng)建即為不可達(dá),因為后面再也沒有用到它們的地方,即一開始就沒有任何對象引用過他們,在引用跟蹤里一直被作為垃圾
?
?
?
這個時候NextObjPrt 已經(jīng)指向了02487520
?
監(jiān)測點5 (出RunCreat)
?
?
?
出RunCreat()這個方法回到RunTest()里,NextObjPrt指向依然沒有變化(沒有新的對象創(chuàng)建)。
?
監(jiān)測點6 (回收G0)
?
?
執(zhí)行完G0回收后 ,NextObjPrt直接變?yōu)榱巳? (其實后面還有跟的8個字節(jié)的數(shù)據(jù)也變?yōu)榱?,這8個字節(jié)可能為G0閥值)
?
?
?這里有個疏忽,本來先要監(jiān)測a1的回收,現(xiàn)在發(fā)現(xiàn)后面的代碼殘留上一次的測試代碼錯誤的把a1引用了, 所以要到這一行結(jié)束a1是垃圾
?
?
?
經(jīng)過G0回收后,bytesStart ?bytesThen,應(yīng)該移動到G1,不過看他們在內(nèi)存里的位置并沒有發(fā)生任何變化(內(nèi)存里也只有這一份)
那10個在RunCreat創(chuàng)建的TypeA也似乎沒有什么變化
?
關(guān)于書上的描述跟圖例,似乎在GC完成后,G0向G1的代提升會移動內(nèi)存,不過現(xiàn)在看來并沒有移動內(nèi)存(目前GC把85000字節(jié)的數(shù)據(jù)當(dāng)作大對象,所以這里的bytesStart ?也不是大對象)
?
?
那如果bytesStart ?bytesThen是存活的,不能回收,那下一個NextObjPrt也一定緊接著在bytesThen后面
整個內(nèi)存符合條件的也就這么一處(即使是搜索?? 6D 48 02 也只有這一塊內(nèi)存符合條件),雖然這塊內(nèi)存看起來沒有什么特別的格式
?
監(jiān)測點7 (下一個地址從什么地方開始分配)
?
?
可以看到bytes這個全a的數(shù)據(jù)真的是從剛剛推測的地址開始分配內(nèi)存的,在RunCreat創(chuàng)建的TypeA也直接被覆蓋了(確實被當(dāng)作了垃圾)
?
?
NextObjPrt現(xiàn)在也正常的指向了bytes的后面
?
監(jiān)測點8
?
?
?
?
數(shù)據(jù)確實在里面被改動了,而且bytesStart ?bytesThen也的確處于G1
監(jiān)測點9
?這次回收除了a1 其他的bytesStart ?bytesThen?bytes ,應(yīng)該都會被回收
?
?
之前放著bytesStart ?bytesThen?bytes?指針的內(nèi)存 數(shù)據(jù)已經(jīng)被覆蓋了,現(xiàn)在他們都被移動到另外一塊內(nèi)存
而a1 現(xiàn)在應(yīng)該由g1提升到了g2
?
?
之前存放a1指針的地址也全部被覆蓋了,在內(nèi)存里搜索到4塊新內(nèi)存,其中一塊還與前面的bytesStart ?bytesThen 新指針放在一起
?
?
雖然現(xiàn)在NextObjPrt 現(xiàn)在不為0 ,但也明顯被重置了 (因為打印的緣故,GC后馬上創(chuàng)建了新的對象)
也很明顯,并沒有覆蓋前面的內(nèi)存,而是直接指向了后面的內(nèi)存
?
?
現(xiàn)在來看剛剛被認(rèn)為是標(biāo)記GC后下一次分配內(nèi)存的地址的的內(nèi)存塊,現(xiàn)在的地址02488A88,這個地址也十分合理,正好在兩次NextObjPrt?的中間
標(biāo)識這個地址的確是標(biāo)記GC后 新NextObjPrt的初始值
?
監(jiān)測結(jié)束
跳出RunTest,馬上就執(zhí)行了一次完全的GC
?
上面寫的比較雜亂,雖然很對東西還是沒有弄明白也沒有發(fā)現(xiàn)什么規(guī)律,不過至少可以得到下面的一些結(jié)果
1:證明了NextObjPrt 的存在,也了解他的基本行為(其結(jié)構(gòu)后面數(shù)據(jù)可能還包含G0閾值等其他數(shù)據(jù))
2:GC回收使用的標(biāo)記方法的確是根搜索
3:被回收的內(nèi)存不會被擦除,只是通過移動NextObjPrt標(biāo)記下一個內(nèi)存能被分配的位置
4:對象從G0移動到G1,內(nèi)存本身不會移動(可能記錄對象的指針的表會有相應(yīng)更新)
5:不是每次回收都會壓縮內(nèi)存,大部分時間都維持原有結(jié)構(gòu)
6:對象在內(nèi)存中的存儲細(xì)節(jié)
?
最后上面寫了那么多,其實不單單就是為了看CLR物理內(nèi)存,同樣也是表達(dá)一種方法,用同樣的方法也可以查看包括jvm在內(nèi)的幾乎所有進(jìn)程的物理內(nèi)存,同時winhex不僅可以查看,還擁有在運行時直接修改物理內(nèi)存的能力。
原文地址:http://www.cnblogs.com/lulianqi/archive/2017/02/27/6471393.html
.NET社區(qū)新聞,深度好文,微信中搜索dotNET跨平臺或掃描二維碼關(guān)注
總結(jié)
以上是生活随笔為你收集整理的在物理内存中观察CLR托管内存及GC行为的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 学习ASP.NET Core,你必须知道
- 下一篇: DataProtection设置问题引起