文章目录
异常结构图1
异常结构图2
1. 异常分类和关键字
1.1 分类
-
Error错误:系统处理不了的
-
Exception异常
- CheckedException受检异常(编译时异常):因为编译时异常必须在编译(编写)阶段预先处理,如果不处理编译器报错
- UnCheckedException非受检异常(运行时异常):运行时才会发生的异常,在编写程序阶段不强制处理
1.2 关键字
try-catch-finally 结构用于异常捕获和处理,而 throw 和 throws 关键字分别用于抛出异常和声明方法可能抛出的异常。
- try
用于定义一个代码块,其中的代码可能会抛出异常。如果在try块中的代码发生异常,则控制权会立即转交给相应的catch块。
try {
// 可能抛出异常的代码
}
- catch
捕获并处理try块中抛出的异常。每个catch块对应一种可能发生的异常类型。
catch (ExceptionType1 e) {
// 处理ExceptionType1类型的异常
}
catch (ExceptionType2 e) {
// 处理ExceptionType2类型的异常
}
- finally
finally块中的代码无论是否发生异常都会被执行。通常用于资源清理(如关闭文件、数据库连接等)。
finally {
// 释放资源或其他必须执行的操作
}
- throw
用于显式地抛出一个异常对象。当方法内部检测到某种错误条件时,可以使用throw关键字来抛出一个异常。
if (someCondition) {
throw new SomeException("Error message");
}
- throws
用在方法声明上,表示该方法可能会抛出某些受检异常,需要调用者负责处理这些异常。
public void someMethod() throws IOException, SQLException {
// 方法体
}
2. Error
2.1 Error定义
-
Error 是一个继承自 Throwable 类的特殊类,它表示系统级的错误或严重的资源耗尽错误,通常是不可恢复的情况。与受检异常(Checked Exception)和运行时异常(Unchecked Exception - 继承自 RuntimeException)不同,程序员通常无法预防或处理这些错误。
-
对于Error类型的异常,它们通常不是通过try-catch块来捕获并处理的,因为它们通常意味着应用程序无法继续正常运行,或者是JVM自身的问题。在实际开发中,更多关注的是如何避免触发这些严重错误以及对系统进行合理的监控和优化。
2.2 常见的Error
2.2.1 VirtualMachineError
此类错误代表JVM内部错误,如内存溢出(OutOfMemoryError)或栈空间不足(StackOverflowError)等。
2.2.2 ThreadDeath
当一个线程被另一个线程中断时抛出,虽然不推荐直接使用。
2.2.3 LinkageError
这类错误发生在类加载器加载类的过程中,例如找不到类定义(NoClassDefFoundError)、类文件格式错误(ClassFormatError)或者静态初始化失败(IncompatibleClassChangeError)等。
2.2.4 AssertionError
在启用断言的情况下,如果一个断言语句的结果为假,则会抛出此错误,用于开发者调试程序逻辑。
2.2.5 InternalError
Java虚拟机内部错误或条件出现异常情况,这种情况通常表明Java虚拟机本身存在bug。
2.2.6 OutOfMemoryError
是 Java 虚拟机(JVM)在运行时抛出的一种错误类型,它表明 JVM 已无法为应用程序分配更多内存。
2.2.6.1 OOM原因
-
Java堆空间溢出
Java对象主要存储在堆内存中。当新创建的对象过多,或者大对象占用的空间过大且生命周期较长,导致堆空间被占满,而垃圾回收器又无法及时回收足够的内存供新对象使用时,就会抛出 java.lang.OutOfMemoryError: Java heap space 错误。 解决方案:可以通过增加JVM的堆内存大小,通过调整 -Xms(初始堆大小)和 -Xmx(最大堆大小)参数;另外,还需要检查代码逻辑,是否存在不必要的对象创建或长期持有大量数据的情况,优化内存使用,提高垃圾回收效率。 -
方法区溢出(Metaspace 或 PermGen)
在Java 8及以后版本中,永久代已被元空间(Metaspace)替代,而在更早的版本中,类信息、常量池、静态变量等数据会存储在永久代(PermGen)。如果动态加载的类数量过多或者字符串常量过大,可能导致方法区内存不足。 解决方案:对于Java 8及以上版本,可通过 -XX:MaxMetaspaceSize 设置元空间的最大大小;对于Java 7及更低版本,可使用 -XX:PermSize 和 -XX:MaxPermSize 参数来限制永久代大小。同时,减少无用类的生成以及优化字符串常量使用也是必要的。 -
本地图像直方图缓存溢出
在处理大型图像或大量图像时,也可能因为本地缓冲区(如DirectBuffer)不足而导致 OOM。 解决方案:根据应用需求适当增加允许使用的本地图像缓冲区大小,或者优化对这类资源的管理与释放。
总之,在遇到 OutOfMemoryError 时,应当首先查看错误的具体信息,了解是哪部分内存区域溢出,然后结合程序逻辑和JVM监控工具(如JVisualVM、jmap等)分析内存使用情况,找出内存泄漏点或过度消耗内存的地方,进而采取针对性的解决方案。
2.2.6.2 OutOfMemoryError会导致宕机吗
-
OutOfMemoryError 是Java虚拟机(JVM)在内存分配失败时抛出的一个严重错误。当JVM无法再为应用程序分配所需的内存时,它会抛出此异常,并且通常会导致受影响的应用程序或服务停止正常工作。
-
对于一个长时间运行的服务来说,一旦出现 OutOfMemoryError,不仅当前请求可能无法完成,而且由于内存资源耗尽,其他正在执行的线程也可能会受到影响,导致系统整体性能急剧下降或者完全失效。虽然严格意义上说,它不会立即导致操作系统层面的“宕机”(即操作系统强制关闭),但对应用程序而言,其效果等同于宕机,因为应用程序将无法继续处理新的请求,甚至可能导致整个应用崩溃。
-
在生产环境中,遭遇 OutOfMemoryError 通常意味着服务需要被立刻重启以恢复功能,如果不及时处理,可能会导致严重的业务中断和服务不可用。解决这类问题的关键在于合理配置JVM内存大小、优化代码以减少不必要的内存消耗以及排查并修复潜在的内存泄漏问题。
-
现在大家的服务都是有监控的,产生了OOM,一般都是立即自动重启服务
2.2.7 StackOverflowError
是Java虚拟机(JVM)在运行时抛出的一种错误,它发生在Java程序的调用栈空间耗尽时。每个线程在执行方法调用时都会使用一个独立的调用栈来存储方法的局部变量、返回地址等信息。当递归调用过深或者在没有合理退出条件的情况下无限递归时,会持续占用栈空间,直到栈内存被占满为止,这时JVM就会抛出 java.lang.StackOverflowError。
2.2.7.1原因与解决策略
- 无限递归
这是最常见的导致 StackOverflowError 的情况,即一个方法不断地调用自身,而没有恰当的终止条件。例如,在计算阶乘或搜索算法中如果逻辑处理不当,可能会陷入死循环递归。 解决方案:检查递归函数的逻辑,确保存在正确的基线条件(基本情况),使得递归能在一定条件下终止。 - 过大或过多的本地变量
虽然不常见,但如果一个方法内部声明了大量局部变量,或者创建了非常大的数组作为局部变量,也可能导致栈空间不足。 解决方案:优化代码结构,减少不必要的局部变量,并考虑是否可以使用堆空间替代栈空间存储大型数据结构。 - 线程栈大小设置过小
每个线程默认分配的栈空间是有限的,可以通过JVM参数 -Xss 进行调整。如果应用程序中需要深度递归或大数量的局部变量,可能需要增大线程栈的大小。 解决方案:适当增加 -Xss 参数值以扩大单个线程的栈空间大小,但要注意过度增加可能导致总的线程数受限,因为总的虚拟机内存是有限的。
总之,遇到 StackOverflowError 时,应重点分析程序中的递归调用部分,确保有合理的退出条件,并且根据实际情况评估和调整JVM的线程栈大小配置。
2.2.7.2 StackOverFlowError会导致宕机吗
-
StackOverflowError 通常会导致受影响的线程停止执行,因为栈空间耗尽意味着无法继续进行方法调用和局部变量分配。对于单个线程来说,抛出 StackOverflowError 时,该线程会立即终止执行,并且错误会被抛出到异常处理机制中(如果没有捕获,则程序通常会崩溃)。
-
在多线程环境中,即使一个线程由于 StackOverflowError 崩溃,其他线程仍然可能继续运行,因此整个Java虚拟机(JVM)不一定会立即宕机或退出。但是,如果受影响的是应用程序的关键线程或者多个线程同时遭遇栈溢出问题,那么整个应用程序可能会变得不稳定,甚至可能导致整个服务不可用,这在实际效果上等同于宕机。
-
总之,虽然 StackOverflowError 不一定会直接导致整个系统的物理宕机(即操作系统层面的关闭),但它确实会导致Java应用出现严重故障,影响其正常功能,进而可能需要重启应用才能恢复服务。对于服务器端应用程序,这种错误状况应当被迅速识别并采取相应的恢复措施。
3. Exception
3.1 受检异常
3.1.1 受检异常定义
-
Java中的受检异常(Checked Exception)是指那些在编译阶段必须显式处理的异常。当一个方法可能会抛出受检异常时,开发者必须在该方法中使用 try-catch 块来捕获并处理这个异常,或者在方法签名上通过 throws 关键字声明该方法可能抛出的受检异常,将异常处理的责任传递给调用者。
-
受检异常通常用于表示程序运行时可能会遇到的、正常情况下可以预见并且应当被合理处理的情况,
要求开发人员在编写代码时显式处理。 -
Java 中的受检异常(Checked Exception)是指那些继承自 java.lang.Exception 类但不继承自 java.lang.RuntimeException 的所有异常类及其子类。这些异常在编译时必须被捕获或声明抛出,否则会导致编译错误。以下列出了一些常见的受检异常:
3.1.2 常见的受检异常
- IOException:与输入/输出操作相关的异常。
- SQLException:与数据库操作相关的异常。
- ClassNotFoundException:当试图动态加载一个类但找不到该类定义时抛出的异常。
- FileNotFoundException:试图打开一个不存在的文件时抛出的异常。
- MalformedURLException:创建URL对象时,如果URL字符串格式不正确会抛出此异常。
- SocketException:网络通信中与套接字操作有关的异常。
- ParseException:解析日期、XML文档等数据结构时遇到格式错误所抛出的异常。
- NoSuchMethodException:当试图调用一个不存在的方法时抛出的异常。
3.2 非受检异常
3.2.1 非受检异常定义
-
非受检异常(Unchecked Exception)主要包括那些继承自 java.lang.RuntimeException 类及其子类的异常。
-
非受检异常在编译阶段不会被检查,因此可以不用捕获或者在方法签名上声明。
然而,在编写代码时,仍然需要关注这些异常,并在适当的地方进行处理以确保程序健壮性。
3.2.2 常见的非受检异常
- NullPointerException:当试图访问或操作一个空引用时抛出。
- IllegalArgumentException:当向方法传递了一个非法或不合适的参数时抛出。
- ArrayIndexOutOfBoundsException:当试图访问数组的一个不存在的位置(即索引超出数组边界)时抛出。
- ClassCastException:在强制类型转换过程中,目标类型与源对象实际类型不匹配时抛出。
- NumberFormatException:当试图将字符串转换为数值类型,但该字符串格式不符合数值要求时抛出。
- IllegalStateException:如果对象当前的状态不允许执行某个方法,则会抛出此异常,通常表示程序内部逻辑错误。
- UnsupportedOperationException:当不支持请求的操作时抛出,比如某些方法在接口中声明但未实现。
- ConcurrentModificationException:并发修改集合时可能会出现此异常,如在迭代过程中修改了集合结构。
- ArithmeticException:算术运算发生错误时,例如除数为零。
- NegativeArraySizeException:创建数组时指定的大小为负数。
示例
public static void main(String[] args) {
System.out.println(3 / 0);
}
执行情况
Exception in thread “main” java.lang.ArithmeticException: / by zero
4. 异常的处理方法
方法a调用会声明抛出异常的方法b,方法a怎么做
4.1 使用 throws 关键字抛出,抛给调用者处理,抛到最后没有处理掉,则此请求线程异常
方法a在方法声明处继续抛出异常
public class ExceptionDemo {
public static void main(String[] args) throws ClassNotFoundException {
m1();
}
private static void m1() throws ClassNotFoundException{
}
}
4.2 try…catch捕获处理异常
方法a捕获异常
public class ExceptionDemo {
public static void main(String[] args) {
try {
m1();
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
private static void m1() throws ClassNotFoundException{
}
}
5.try catch finally 几种形式
不能缺少try,也不能只有try,catch、finally可以只有其一
- try + catch + finally
- try + catch
- try + finally
catch主要用来处理异常发生时的情况,可以做重试操作、打印日志或者继续抛出异常
finally用来做最后一定会执行的事情,比如资源释放、关闭
try中有return,catch或finally中必须有return
变量在try代码块中定义,作用域是延伸不到catch、finally代码块中的。
在catch代码块中定义的变量作用域也是延伸不到finally代码块中的
6. catch规则
- catch后面的小括号中的类型可以是 具体的异常类型,也可以是该异常类型的 父类型。
- catch可以写多个。建议catch的时候,精确的一个一个处理。这样有利于程序的调试。
- catch写多个的时候,从上到下,必须遵守从小到大,否则会编译报错。
- catch有多个时,只会执行一个。
- catch()异常类如果没有父子关系,可以用 | 分割。
7. 异常的的两个重要方法
- String getMessage() 返回异常的详细消息字符串
- void printStackTrace() 追踪堆栈异常信息(采用异步线程)
8. finally规则
在finally子句中的代码是最后执行的,并且是 一定会执行 的,即使try语句块中的代码出现了异常。
finally子句必须和try一起出现,不能单独编写。
8.1 f finally中代码不会执行的情况
在正常的异常处理流程和方法返回流程中,finally 块都会被执行。只有在上述特殊、极端的情况下,finally 块才可能不被执行。
8.1.1 System.exit(int) 使程序提前终止
如果在 try 或 catch 块中有 System.exit(int) 方法调用,并且该方法导致JVM退出,则finally块不会执行。
public static void main(String[] args) throws ClassNotFoundException{
try {
System.out.println("try...");
System.exit(0);
} finally {
System.out.println("finally...");
}
}
打印
8.1.2 线程中断
当运行代码的线程被中断(如通过 Thread.interrupt()),并且该中断导致了虚拟机的退出或者线程的死亡,那么 finally 块可能无法执行。
8.1.3 系统级错误
在极少数情况下,如果 JVM 遇到严重的系统错误或资源耗尽(如 StackOverflowError 或 OutOfMemoryError),而这些错误使得 JVM 无法正常完成 finally 块的执行时,finally 块可能不会被执行。
8.1.4 无限循环或死锁
若在 try 或 catch 块内出现了无限循环或死锁情况,使得控制流永远不能到达 finally 块,那么 finally 块当然也就无法执行。
8.2 return 语句和 finally 块的执行顺序
-
try块执行:
当程序执行到 try 块时,首先会执行 try 块中的代码。 -
异常处理:
如果 try 块中抛出了一个异常,则控制权立即转移到相应的 catch 块(如果有)。
如果没有异常发生,那么直接跳过 catch 块。 -
finally块执行:
- 不论 try 块是否抛出异常以及是否被 catch 块捕获,finally 块总是会被执行。
- 即使在 try 或 catch 块中有 return 语句,finally 块也会在方法返回前被执行。
-
与return结合时的特殊情况:
- 如果 try 或 catch 块中有 return 语句,在该 return 执行前,finally 块会先被执行。
- 若 finally 块中也有 return 语句,那么它会覆盖之前 try 或 catch 块中的 return 语句的返回值,并且这个最终的 return 将结束整个方法调用。
其实是两个return语句都执行了,若两个值有关联,则值是累加,若两个值没有关联,则是finally块中return语句的返回值覆盖 try 或 catch 块中的 return 语句的返回值。也有try、catch、finally三个代码块中return语句都执行的场景,如累加4示例。
累加1:
累加2
累加3
累加4
若try和catch的retrun语句可以同时执行,那么try、catch、finally的return语句都会执行起到作用,
比如下面的try中return语句后半句报错,前半句成功执行,下面的catch和finally中的return语句也执行到了。也就是说三个return语句都会执行到覆盖(其实都是try或catch和finally中的两个retrun都执行了,也有三个语句都执行的,也就是说能执行的return语句都可以执行,不会互斥,只是有先后顺序)
- 尽量不要在finally中做修改变量的操作,有没有return语句是不同的现象
finally 块中没有 return 语句,对变量进行了修改,对return的结果值是没有影响的
通过编译再反编译查看原因:
finally 块中有 return 语句,对变量进行了修改,对return的结果值有影响
通过编译再反编译查看原因:
总结来说,finally 块的执行时机是在方法即将返回前的一个固定步骤,无论是否有异常或 return 语句出现。这意味着在方法正常返回或因异常退出前,都会先执行完 finally 块中的代码。
9. final finally finalize有什么区别
- final, finally, 和 finalize 在Java中是三个不同的关键字,它们各自具有独特的用途和含义。
- final 关键字主要用于定义不可变的类、方法和变量;finally 是Java异常处理结构的关键部分,用于确保某些清理代码总能被执行;而 finalize() 是Java对象生命周期结束前可能被调用的方法,不过由于其非确定性和低效性,现代Java编程实践中往往避免直接使用。
9.1 final
- 修饰符(关键字),用于声明类、方法或变量的不可变性。
- 当用在类上时,表示该类不能被其他类继承(即它是最终类)。
- 当用在方法上时,表示该方法不能被子类重写(override)。
- 当用在变量上时,如果是基本类型,则表示常量,一旦赋值后就不能再修改;如果是引用类型,则表示引用不可变,即不能改变它所指向的对象,但可以改变对象内部的状态。
9.2 finally
- 是异常处理机制的一部分,在 try-catch-finally 结构中使用。
- 不论 try 块中的代码是否抛出异常或者使用了 return 语句,finally 块中的代码都会被执行。
- 主要用于资源清理工作,如关闭文件流、数据库连接等,确保无论程序正常执行还是出现异常都能正确释放资源。
9.3 finalize()
- 是一个方法,属于 Object 类的一个protected方法。
- 每个Java类都隐式继承自 Object 类,因此可以覆盖这个方法。
- finalize() 方法会在对象即将被垃圾回收器回收时由系统自动调用一次,给对象提供最后一次自我清理的机会。
- 注意:finalize() 方法并不保证一定会被调用,而且它的执行时机不确定,效率较低,所以不推荐依赖此方法进行重要资源的清理,而是应该通过 try-with-resources 或者显式 close 方法来管理资源。
10. 重写抛出异常限制
子类重写的方法抛出的受检异常类型应与父类方法声明的异常相容,
即子类重写的方法可以不抛出异常,也可以抛出父类方法所抛出的异常或其子类