1. 程式人生 > 實用技巧 >Java面試必會-異常

Java面試必會-異常

目錄

在 Java 中,所有的異常都有一個共同的祖先 java.lang 包中的 Throwable 類。Throwable: 有兩個重要的子類:Exception(異常)Error(錯誤) ,二者都是 Java 異常處理的重要子類,各自都包含大量子類。

Error(錯誤):是程式無法處理的錯誤,表示執行應用程式中較嚴重問題。大多數錯誤與程式碼編寫者執行的操作無關,而表示程式碼執行時 JVM(Java 虛擬機器)出現的問題。例如,Java 虛擬機器執行錯誤(Virtual MachineError),當 JVM 不再有繼續執行操作所需的記憶體資源時,將出現 OutOfMemoryError。這些異常發生時,Java 虛擬機器(JVM)一般會選擇執行緒終止。

這些錯誤表示故障發生於虛擬機器自身、或者發生在虛擬機器試圖執行應用時,如 Java 虛擬機器執行錯誤(Virtual MachineError)、類定義錯誤(NoClassDefFoundError)等。這些錯誤是不可查的,因為它們在應用程式的控制和處理能力之 外,而且絕大多數是程式執行時不允許出現的狀況。對於設計合理的應用程式來說,即使確實發生了錯誤,本質上也不應該試圖去處理它所引起的異常狀況。在 Java 中,錯誤通過 Error 的子類描述。

Exception(異常):是程式本身可以處理的異常。Exception 類有一個重要的子類 RuntimeException。RuntimeException 異常由 Java 虛擬機器丟擲。NullPointerException(要訪問的變數沒有引用任何物件時,丟擲該異常)、ArithmeticException(算術運算異常,一個整數除以 0 時,丟擲該異常)和 ArrayIndexOutOfBoundsException (下標越界異常)。

注意:異常和錯誤的區別:異常能被程式本身處理,錯誤是無法處理。

  • 典型的RuntimeException(執行時異常)包括NullPointerException,** ClassCastException(型別轉換異常),IndexOutOfBoundsException(越界異常), IllegalArgumentException(非法引數異常),ArrayStoreException(陣列儲存異常),ArithmeticException(算術異常),BufferOverflowException(緩衝區溢位異常), 併發修改異常 java.util.ConcurrentModificationException、OutOfMemoryError記憶體溢位

常見六種OOM異常和錯誤

java.lang.StackOverflowError

報這個錯誤一般是由於方法深層次的呼叫,預設的執行緒棧空間大小一般與具體的硬體平臺有關。棧記憶體為執行緒私有的空間,每個執行緒都會建立私有的棧記憶體。棧空間記憶體設定過大,建立執行緒數量較多時會出現棧記憶體溢位StackOverflowError。同時,棧記憶體也決定方法呼叫的深度,棧記憶體過小則會導致方法呼叫的深度較小,如遞迴呼叫的次數較少。

Demo:

public class StackOverFlowErrorDemo {

   static int i = 0;

    public static void main(String[] args) {
        stackOverflowErrorTest();
    }

    private static void stackOverflowErrorTest() {

        i++;
        System.out.println("這是第 "+i+" 次呼叫");
        stackOverflowErrorTest();

    }
}
//執行結果:
。。。。
這是第 6726 次呼叫
這是第 6727 次呼叫
Exception in thread "main" java.lang.StackOverflowError
。。。。

注意:這是一個Error!!!!

java.lang.OutOfMemoryError: Java heap space

Heap size 設定 JVM堆的設定是指:java程式執行過程中JVM能夠調配使用的記憶體空間的設定。JVM在啟動的時候會自己主動設定Heap size的值,其初始空間(即-Xms)是實體記憶體的1/64最大空間(-Xmx)是實體記憶體的1/4。能夠利用JVM提供的-Xmn -Xms -Xmx等選項可進行設定。Heap size 的大小是Young Generation 和Tenured Generaion 之和。

Demo:

public class OOMHeapSpaceDemo {
    public static void main(String[] args) {
        byte[] bytes = new byte[30*1024*1024];
    }
}

然後修改堆記憶體的初始容量和最大容量為5MB

執行程式,檢視結果:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at jvm.OOMHeapSpaceDemo.main(OOMHeapSpaceDemo.java:7)

注意:這是一個Error!!!!

java.lang.OutOfMemoryError:GC overhead limit exceeded

GC回收時間過長時會丟擲的OutOfMemory。過長是指,超過98%的時間都在用來做GC並且回收了不到2%的堆記憶體。連續多次的GC,都回收了不到2%的極端情況下才會丟擲。假如不丟擲GC overhead limit 錯誤會發生什麼事情呢?那就是GC清理出來的一點記憶體很快又會被再次填滿,強迫GC再次執行,這樣造成惡性迴圈,CPU的使用率一直很高,但是GC沒有任何的進展。

Demo:

/**
 * 調整虛擬機器的引數:
 * -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
 */
public class GCOverHeadDemo {
    public static void main(String[] args) {
        int i= 0;
        List<String> list = new ArrayList<>();
        while (true){
            list.add(String.valueOf(++i).intern());
            System.out.println(i);

        }
    }
}


//執行結果:
[Full GC (Ergonomics) [PSYoungGen: 1024K->1024K(2048K)] [ParOldGen: 7101K->7099K(7168K)] 8125K->8123K(9216K), [Metaspace: 3264K->3264K(1056768K)], 0.0296282 secs] [Times: user=0.08 sys=0.00, real=0.03 secs] 

Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded

java.lang.OutOfMemoryError:Direct buffer memory

寫NIO程式經常使用到ByteBuffer來讀取或者寫入資料,這是一種基於通道與緩衝區的I/O方式。它可以使用Native函式庫直接分配堆外記憶體,然後通過一個儲存在java堆裡面的DirectByteBuffer物件作為這塊記憶體的引用進行操作。這樣能在一些場景中提高效能,因為避免了java堆和Native堆中來回複製資料。

  • ByteBuffer.allocate(capability) :這種方式是分配JVM堆記憶體,屬於GC管轄範圍之內。由於需要拷貝,所以速度相對較慢;
  • ByteBuffer.allocateDirect(capability):這種方式是直接分配OS本地記憶體,不屬於GC管轄範圍之內,由於不需要記憶體拷貝所以速度相對較快。

但是如果不斷分配本地記憶體,堆記憶體很少使用,那麼JVM就不需要執行GC,DirectByteBuffer物件就不會被回收。這時候堆記憶體充足,但是本地記憶體已經用光了,再次嘗試分配的時候就會出現OutOfMemoryError,那麼程式就直接崩潰了。

Demo:

/**
 * JVM配置引數:
 * -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
 */
public class DirectBufferMemoryDemo {
    public static void main(String[] args) {
        System.out.println("配置的MaxDirectMemorySize"+sun.misc.VM.maxDirectMemory()/(double)1024/1024+" MB");
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(6*1024*1024);
    }
}


//執行結果:
配置的MaxDirectMemorySize5.0 MB
[GC (System.gc()) [PSYoungGen: 1785K->488K(2560K)] 1785K->728K(9728K), 0.0019042 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 488K->0K(2560K)] [ParOldGen: 240K->640K(7168K)] 728K->640K(9728K), [Metaspace: 3230K->3230K(1056768K)], 0.0077924 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 

Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory

java.lang.OutOfMemoryError:unable to create new native thread

準確的說,這一個異常是和程式執行的平臺相關的。導致的原因:

  • 建立了太多的執行緒,一個應用建立多個執行緒,超過系統承載極限;
  • 伺服器不允許應用程式建立這麼多的執行緒,Linux系統預設的允許單個程序可以建立的執行緒數量是1024個,當建立多 執行緒數量多於這個數字的時候就會丟擲此異常

如何解決呢?

  • 想辦法減少應用程式建立的執行緒的數量,分析應用是否真的需要建立這麼多的執行緒。如果不是,改變程式碼將執行緒數量降到最低;
  • 對於有的應用,確實需要建立很多的執行緒,遠超過Linux限制的1024個 限制,那麼可以通過修改Linux伺服器的配置,擴大Linux的預設限制。

Demo:

public class UnableCreateNewThreadDemo {
    public static void main(String[] args) {
        for (int i = 1; ;i++){
            System.out.println("i = " +i);
            new Thread(()->{
                try {
                    Thread.sleep(Integer.MAX_VALUE);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            },i+"").start();
        }
    }
}

//執行結果:
。。。。
i = 92916
i = 92917
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread

java.lang.OutOfMemoryError:MetaSpace

元空間的本質和永久代類似,都是對JVM規範中的方法區的實現。不過元空間與永久代之間最大的區別在於:元空間不在虛擬機器中,而是使用的本地記憶體。因此,預設情況下,元空間的大小僅僅受到本地記憶體的限制 。

元空間存放了以下的內容:

  • 虛擬機器載入的類資訊;
  • 常量池;
  • 靜態變數;
  • 即時編譯後的程式碼

模擬MetaSpace空間溢位,我們不斷生成類往元空間裡灌,類佔據的空間總是會超過MetaSpace指定的空間大小的

檢視元空間的大小:java -XX:+PrintFlagsInitial

Demo:

/**
 * JVM引數配置:
 * -XX:MetaSapceSize=5m
 */
public class MetaSpaceDemo {
    public static void main(String[] args) {
        for (int i = 0; i < 100_000_000; i++) {
            generate("eu.plumbr.demo.Generated" + i);
        }
    }
    public static Class generate(String name) throws Exception {
        ClassPool pool = ClassPool.getDefault();
        return pool.makeClass(name).toClass();
    }
}

Throwable 類常用方法

  • public string getMessage():返回異常發生時的簡要描述
  • public string toString():返回異常發生時的詳細資訊
  • public string getLocalizedMessage():返回異常物件的本地化資訊。使用 Throwable 的子類覆蓋這個方法,可以生成本地化資訊。如果子類沒有覆蓋該方法,則該方法返回的資訊與 getMessage()返回的結果相同
  • public void printStackTrace():在控制檯上列印 Throwable 物件封裝的異常資訊

使用 try-with-resources 來代替try-catch-finally

  • try 塊: 用於捕獲異常。其後可接零個或多個 catch 塊,如果沒有 catch 塊,則必須跟一個 finally 塊。
  • catch 塊: 用於處理 try 捕獲到的異常。
  • finally 塊: 無論是否捕獲或處理異常,finally 塊裡的語句都會被執行。當在 try 塊或 catch 塊中遇到 return 語句時,finally 語句塊將在方法返回之前被執行。

在以下 4 種特殊情況下,finally 塊不會被執行:

  1. 在 finally 語句塊第一行發生了異常。 因為在其他行,finally 塊還是會得到執行
  2. 在前面的程式碼中用了 System.exit(int)已退出程式。 exit 是帶參函式 ;若該語句在異常語句之後,finally 會執行
  3. 程式所在的執行緒死亡。
  4. 關閉 CPU。

注意: 當 try 語句和 finally 語句中都有 return 語句時,在方法返回之前,finally 語句的內容將被執行,並且 finally 語句的返回值將會覆蓋原始的返回值。如下:

public class Test {
    public static int f(int value) {
        try {
            return value * value;
        } finally {
            if (value == 2) {
                return 0;
            }
        }
    }
}

如果呼叫 f(2),返回值將是 0,因為 finally 語句的返回值覆蓋了 try 語句塊的返回值。

  1. 適用範圍(資源的定義): 任何實現 java.lang.AutoCloseable或者``java.io.Closeable` 的物件
  2. 關閉資源和final的執行順序:try-with-resources 語句中,任何 catch 或 finally 塊在宣告的資源關閉後執行

《Effecitve Java》中明確指出:

面對必須要關閉的資源,我們總是應該優先使用 try-with-resources 而不是try-finally。隨之產生的程式碼更簡短,更清晰,產生的異常對我們也更有用。try-with-resources語句讓我們更容易編寫必須要關閉的資源的程式碼,若採用try-finally則幾乎做不到這點。

Java 中類似於InputStreamOutputStreamScannerPrintWriter等的資源都需要我們呼叫close()方法來手動關閉,一般情況下我們都是通過try-catch-finally語句來實現這個需求,如下:

        //讀取文字檔案的內容
        Scanner scanner = null;
        try {
            scanner = new Scanner(new File("D://read.txt"));
            while (scanner.hasNext()) {
                System.out.println(scanner.nextLine());
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } finally {
            if (scanner != null) {
                scanner.close();
            }
        }

使用Java 7之後的 try-with-resources 語句改造上面的程式碼:

try (Scanner scanner = new Scanner(new File("test.txt"))) {
    while (scanner.hasNext()) {
        System.out.println(scanner.nextLine());
    }
} catch (FileNotFoundException fnfe) {
    fnfe.printStackTrace();
}

當然多個資源需要關閉的時候,使用 try-with-resources 實現起來也非常簡單,如果你還是用try-catch-finally可能會帶來很多問題。

通過使用分號分隔,可以在try-with-resources塊中宣告多個資源。

try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));
             BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")))) {
            int b;
            while ((b = bin.read()) != -1) {
                bout.write(b);
            }
        }
        catch (IOException e) {
            e.printStackTrace();
        }

ConcurrentModificationException

當幾十個執行緒對同一個ArrayList併發寫操作時,出現併發修改異常ConcurrentModificationException,如:

    public static void main(String[] args) {
        List<String> list = new ArrayList<>();

        for (int i = 1; i <= 30; i++) {
            new Thread(() -> {
                list.add(randomUUID().toString().substring(0,8));
                System.out.println(list);

            }, String.valueOf(i)).start();
        }
    }

這是因為ArrayList執行緒不安全,add方法在併發寫操作時容易導致異常

解決

  • new Vector<>() 執行緒安全的集合類,但效能較差

  • 可以使用 synchronizedList 封裝ArrayList

            List<Object> list = Collections.synchronizedList(new ArrayList<>());
    
  • 最好是使用JUC的寫時複製集合類,CopyOnWriteArrayList,原理是讀寫分離,寫時複製的思想,即先拷貝一份副本然後對add方法加鎖lock.lock(),在副本寫資料完成後,將集合的應用指向剛寫完的副本,代替原來的集合,這種集合適合多寫少讀的情況

    List<Object> list = new CopyOnWriteArrayList<>();
    

優化和建議

異常處理

處理流程

  • 示例,處理使用者輸入異常

    public static void main(String[] args) {
            Scanner scanner = new Scanner(System.in);
            int x1;
            while (true) {
                System.out.println("請輸入第1個數字:");
                String text1 = scanner.nextLine();
                try {
                    x1 = Integer.parseInt(text1);
                    break;
                } catch (NumberFormatException e) {
                    System.out.println("輸入有誤,請輸入數字!");
                }
            }
        }
    

try-catch-finally

1.一旦產生異常,則系統會自動產生一個異常類的例項化物件;
2.那麼,此時如果異常發生在try語句,則會自動找到匹配的catch語句執行,如果沒有在try語句中,則會將異常丟擲;
3.所有的catch根據方法的引數匹配異常類的例項化物件,如果匹配成功,則表示由此catch進行處理,

  • finally

在進行異常的處理之後,在異常的處理格式中還有一個finally語句,那麼此語句將作為異常的統一出口,不管是否產生了異常,最終都要執行此段程式碼。即使沒有發生異常,在try中使用了return語句,finally仍然會執行。

異常捕獲

異常指的是Exception,Exception類,在Java中存在一個父類Throwable(可能的丟擲)
Throwable存在兩個子類:
1.Error:表示的是錯誤,是JVM發出的錯誤操作,只能儘量避免,無法用程式碼處理。
2.Exception:一般表示所有程式中的錯誤,所以一般在程式中將進行try…catch的處理。
其中Exception包括以下兩種,它們的處理方式相同:
1.受檢異常:
當程式寫好後,編譯器會自動對所寫程式碼進行檢測,如果有問題,程式碼將會飄紅線。
例如:SQLException、IOException、ClassNotFoundException等。
2.非受檢異常:
即執行時異常(RunntimeException),編譯器無法對所寫程式碼異常進行檢測,程式將在會在執行時報錯。
例如:NullPointException、ArithmethicException、ClassCastException、ArrayIndexOutOfBundException等。

RuntimeException和Exception區別

Integer類:public static int parseInt(String text)throws NumberFormatException
此方法丟擲了異常,但是使用時卻不需要進行try…catch捕獲處理,原因:
因為NumberFormatException並不是Exception的直接子類,而是RuntimeException的子類,只要是RuntimeException的子類,則表示程式在操作的時候可以不必使用try…catch進行處理(不飄紅線),如果有異常發生,則由JVM進行處理。當然,也可以通過try…catch處理。

throws關鍵字

隨異常一起的還有一個稱為throws關鍵字,此關鍵字主要在方法的宣告上使用,表示方法中不處理異常,而交給呼叫處處理,即往上拋給它的呼叫方處理。

面試題

1.try-catch-finally中哪個部分可以省略?

答:catch和finally可以省略其中一個,catch和finally不能同時省略。
注意:格式上允許省略catch塊,但是發生異常時就不會捕獲異常了,在開發中也不會這樣去寫程式碼。

2.try-catch-finally中,如果catch中return了,finally還會執行嗎?

答:finally中的程式碼會執行。
執行流程:
1.先計算返回值,並將返回值儲存起來,等待返回;
2.執行finally程式碼塊;
3.將之前儲存的返回值返回出去。
需注意:
1.返回值是在finally運算之前就確定了,並且快取了,不管finally對該值做任何的改變,返回的值都不會改變。
2.finally程式碼中不建議包含return,因為程式會在上述的流程中提前退出,也就是說返回的值不是try或catch中的值。
3.如果在try或catch中停止了JVM,則finally不會執行。例如停電,或通過如下程式碼退出:
JVM:System.exit(0);