Java中的异常机制

一、异常

1、类继承结构

在 Java 中,所有的异常都有一个共同的祖先 Throwable(可抛出)。Throwable 指定代码中可用异常传播机制通过 Java 应用程序传输的任何问题的共性。

从图中可以看出,Throwable有两个子类,Error(错误)和Exception(异常)。二者都是 Java 异常处理的重要子类,各自都包含大量子类。

  • Error(错误):程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,Java虚拟机运行错误(Virtual MachineError),当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。

    这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如Java虚拟机运行错误(Virtual MachineError)、类定义错误(NoClassDefFoundError)等。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在 Java中,错误通过Error的子类描述。

  • Exception(异常):是程序本身可以处理的异常。

    Exception 类有一个重要的子类 RuntimeException。RuntimeException 类及其子类表示“JVM 常用操作”引发的错误。例如,若试图使用空值对象引用、除数为零或数组越界,则分别引发运行时异常(NullPointerException、ArithmeticException)和 ArrayIndexOutOfBoundException。

  • 注意:异常和错误的区别:异常能被程序本身可以处理,错误是无法处理。

2、运行时异常与非运行时异常

通常,Java的异常(包括Exception和Error)分为可查的异常(checked exceptions)和不可查的异常(unchecked exceptions)。这两个概念和运行时异常与非运行时异常差不多,但是有一点区别。

  • 可查异常(编译器要求必须处置的异常):也就是非运行时异常。正确的程序在运行中,很容易出现的、情理可容的异常状况。可查异常虽然是异常状况,但在一定程度上它的发生是可以预计的,而且一旦发生这种异常状况,就必须采取某种方式进行处理。

    除了RuntimeException及其子类以外,其他的Exception类及其子类都属于可查异常。这种异常的特点是Java编译器会检查它,也就是说,当程序中可能出现这类异常,要么用try-catch语句捕获它,要么用throws子句声明抛出它,否则编译不会通过。

  • 不可查异常(编译器不要求强制处置的异常):包括运行时异常(RuntimeException与其子类)和错误(Error)。

下图中粉色的就是可检查异常,(checked exceptions)。而不可查异常就是运行时异常加上错误。

二、异常处理机制

与异常有关的几个关键字有

  • throws
  • throw
  • try
  • catch
  • finally

1、throws与throw

throws总是出现在一个函数头中,用来标明该成员函数可能抛出的各种异常

1
2
3
public void fun1() throws Exception{

}

throw总是出现在函数体中,用来抛出一个异常。

1
2
3
public void fun1() throws Exception{
throw new Exception();
}

程序会在throw语句后立即终止,它后面的语句执行不到,然后在包含它的所有try块中(可能在上层调用函数中)从里向外寻找含有与其匹配的catch子句的try块。

如果一个函数体中有throw语句或者调用了一个带有throws的方法,那么这个方法也必须加上throws表明它可能抛出的异常。

2、try与catch

如果方法不想抛出异常,可以在自己的函数体内将可能出现的异常捕获并处理

1
2
3
4
5
6
7
8
9
10
11
public void fun1() throws Exception{
throw new Exception();
}

public void fun2(){
try{
fun1();
}catch(Exception e){

}
}

catch语句的参数类似于方法的声明,包括一个例外类型和一个例外对象。例外类型必须为Throwable类的子类,它指明了catch语句所处理的例外类型,例外对象则由运行时系统在try所指定的代码块中生成并被捕获,大括号中包含对象的处理,其中可以调用对象的方法。

catch语句可以有多个,分别处理不同类的例外。Java运行时系统从上到下分别对每个catch语句处理的例外类型进行检测,直到找到类型相匹配的catch语句为止。这里,类型匹配指catch所处理的例外类型与生成的例外对象的类型完全一致或者是它的父类,因此,catch语句的排列顺序应该是从特殊到一般。

3、finally

try所限定的代码中,当抛弃一个例外时,其后的代码不会被执行。通过finally语句可以指定一块代码。无论try所指定的程序块中抛弃或不抛弃例外,也无论catch语句的例外类型是否与所抛弃的例外的类型一致,finally所指定的代码都要被执行,它提供了统一的出口。通常在finally语句中可以进行资源的清除工作。如关闭打开的文件等。

  • 不论是return还是抛出异常,finally中的代码都会被执行。
  • 如果执行到return后再执行finally,return的值不会被改变

三、虚拟机错误

开始对这里的理解有些误区,以为虚拟机抛出的也是异常,经过上面的学习才知道在类继承关系上是属于错误。从控制台中也可以看出确实是Error二部是Exception。

在了解虚拟机异常之前,首先要知道JVM的运行时数据区域

  1. 程序计数器:是一块较小的内存空间,线程间相互独立
  2. Java虚拟机栈:也是线程私有的。每个方法执行的时候都会有一个栈帧,方法调用到执行完成的过程就是栈帧从虚拟机栈中入栈和出栈的过程。
  3. 本地方法栈:与虚拟机栈作用相似,用于Native方法,有些虚拟机中将二者合二为一。
  4. Java堆:内存中最大的一块,所有新对象都在这里创建。还可以细分为新生代和老年代。
  5. 方法区:和堆一样,是所有线程共享的,用于存储已经被加载的类信息、常量、静态变量等。也称为永久代。
  6. 运行时常量池:方法区的一部分,存放编译期生成的各种字面量和符号引用。
  7. 直接内存:直接内存并不是虚拟机内存的一部分,也不虚拟机的分配。比如NIO中的直接内存

JVM主要会产生OutOfMemory和StackOverFlow两种异常,严格的说,是java.lang.StackOverflowError和java.lang.OutOfMemoryError两种错误。

顺便说一下,下面的程序主要来自深入理解Java虚拟机这本书,SOF这个异常的例子确实是运行试验过,不过OOM这个异常把电脑跑的卡死了都没出现,不知道是不是因为虚拟机的参数设置问题还是怎么了。

1、Java堆溢出

堆是存放实例对象和数组的地方,当对象多过设置的堆大小,同时避免GC回收即可。最大内存块Xmx和最小内存块Xms一样,堆就不可扩展了。将new出的对象放到List中可防止GC回收。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 
* VM args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
* Xms equals Xmx lead to head value can't extend
*/
import java.util.ArrayList;
import java.util.List;
public class HeapOOM {
static class OOMObject {

}

public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
while (true) {
list.add(new OOMObject());
}
}
}

2、虚拟机栈和本地方法栈溢出

HotSpot虚拟机不区分虚拟机栈和本地方法栈

关于虚拟机栈和本地方法栈,Java虚拟机规范种描述了两种异常:

  • 如果线程请求的栈深度大于虚拟机所能承受的最大深度,将抛出StackOverFlow异常
  • 如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemory异常

StackOverFlow

当请求的栈深度超过JVM允许最大深度即可,用Xss设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* 
*VM args: -Xss128K
*/
public class JavaVMStackSOF {
private int stackLength = 1;
public void stackLeak() {
stackLength++;
stackLeak();
}
public static void main(String[] args) throws Throwable {
JavaVMStackSOF oom = new JavaVMStackSOF();
try {
oom.stackLeak();
} catch(Throwable e) {
System.out.println("stack length:" + oom.stackLength);
throw e;
}
}
}

在递归时如果结束判断写的有问题就会出现这种情况

OutOfMemory

不断创建线程,因为虚拟机栈是线程私有的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/* 
* VM args: -Xss2M
*/
public class JavaVMStackOOM {
private void dontStop() {
while (true) {

}
}

public void stackLeakByThread() {
while (true) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
dontStop();
}
});
thread.start();
}
}

public static void main(String[] args) {
JavaVMStackOOM oom = new JavaVMStackOOM();
oom.stackLeakByThread();
}
}

为每个线程的栈分配的内存越大,反而越容易产生内存溢出异常。

因为每个线程分配到的栈容量越大,可以建立的线程数量就越少,建立线程时就越容易把剩下的内存耗尽。

3、方法区和运行时常量池溢出

由于运行时常量池是方法区的一部分,所以他们属于一类

方法区

方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。

当前很多主流框架如Spring、Hibernate在对类进行增强时,都用到了CGLib这类字节码技术,增强的类越多,就需要越大的方法区来保证动态生成的Class可以载入内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/* 
* VM args: -XX:PermSize=10M -XX:MaxPermSize=10M
*/
import java.lang.reflect.Method;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

public class JavaMethodAreaOOM {
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(obj, args);
}
});
enhancer.create();
}
}

static class OOMObject() {

}
}

CGLib是一个强大的,高性能,高质量的Code生成类库,它可以在运行期扩展Java类与实现Java接口。Hibernate用它来实现PO(Persistent Object 持久化对象)字节码的动态生成。

运行时常量池

使用String.interm()填充常量池。intern的作用是如果该常量不再常量池中,则添加到常量池,否则返回该常量引用。常量池是方法区一部分,运行时可限制方法区PermSize和最大方法区MaxPermSize大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 
* VM args: -XX:PermSize=10M -XX:MaxPermSize=10M
*/

import java.util.List;
import java.util.ArrayList;

public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
//keep reference,avoid GC collect
List<String> list = new ArrayList<String>();
//10M PermSize in integer range enough to lead to OOM
int i = 0;
while (true) {
list.add(String.valueOf(i++).intern());
}
}
}

总结一下

溢出点 异常 原因
虚拟机栈 SOF 方法递归调用
虚拟机栈 OOM 线程太多
方法区或运行时常量池 OOM 类太多或常量太多
OOM 对象太多

四、参考地址

http://blog.csdn.net/renfufei/article/details/16344847

http://blog.csdn.net/hguisu/article/details/6155636

http://blog.csdn.net/ronawilliam/article/details/3299676