Java中不合理的使用遞歸調用,可能會導致棧內存溢出,這點是需要注意的。
java將為每個線程維護一個棧,棧里將為每個方法保存一個棧幀,棧幀代表了一個方法的運行狀態。 也就是我們常說的方法棧。最后一個為當前運行的棧幀。
那么每一次方法調用會為新調用方法的生成一個棧幀,保存當前方法的棧幀狀態,棧幀上下文切換,切換到最新的方法棧幀。
在遞歸和循環之間選擇時,應該優先選擇的是循環而非遞歸,特別是要避免深度的遞歸。
關於遞歸還需要了解的是尾遞歸調用,尾遞歸調用是可以被進行優化的。
尾調用指的是一個方法或者函數的調用在另一個方法或者函數的最后一條指令中進行。下面定義了一個foo()函數作為例子:
int foo(int a) { a = a + 1; return func(a); }
尾調用,不只是尾遞歸,函數調用本身都可以被優化掉,變得跟goto操作一樣。這就意味着,在函數調用前先把棧給設置好,調用完成后再恢復棧的這個操作(分別是prolog和epilog)可以被優化掉。
函數式語言的開發人員經常使用遞歸,所以大多數函數式語言的解釋器都會進行尾調用的優化。但是在Java中使用深度的遞歸一定要非常的小心,否則很有可能會導致棧溢出的發生。
下面是不合理使用遞歸的例子:
package test; public class RecursiveTest { /** * 遞歸實現 * * @param n * @return */ public static double recursive(long n) { if (n == 1) { return Math.log(1); } else { return Math.log(n) + recursive(n - 1); } } /** * 非遞歸實現 * * @param n * @return */ public static double directly(long n) { double result = 0; for (int i = 1; i <= n; i++) { result += Math.log(i); } return result; } public static void main(String[] args) { int i = 5000000; long test = System.nanoTime(); long start1 = System.nanoTime(); double r1 = recursive(i); long end1 = System.nanoTime(); long start2 = System.nanoTime(); double r2 = directly(i); long end2 = System.nanoTime(); System.out.println("recursive result:" + r1); System.out.println("recursive time used:" + (end1 - start1)); System.out.println("non-recursive result:" + r2); System.out.println("non-recursive time used:" + (end2 - start2)); } }
JVM中可能導致內存溢出的其他原因還包括:
- 引用變量過多使用了Static修飾 如public staitc Student s;在類中的屬性中使用 static修飾的最好只用基本類型或字符串。如public static int i = 0; //public static String str;
- 使用了大量的遞歸或無限遞歸(遞歸中用到了大量的建新的對象)
- 使用了大量循環或死循環(循環中用到了大量的新建的對象)
- 是否使用了向數據庫查詢所有記錄的方法。即一次性全部查詢的方法,如果數據量超過10萬多條了,就可能會造成內存溢出。所以在查詢時應采用“分頁查詢”。
- 是否有數組,List,Map中存放的是對象的引用而不是對象,因為這些引用會讓對應的對象不能被釋放。會大量存儲在內存中。
- 是否使用了“非字面量字符串進行+”的操作。因為String類的內容是不可變的,每次運行"+"就會產生新的對象,如果過多會造成新String對象過多,從而導致JVM沒有及時回收而出現內存溢出。
如String s1 = "My name";
String s2 = "is";
String s3 = "xuwei";
String str = s1 + s2 + s3 +.........;這是會容易造成內存溢出的