Java面试必会-异常

img

img

在 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);

posted @ 2020-11-20 20:41  JavaJayV  阅读(162)  评论(0)    收藏  举报