理解ClassNotFoundException与NoClassDefFoundError的区别

上篇文章已经介绍过Java的类加载机制,在类加载的过程中我们最常遇到的异常就是:

ClassNotFoundException
NoClassDefFoundError

但是你知道他们的区别吗?以及什么情况下发生上面的异常? 如果你还不清楚,那么不着急,我们来仔细分析一下:

先来说说第一个异常提示名字已经非常友好了,就是告诉我们使用类加载器就加载某个类的时候,发现所有的path下面都没有找到,从引导类路径,扩展类路径到当前的classpath下全部没有找到,就会抛出上面的异常,最常见的例子就是加载JDBC驱动包的时候,它的依赖jar并不在classpath里面,如下:

.
package class_loader.exception;

public class ExceptionTest {

    public static void main(String[] args)throws Exception {
        Class.forName("oracle.jdbc.driver.OracleDriver");
    }
}

就会抛出异常ClassNotFoundException:

Exception in thread "main" java.lang.ClassNotFoundException: oracle.jdbc.driver.OracleDriver
    at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    at java.lang.Class.forName0(Native Method)
    at java.lang.Class.forName(Class.java:264)
    at class_loader.exception.ExceptionTest.main(ExceptionTest.java:8)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)

这种情况下,其实就是类找不到,通常在执行下面的方法时容易抛出:

Class.forName(),
ClassLoader.loadClass()  
ClassLoader.findSystemClass()

接着我们看NoClassDefFoundError这个异常,严格来说不能叫异常,这种级别属于JVM的ERROR错误了,其严重级别要更高。

这个错误,主要有两种情况:

(1)编译时存在某个类,但是运行时却找不到,如下:

public class A {

    public void hello(){

        System.out.println("A hello");
    }

}

 class B {

     public static void main(String[] args) {

         A a=new A();

     }

}

上面的Java类编译后会生成两个类文件,一个A.class,一个B.class,现在我在编译后,删掉了A的class文件,然后直接执行B的main方法,就会抛出 NoClassDefFoundError错误,因为当执行到 A a=new A();这一步的时候,jvm认为这个类肯定在当前的classpath里面的,要不然编译都不会通过,更不用提执行了。既然它存在,那么在jvm里面一定能找到,如果不能找到,那就说明出大事了,因为编译和运行不一致,所以直接抛出这个ERROR,代表问题很严重。

(2)第二种情况,类根本就没有初始化成功,结果你还把它当做正常类使用,所以这事也不小,必须抛出ERROR告诉你不能再使用了。

看下面的一段代码:

public class  Loading {

    static double i=1/0;//故意使得类初始化失败.

    public static void print(){

        System.out.println("123");
    }

}

调用如下:

public static void main(String[] args) {

        try {
            double i=Loading.i;
        }catch (Throwable e){
        //此处,必须用Throwable,用Exception会直接退出.
            System.out.println(e);
        }
        //继续使用.
        Loading.print();


    }

结果如下:

Exception in thread "main" java.lang.NoClassDefFoundError: Could not initialize class class_loader.exception.Loading
java.lang.ExceptionInInitializerError
    at class_loader.exception.NoClassFoundErrorTest.main(NoClassFoundErrorTest.java:18)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)

注意这种情况比较特殊,并不是因为编译时和运行时环境不一致导致的,而是对于一个类如果初始化失败后,你还继续使用,那么JVM会认为是不正常的,由于它第一次调用已经失败,JVM就会假设后面继续调用肯定仍然会失败,所以直接抛ERROR给客户端。

这里需要注意,类初始化失败的异常是:

java.lang.ExceptionInInitializerError

也是一个严重级别的错误。

总结:

本文主要对比介绍了ClassNotFoundException与NoClassDefFoundError的区别和发生条件,从上面的测试我们可以分析出,直接采用反射或者类加载器的loadClass方法去动态加载一个所有classpath里面的都不存在的类,类加载器在运行时的load阶段就会直接抛出ClassNotFoundException异常。此外jvm认为这个异常是可以被预知的需要提前被check。对于另一种请情况,如果在编译时候正常,但在运行时执行new关键词的时候,发现依赖类找不到,或者是对于初始化失败的一个类,再次访问其静态成员或者方法,那么会直接抛出NoClassDefFoundError错误。这两种异常本质上的侧重点还是不一样的,前者侧重在类加载器加载阶段找不到类信息,后者则侧重在使用阶段时却出现了问题比如实例化依赖类找不到或者类本身就初始化失败了。

原文发布于微信公众号 - 我是攻城师(woshigcs)

原文发表时间:2018-09-26

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏码匠的流水账

easy-rules小试牛刀

easy-rules-core-3.1.0-sources.jar!/org/jeasy/rules/api/Rule.java

31610
来自专栏黑泽君的专栏

51单片机数据传送指令

22230
来自专栏林德熙的博客

C# 动态加载卸载 DLL

我最近做的软件,需要检测dll或exe是否混淆,需要反射获得类名,这时发现,C#可以加载DLL,但不能卸载DLL。于是在网上找到一个方法,可以动态加载DLL,不...

20310
来自专栏向治洪

android classloader双亲委托模式

概述 ClassLoader的双亲委托模式:classloader 按级别分为三个级别:最上级 : bootstrap classLoader(根类加载器) ;...

27380
来自专栏我是业余自学C/C++的

汇编语言-第三章 寄存器(栈存储)

35410
来自专栏皮皮之路

【JVM】浅谈双亲委派和破坏双亲委派

笔者曾经阅读过周志明的《深入理解Java虚拟机》这本书,阅读完后自以为对jvm有了一定的了解,然而当真正碰到问题的时候,才发现自己读的有多粗糙,也体会到只有实践...

28320
来自专栏Golang语言社区

Go Channel 源码剖析

0. 引言 这篇文章介绍一下 Golang channel 的内部实现,包括 channel 的数据结构以及相关操作的代码实现。代码版本 go1.9rc1,部分...

72560
来自专栏java技术学习之道

JVM初探 -JVM内存模型

16740
来自专栏Golang语言社区

GO语言标准库概览

在Go语言五周系列教程的最后一部分中,我们将带领大家一起来浏览一下Go语言丰富的标准库。 Go标准库包含了大量包,提供了丰富广泛的功能特性。这里提供了概览仅仅是...

391100
来自专栏Java编程技术

结合JVM源码谈Java类加载器

之前文章 Java 类加载器揭秘 从Java层面讲解了Java类加载器的原理,这里我们结合JVM源码在稍微深入讲解下。

11510

扫码关注云+社区

领取腾讯云代金券