Java面試必會-異常
- 常見六種OOM異常和錯誤
- java.lang.StackOverflowError
- java.lang.OutOfMemoryError: Java heap space
- java.lang.OutOfMemoryError:GC overhead limit exceeded
- java.lang.OutOfMemoryError:Direct buffer memory
- java.lang.OutOfMemoryError:unable to create new native thread
- java.lang.OutOfMemoryError:MetaSpace
- Throwable 類常用方法
- 使用 try-with-resources 來代替try-catch-finally
- ConcurrentModificationException
- 異常處理
- 面試題
在 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 塊不會被執行:
- 在 finally 語句塊第一行發生了異常。 因為在其他行,finally 塊還是會得到執行
- 在前面的程式碼中用了 System.exit(int)已退出程式。 exit 是帶參函式 ;若該語句在異常語句之後,finally 會執行
- 程式所在的執行緒死亡。
- 關閉 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 語句塊的返回值。
- 適用範圍(資源的定義): 任何實現
java.lang.AutoCloseable
或者``java.io.Closeable` 的物件 - 關閉資源和final的執行順序: 在
try-with-resources
語句中,任何 catch 或 finally 塊在宣告的資源關閉後執行
《Effecitve Java》中明確指出:
面對必須要關閉的資源,我們總是應該優先使用
try-with-resources
而不是try-finally
。隨之產生的程式碼更簡短,更清晰,產生的異常對我們也更有用。try-with-resources
語句讓我們更容易編寫必須要關閉的資源的程式碼,若採用try-finally
則幾乎做不到這點。
Java 中類似於InputStream
、OutputStream
、Scanner
、PrintWriter
等的資源都需要我們呼叫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);