1. 程式人生 > >遞迴引發的jvm棧溢位的理解--堆和棧的概念整理

遞迴引發的jvm棧溢位的理解--堆和棧的概念整理

1、程式說明:

事 情是這樣的,使用者的需求是希望將某個路徑作為引數傳遞給工具,然後工具可以遍歷該目錄下的所有子目錄和檔案,將所有資料檔案進行解析轉換。對於這樣一個需 求,最常規的思路是做一個遞迴函式,遇到檔案時處理檔案,遇到目錄時則進行遞迴,這樣很快就可以把某個路徑下的所有子目錄和檔案都遍歷一遍,程式也會顯得 簡潔明瞭。程式碼一般如下:

private void recursion (Path path) {

   FileStatus[] children = fs.listStatus (path);

   for(FileStatus child : children){

if(child.isDir()){

   recursion(child.getPath());

   }

else{

    …… //執行檔案處理程式碼

   }

     }

}

這樣一段程式在我個人自測階段也沒有發現什麼問題,但是放到雲梯上實際使用的時候問題就來了——棧溢位了。工具丟擲了StackOverflowError異常。

當用戶告知出現這個問題的時候,一剎那間我曾經通讀過的DistCP的原始碼立即在我腦海中閃現了出來,曾經不理解為何要那麼寫的一段程式碼,此時此刻我終於恍然大悟。這個問題的根源在於hdfs檔案系統的目錄層次太深了,因此每一層遞迴累積起來終於將jvm的棧空間撐爆了。自測階段之所以沒有暴露出問題,完全是因為雲梯線上的目錄檔案樹在一個小叢集中很難模擬。

解決這個問題是非常簡單的,只需要將遞迴演算法替換成迭代演算法就可以了。修改後的程式碼如下:

Stackpathstack = new Stack();

for(pathstack.push(fs.getFileStatus(path));  !pathstack.empty();){

           FileStatus cur = pathstack.pop();

           FileStatus[] children = fs.listStatus(cur.getPath());

           for(int i = 0; i < children.length; i++) {

            final FileStatus child = children[i];

            if (child.isDir()) {

             pathstack.push(child);

                        }

                  else {

                        …… //執行檔案處理程式碼

                          }

            }

   }

問題雖然解決了,但對jvm堆疊方面技術需要重新審視深究,於是我順便查了些資料學習了一下。眾所周知,堆是有序完全二叉樹,棧是一種先進後出的線性表,棧的特點是速度快,jvm的壓棧和出棧操作都是非常高效的(相對來說,堆的二叉樹遍歷是要比先進後出線性表要慢的)。

“每一個Java應用都唯一對應一個JVM例項,每一個例項唯一對應一個堆。應用程式在執行中所建立的所有類例項或陣列都放在這個堆中,並由應用所有的執行緒共享.跟C/C++不同,Java中分配堆記憶體是自動初始化的。Java中所有物件的儲存空間都是在堆中分配的,但是這個物件的引用卻是在棧中分配,也就是說在建立一個物件時從兩個地方都分配記憶體,在堆中分配的記憶體實際建立這個物件,而在堆疊中分配的記憶體只是一個指向這個堆物件的指標(引用)而已。”

“JVM堆中存的是物件。JVM棧中存的是基本資料型別和JVM堆中物件的引用。一個物件的大小是不可估計的,或者說是可以動態變化的,但是在JVM棧中,一個物件只對應了一個4btye的引用。”

關於這一點,我製作了下面這段程式來進行證實:

public class StackLevel {

private int level = 1;

public void stackLevel(){

level++;

stackLevel();

}

public static void main(String[]args) throws Throwable{

StackLevel sl = new StackLevel();

try{

sl.stackLevel();

}catch(StackOverflowError e){

System.out.println(sl.level);

}

}

}

這段程式碼執行下來,可以看到預設情況下遞迴深度是10827(java version "1.6.0_65",系統不同值略有不同)。在stackLevel函式中,增加一個變數(Stringbuf = “”;)之後,再執行一遍,得到的深度為9925。隨意的給字串buf賦值,無論字串長度是多少,9925這個深度都不會發生變化。而如果再增加一個變數申明,則遞迴深度又會再次變小。從而證明了棧只存放指標,而堆負責儲存這件事情。

對於我們一般的本地化應用來說,遍歷目錄這樣的簡單任務,用遞迴還是最高效的,這不僅是因為演算法設計上較為清晰簡單,更因為遞迴利用了棧的高效,程式整體執行速度會比較快。但是對於hdfs這樣的檔案系統來說,目錄的層次深度可能會多達數十甚至數百層,這樣一來,使用遞迴必然會使得函式空間層層堆疊直到導致棧溢位,這也是為什麼DistCP中使用了Stack類來規避遞迴的原因(而我最開始看到這一段的時候只是覺得這樣的演算法費事不討巧,完全沒有理解其深層次的原因)。此外,對於MR程式設計框架來說,計算量的增加或者說計算速度的增加到是可以通過增加slot數來進行彌補的,所以,如果真遇到大量需要遍歷的應用,合理切分到多個slot中去執行才是提高效率的正道。

有趣的是,不同的語言對遞迴深度都有不同的解釋,我嘗試了python和C這兩種語言,python程式碼如下:

global level

level = 1

def stackLevel():

global level

level += 1

stackLevel()

try:

stackLevel()

except RuntimeError:

print level

得到的結果是1000,且無論在函式stackLevel中增加多少個變數,其遞迴深度都始終是1000。對於python來說遞迴深度是一個可以設定的值,其預設就是1000,可以通過(sys.setrecursionlimit(遞迴深度值))來進行設定。但是這個設定只不過是起到一個保護的作用,當設定的深度非常大,以至於超過程序記憶體空間時,python依然會報出“Segmentation fault: 11”(11是SIGSEGV訊號,無法捕獲)。

在C裡面,遞迴深度則可以到一個很大的數值,不過最終也會被SIGSEGV訊號中斷。

由這個問題再引申一下,我對遞迴的使用更加感覺需要審慎,尤其類似大資料專案的測試中,每一個遞迴都應該仔細考量其規模,因為大資料的檔案系統往往在深 度、廣度上都不是普通檔案系統可以比擬的,單機上不會出現的問題,到了大資料上都有可能成為問題。再進一步,有了這次的這個經驗,更提醒我在將來的coding中,使用遞迴前也要預估一下可能帶來的後果,防止類似的bug重現。

相關推薦

引發jvm溢位理解--概念整理

1、程式說明: 事 情是這樣的,使用者的需求是希望將某個路徑作為引數傳遞給工具,然後工具可以遍歷該目錄下的所有子目錄和檔案,將所有資料檔案進行解析轉換。對於這樣一個需 求,最常規的思路是做一個遞迴函式,遇到檔案時處理檔案,遇到目錄時則進行遞迴,這樣很快就可以把某個路徑下的所有子目錄和檔案都遍歷一遍,程式也會

JVM管理記憶體空間的區別

  在說堆和棧之前,我們先說一下JVM(虛擬機器)記憶體的劃分:      Java程式在執行時都要開闢空間,任何軟體在執行時都要在記憶體中開闢空間,Java虛擬機器執行時也是要開闢空間的。JVM執行時在記憶體中開闢一片記憶體區域,啟動時在自己的記憶體區域中進行更細緻的劃分,

】Java的區別

class 是否 基本類 直接 單位 自動 AI 和數 靈活性 1、概述 在Java中,內存分為兩種,一種是棧內存,另一種就是堆內存。 2、堆內存 1.什麽是堆內存? 堆內存是是Java內存中的一種,它的作用是用於存儲Java中的對象和數組,當我們n

JVM記憶體溢位詳解(溢位溢位,持久代溢位以及無法建立本地執行緒)

寫在前面 記憶體溢位和記憶體洩漏的區別: 記憶體溢位 out of memory,是指程式在申請記憶體時,沒有足夠的記憶體空間供其使用,出現out of memory;比如申請了一個integer,但給它存了long才能存下的數,那就是記

Java JVM:記憶體溢位溢位溢位,持久代溢位以及 nable to create native thread)

轉載自https://github.com/pzxwhc/MineKnowContainer/issues/25 包括: 1. 棧溢位(StackOverflowError) 2. 堆溢位(OutOfMemoryError:java heap space) 3. 永久代

Java JVM:記憶體溢位溢位溢位,持久代溢位以及 nable to create native thread),

Hotspot jvm的實現中,將堆記憶體分為了兩部:新生代,老年代。在堆記憶體之外,還有永久代, 其中永久代實現了規範中規定的方法區。 棧溢位:出現此種情況是因為方法執行的時候,棧的深度超過了虛擬機器容許的最大深度所致。 死遞迴: import java.util.*;

Java 中的 JVM -- 初步了解

eap 調用 程序 mmm 劃分 創建 都是 分配 2015a JVM -- Java Virtual Machine(Java虛擬機)   —— 因為要說堆和棧,所以我們必須要先簡單的說一下JVM。(JVM詳細請找度娘啦~)   首先,我們都知道 java 一直宣傳的口號

理解

啟動 操作系統 category 緩存 存儲空間 結果 來講 集中 申請   棧,在執行函數時,函數內局部變量的存儲單元都可以在棧上創建,函數執行結束時這些存儲單元自動被釋放。棧內存分配運算內置於處理器的指令集中,效率很高,但是分配的內存容量有限。  堆,就是那些由new分

C++ 理解

一、預備知識—程式的記憶體分配    一個由C/C++編譯的程式佔用的記憶體分為以下幾個部分    1、棧區(stack)—   由編譯器自動分配釋放   ,存放函式的引數值,區域性變數的值等。其    操作方式類似於資料結構中的棧。    2、堆區(heap)   —  

寫程式碼實現溢位溢位、永久代溢位、直接記憶體溢位

棧溢位(StackOverflowError) 堆溢位(OutOfMemoryError:Java heap space) 永久代溢位(OutOfMemoryError: PermGen space) 直接記憶體溢位 一、堆溢位 建立物件時如果沒有可以分配的堆記憶體,

【轉】C++中理解

一、預備知識—程式的記憶體分配 一個由c/C++編譯的程式佔用的記憶體分為以下幾個部分 1、棧區(stack)— 由編譯器自動分配釋放 ,存放函式的引數值,區域性變數的值等。其操作方式類似於資料結構中的棧。 2、堆區(heap) — 一般由程式設計師分配釋放, 若程式設計

JVM記憶體結構------,方法區,以及的區別

一 、 定義 堆:FIFO佇列優先,先進先出。JVM只有一個堆區被所有執行緒所共享!堆存放在耳機快取中,呼叫物件的速度相對慢一些,生命週期由JVM的垃圾回收機制定。 棧:FILO先進後出,暫存資料的地方。每個執行緒都包含一個棧區!棧存放在一級快取中,存取速度較快,“棧是限定

的個人理解

在瞭解堆和棧的概念之前,首先明確下資料型別的分類。 基本資料型別:Number、String、Boolean、undefined、null 引用資料型別:Object 其中基本資料型別存放在棧中,而

理解,你需要先理解……

1 “咚咚咚” “誰?” 過了很久…… “Java” 2 換一個電燈泡需要幾個程式設計師? 一個也不要,這是硬體問題。 3 一個計算機系學生坐在樹下學習,又有一個計算機系學生騎著一輛很炫的自行車經過。前 一個學生問道:“你的車從哪兒弄來的?” 騎車的回答說:“我在外面學習

深入理解Java中的

  我想這篇足以讓大家很清晰理解Java的棧和堆疊的區別。下面的是我收集了好多網友的資料加以整理的。 Java 中的堆和棧 Java把記憶體劃分成兩種:一種是棧記憶體,一種是堆記憶體。 1.棧(stack)與堆(heap)都是Java用來在Ram中存

JVM以及GC演算法的介紹

JVM就是java虛擬機器,我們可以把它理解成一個作業系統,每個不同的平臺都有不同的JVM,比如linux系統和windows系統,就是因為這個原因所以java程式就有了一個很突出的特性就是 跨平臺性 其中JVM中的堆和棧這兩個東西以及它的垃圾回收機制是我們平

Java實現二叉樹後序非遍歷(好理解

//不明白的大家可以一起討論!歡迎留言! /** * public class Node { public int data; //樹結點標號 public Node lchild;

Java的記憶體機制()簡單理解

偶然看到一道面試題,Java在例項化一個類的時候,資料在堆和棧中是如何存放的? public class A{ public int i=1; public static A a1

JVM的區別

動態分配內存 分配內存 err ava error tof over 可用 flow 1、棧是線程私有的;堆事線程公有的。2、棧主要用於存儲局部變量和對象的引用變量;堆主要用於存儲實例化的對象,數組,由JVM動態分配內存空間。3、棧中的變量超過其作用域後,JVM會自動釋放掉

VC++ 記憶體機理的個人理解(二)——

     說完了地址和指標,我們來說說堆和棧的不同,在此之前,感謝Polaris給我的幫助,在這個問題是他給我講了很多,也舉了很多例子,讓我懂了很多關於堆疊的東西。     首先,關於堆和棧的資料結構小凡就不多說什麼了,是先進先出還是後進先出也不是我們討論的範圍。     但