NoClassDefFoundError错误而引起的JVM知识点串联

前言

今天项目在本地运行代码时报错:java.lang.NoClassDefFoundError: Could not initialize class。排查此问题同时顺便借此复习了一波Java类加载顺序及对象实例化顺序的知识。同时,写这篇博客的目的是为了对已学过的知识做一下笔记总结,如果想要从这篇文章中得到一些与众不同的理解,那么可能要让各位失望了。

分析

  • 原因: NoClassDefFoundError这个错误的发生是因为Java虚拟机在编译时能找到合适的类,而在运行时类加载器不能找到合适的类导致的错误。而其中一种原因是在运行时我们想调用某个类的方法或者访问这个类的静态成员的时候,发现这个类不可用,此时Java虚拟机就会抛出NoClassDefFoundError错误。
  • 编译时和运行时: JAVA是编译型语言,编译是将你写的代码编译成Java虚拟机可以执行的字节码。 Java代码是为了编程方便,而Java虚拟机是不能直接执行Java代码的,所以要把Java代码编译,也就是生成.class文件,然后再由Java虚拟机运行字节码文件,结果才是你写的程序。运行是Java虚拟机运行你写的代码(编译后的字节码class文件),然后显示运行结果。有些类在编译时可以通过,但运行时会报错抛出。
  • 解决: 代码中报错的类就是定义了一个静态变量:
    • public static Map<Object, String> errorCodeMap;
    • 这个静态变量没有初始化就使用了,因此加了一句代码就解决了:
      • if (errorCodeMap == null) errorCodeMapXGDX = new HashMap<>();

Java对象的实例化

  • 对象的实例化过程分为两部分:类的加载初始化和对象的初始化,对应的有类的加载顺序和对象的实例化顺序。类初始化就是执行方法,对象实例化是执行方法

Java类加载顺序

  • 类的完整生命周期:

a6566e084596e1700786813d1d98d638.png
从上图可知,类的生命周期主要有七个过程,加载,验证,准备,解析,初始化,使用,卸载。这一系列过程都是对JAVA代码编译后生成的class文件进行操作。所谓类的声明周期其实就是一个类的class文件从加载到内存开始,一直到被卸载结束,

  • 加载:

“加载”是”类加载”这个过程的一个阶段,在这个阶段,虚拟机主要做三件事:

1、根据类的全限定名获取此类的二进制字节流

2、将这个类的静态存储结构转化为方法区的运行时数据结构

3、在方法区为这个类生成java.lang.Class对象,作为方法区访问这个类的入口

由于虚拟机没有规定如何获取二进制字节流,所以这个阶段对于用户来说是很灵活的,比如可以从zip包中读取二进制文件或者从网络中获取,甚至可以通过动态代理技术在运行时生成。
普通类的加载和数组类的加载又是不同的。数组类不是由类加载器加载产生,是由Java虚拟机直接创建,但数组的类型是通过类加载器完成加载的。

  • 验证

验证是为了保证class文件中的内容是符合虚拟机规范的二进制字节流,防止通过执行一些不安全的二进制字节流而导致虚拟机奔溃。Java虚拟机并不只是执行Java语言编译后的class文件,它可以执行所有的二进制字节流文件(只要符合文件规范),所以我们不能保证其他的文件是合法的,所以需要进行一些安全校验,以保证虚拟机执行的代码是不会危害虚拟机本身安全的。

验证阶段可以分为四个部分:文件格式验证、元数据验证、字节码验证和符号引用验证。

  • 准备

准备阶段是证实为类变量分配内存并且设置初始化值的阶段,所谓初始化值通常情况下是数据类型默认的零值。这些变量所使用的内存都在方法区分配。这个阶段进行初始化的数据只有静态字段,并且是赋值初始化值(final修饰的字段除外),不是代码中定义的值。
注意进行内存分配的仅包括类变量,即被static修饰的变量,在Java中一个类的变量有两种,一种是被static修饰的类变量,一种是实例变量,从理解上来说,类相当于模板,而对象则是从模板生成的实例,所以static变量在层次上是属于类变量,而不是实例变量。而类变量所使用的内存都应当在 方法区 中进行分配。

Java基本数据类型初始值:

image.png

  • 解析

解析阶段是虚拟机将符号引用转化为直接引用的过程符号引用在class文件中以形如
“CONSTANT_Class_info”、”CONSTANT_Fieldref_info”、”CONSTANT_Methodref_info”格式存在。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类符号引用进行。

  • 初始化

初始化阶段是类加载过程的最后一步,这个阶段才开始真正的执行用户定义的Java程序。在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则需要为类变量(非final修饰的类变量)和其他变量赋值,其实就是执行类的< clinit >()方法。
< clinit >()方法是编译之后自动生成的。与类的构造方法不同,它不需要用户显示的调用,虚拟机会保证父类的< clinit >()方法先于子类的< clinit >()执行,java.lang.Object的< clinit >()方法是最先执行的。

对于初始化阶段,虚拟机严格规范了有且只有 5 种情况下,必须对类进行初始化(只有主动去使用类才会初始化类):
当遇到 new 、 getstatic、putstatic 或 invokestatic 这 4 条直接码指令时,比如 new 一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时,下面几条规则同时也是触发类加载的条件:

  • 当 jvm 执行 new 指令时会初始化类。即当程序创建一个类的实例对象。
  • 当 jvm 执行 getstatic 指令时会初始化类。即程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池)。
  • 当 jvm 执行 putstatic 指令时会初始化类。即程序给类的静态变量赋值。
  • 当 jvm 执行 invokestatic 指令时会初始化类。即程序调用类的静态方法。
  • 卸载

卸载类即该类的 Class 对象被 GC。在 JVM 生命周期类,由 jvm 自带的类加载器加载的类是不会被卸载的。但是由自定义的类加载器加载的类是可能被卸载的。
jdk 自带的 BootstrapClassLoader, ExtClassLoader, AppClassLoader 负责加载 jdk 提供的类,所以它们(类加载器的实例)肯定不会被回收。而我们自定义的类加载器的实例是可以被回收的,所以使用我们自定义加载器加载的类是可以被卸载掉的。

对象的初始化顺序

(1)父类和子类的final static属性初始化
(2)父类的static属性初始化、父类的static代码块执行
(3)子类的static属性初始化、子类的static代码块执行
(4)父类的非静态属性、父类的非静态代码块执行、父类的构造函数执行
(5)子类的非静态属性、 子类的非静态代码块执行、子类的构造函数执行

简而言之:先父类后子类,先静态后常量再构造,同等级内代码按顺序执行。当静态代码和非静态代码中成员变量包含对象,也会先执行该对象类的静态代码和构造函数

双亲委派模型

类加载阶段中的“通过一个类的全限定名来获取定义此类的二进制字节流”的动作放在虚拟机外部实现,以便让应用程序决定如何去获取所需要的类,实现这个动作的代码模块称为”类加载器“

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确定其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间

  • 有以下三种系统提供的类加载器:

(1)启动类加载器:最顶层的加载类,由C++实现,负责加载 %JAVA_HOME%/lib目录下的jar包和类或者或被 -Xbootclasspath参数指定的路径中的所有类。

(2)扩展类加载器:主要负责加载目录 %JRE_HOME%/lib/ext 目录下的jar包和类,或被 java.ext.dirs 系统变量所指定的路径下的jar包。

(3)应用程序类加载器:负责加载用户路径下(ClassPath)的代码

image.png

  • 双亲委派模型的工作机制是: 如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载。

  • 好处:

  • 使用双亲委派模型的好处在于Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存在在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的Bootstrap ClassLoader进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,那系统中将会出现多个不同的Object类,程序将混乱。因此,如果开发者尝试编写一个与rt.jar类库中重名的Java类,可以正常编译,但是永远无法被加载运行。
  • 能够提高软件系统的安全性,在此机制下,用户自定义的类加载器不可能加载本应该由父类加载器加载的可靠类

总结

一个对象想要实例化,得先进行类的加载初始化再进行对象的初始化,之前一直不太明白类加载和对象初始化的前后联系,今天才明白。具体可以参考这篇文章,写的很好:基础篇:详解JAVA对象实例化过程

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享