之前的文章中有系統(tǒng)的講過GC相關(guān)的理論知識方库,如果對GC相關(guān)的理論知識不太理解的朋友障斋,可以閱讀一下:漫談GC —— GC基本理論和深度剖析
提到遞歸,很多人的第一反應(yīng)就是著名的 StackOverflowException
, 棧溢出錯誤, 能夠理解遞歸調(diào)用的邏輯是操作系統(tǒng)上的一個壓棧操作,通常情況下晴裹,棧的內(nèi)存非常小,所以調(diào)用層次很深的話就會產(chǎn)生類似的錯誤只磷。我做了一個小測試,看一下調(diào)用多少層之后预厌,我的系統(tǒng)會報出棧溢出元媚。
代碼如下(C#實現(xiàn)):
public class TestClass
{
public void M1(int num)
{
if (num < int.MaxValue)
{
num++;
Console.WriteLine(num);
M1(num);
}
}
}
可以看到我設(shè)置的遞歸出口是int自增到int最大值的時候退出。
可以看到我的機器大概會在51520次的時候炭晒,棧溢出甥角。由于方法里面幾乎沒有額外操作,所以這個數(shù)值比實際情況中大一些震束,而且計算機不同当犯,結(jié)果肯定也不一樣,日常的方法調(diào)用層數(shù)一般不會大于這個數(shù)值嚎卫,所以常規(guī)的開發(fā)中,棧溢出基本上不會碰到胸懈。這里不是為了測試棧溢出的臨界值的恰响,所以只是簡單理解一下棧溢出的異常就夠了。
那這里提出一個問題首有,遞歸有沒有可能內(nèi)存溢出呢枢劝?
看下面這段測試代碼(C#):
public void M1(int num)
{
Console.WriteLine("倒數(shù)第:" + num); //輸出
int[] arr = new int[100000]; //定義了一個長度為10W的數(shù)組
for (int i = 0; i < arr.Length; i++)
{
arr[i] = 100;
}
if (num > 0)
{
M1(num - 1);
}
}
public static void Main(string[] args)
{
temp.M1(50000);//由于我的電腦的棧溢出大概在50000以上,所以設(shè)置50000烙常,看一下效果
}
上面的代碼也不難理解,有幾點提及一下:
- 我的電腦 的棧溢出在50000+蚕脏,所以調(diào)用層數(shù)設(shè)置為50000驼鞭,
- 每次遞歸的時候,定義了一個比較大的數(shù)組挣棕,長度為10W,每次賦值之后沒有額外的任何操作固耘。
我們看一下執(zhí)行的結(jié)果:
可以看到词身,發(fā)生了
OutOfMemoryException
內(nèi)存溢出,而遞歸只進行了不到10000次。
C# & Java GC算法
這里不談太多的理論上的內(nèi)容户辫,理論上的細節(jié)可以看我在開篇引言中提到的另一篇文章渔欢。
C#與Java中,GC采用的是GC Root 的鏈路可達性分析算法解決的GC標記問題奥额。原理就是一個對象只要有GC Root引用,就不會釋放韩肝,這個不難理解九榔,那么什么對象會被定義為GC Root對象呢?
GC Root對象大概包括這幾類(我在概念文章里也提及了):
- 虛擬機棧對象(JVM哲泊,CLR等)
- OS 棧(操作系統(tǒng)本地方法的棧切威,Native)
- 方法區(qū)中的常量或者靜態(tài)引用的對象等。
上面的例子就是由于方法壓棧先朦,產(chǎn)生的問題犬缨,每一個新生成的數(shù)組锋谐,都被壓棧進去的方法所引用涮拗,進行GC的時候,都是可達對象三热,無法GC,所以不會被釋放呐能。
實際生產(chǎn)問題
這里描述一下當時的問題:
當時是有一個服務(wù)器的文件夾抑堡,里面存儲了大量的mp3文件,遞歸的過程中首妖,需要把他們統(tǒng)一取出來有缆,放入一個數(shù)組,過程中使用了遞歸的方式遍歷所有文件夾下的文件棚壁,生成了大量的臨時字符串,而字符串本質(zhì)上也是一個byte數(shù)組史隆,長度很大的話曼验,也是比較消耗內(nèi)存的,遞歸過程中同樣不被釋放魄幕,而服務(wù)器的內(nèi)存較小颖杏,只有2GB。本地測試的時候,機器是16G電腦咙轩,沒有問題阴颖,但是到服務(wù)器上就崩潰了。
遞歸雖然是很優(yōu)雅的編程方式钾菊,在諸多算法里面也是熱點偎肃,但是同樣不能忽視遞歸調(diào)用過程中的棧溢出,以及上文提及的內(nèi)存溢出累颂。
后記
Go語言的垃圾回收機制,沒有使用鏈路可達性分析料饥,而是三色標記法朱监,細節(jié)可以看這里
這里還是貼一段代碼:
package main
var count = 0
func main() {
arr := make([]int,10)
test(arr)
}
func test(arr []int ) []int {
if count == 20 {
return arr
}else{
count++
caps:= cap(arr)
for i:= 0;i< 2 * caps;i++{
arr = append(arr, i)
}
println(arr, cap(arr))
arr = test(arr)
}
return arr
}
上面的遞歸算法同樣沒有到20次赌朋,就拋出了內(nèi)存溢出篇裁,請了解Go的小伙伴通過對三色標記法的理解,留言討論一下吧达布。