前面介紹的幾種異常,其實都存在這樣那樣的邏輯問題,屬於程式員的編碼手誤。還有一大類系統錯誤,錶面上看不出什麼問題,但是程式仍然運行不下去,茲舉二例說明。第一個例子且看下列的測試代碼: 執行測試代碼中的testUnlimitedString方法,一開始程式正常列印日誌,然而不一會兒就報錯退出了,錯誤信 ...
前面介紹的幾種異常,其實都存在這樣那樣的邏輯問題,屬於程式員的編碼手誤。還有一大類系統錯誤,錶面上看不出什麼問題,但是程式仍然運行不下去,茲舉二例說明。
第一個例子且看下列的測試代碼:
// 測試記憶體溢出錯誤:程式需要的記憶體超過了最大的堆記憶體配置 private static void testUnlimitedString() { String str = "Hello world"; String result = getUnlimitedString(str); // 獲取無限大小的字元串 System.out.println("result="+result.toString()); } // 獲取無限大小的字元串 private static String getUnlimitedString(String str) { System.out.println("getUnlimitedString"); String append = String.format("%s+%s", str, str); return getUnlimitedString(append); }
執行測試代碼中的testUnlimitedString方法,一開始程式正常列印日誌,然而不一會兒就報錯退出了,錯誤信息為“java.lang.OutOfMemoryError: Java heap space”,意思是記憶體溢出。仔細閱讀測試代碼,發現其中的getUnlimitedString方法會調用自身,從而形成了遞歸調用。要命的是,方法遞歸的同時不斷拼接更長的字元串append;而這意味著,每次遞歸調用之後,新的append串長度都要翻番;經過多次調用,append串所需的存儲空間以指數級別增長,於是沒多久便撐爆了程式所能用到的記憶體了。
第二個例子依舊先看下麵的下麵的測試代碼:
// 測試棧溢出錯誤:程式占用的棧空間超過了配置的棧記憶體大小 private static void testUnlimitedRecursion() { recursionAction(); // 用於遞歸動作的方法 } // 用於遞歸動作的方法 public static void recursionAction() { System.out.println("recursionAction"); recursionAction(); }
執行測試代碼中的testUnlimitedRecursion方法,結果還是很快就報錯退出了,錯誤信息為“java.lang.StackOverflowError”,意思是棧溢出。可是第二個例子在遞歸調用中並未拼接字元串,為啥仍舊出現溢出錯誤了呢?這是因為程式在運行時會申請兩塊記憶體空間,一塊叫堆記憶體,另一塊叫棧記憶體;其中堆記憶體承包了程式運行所需的大部分存儲需求,包括變數、數組、對象實例等等;而棧記憶體僅僅負責保管每次方法調用的現場數據,包括方法自身、方法的輸入參數、方法內部的基本變數等等,併在方法調用結束時釋放該方法占用的記憶體空間。前述的第一個例子,它的記憶體溢出發生於堆記憶體;至於後面的第二個例子,它的記憶體溢出發生於棧記憶體。
那麼為什麼方法調用的有關數據放在棧記憶體而不是堆記憶體呢?舉個現實生活中的例子,假設一對小夫妻帶著寶寶回家過年,隨身攜帶的物品都放在行李箱里,則行李箱就是屬於他們的堆記憶體。然後一家三口準備坐動車回來,在路上還得處理一些事情,每件事情都相當於一次方法調用。例如在車站買車票,用手掏出錢包,抽出人民幣付款買完車票,再把錢包塞回去。在這個買車票的方法中,輸入參數是錢包,輸出參數是車票,而手充當了棧記憶體的角色。買票之前,兩手空空;買票的過程中,一隻手抓著錢包;買完票後,錢包塞回去,兩手又變空了。打電話也可看作是方法調用,打電話前,兩手空空;打電話的時候,一隻手握住手機通話;打完電話,收好手機,兩手依然空空。此時雙手屬於分配給他們的棧記憶體,由於有兩隻手,因此棧記憶體的大小為二,即最多同時辦理兩件事情。
一家三口檢票上車,女人有事走開了一會兒,這時寶寶餓得大哭,男人趕緊泡奶給寶寶喂。只見這個奶爸先用左手抱著寶寶,再用右手扶著奶瓶,相當於喂奶事件擁有方法嵌套,外層的喂奶方法占用了左手這塊棧記憶體,內層的扶奶瓶方法又占用了右手這塊棧記憶體。寶寶還在喝奶的時候,苦逼的奶爸忽然內急,於是抱著寶寶一邊喂奶一邊飛奔至廁所,站在馬桶面前準備小便,猛然發現兩隻手都在忙,無法解手。只有等寶寶喝完奶,右手把奶瓶放旁邊,這樣空出來的右手才能幫忙方便。但是寶寶喝奶的方法還沒結束調用,上廁所的方法已經等不及了,怎麼辦怎麼辦?可憐的奶爸情急之下只好尿褲子了,對程式來說便發生了棧記憶體溢出。要麼有個路人伸出援手(棧記憶體大小加一),一把扯下奶爸的褲子,方能避免尿褲子的尷尬(棧溢出的錯誤)。
總結一下,凡是編碼問題造成的程式崩潰,都歸類為異常Exception;凡是系統不堪重負造成的程式崩潰,都歸類為錯誤Error。不過異常與錯誤僅是分類上的區別,實際開發中,二者的扔出和捕捉操作無甚差別,所以若沒有特殊情況,今後將使用“異常”一詞統稱異常與錯誤。
更多Java技術文章參見《Java開發筆記(序)章節目錄》