-
ClassNotFoundException:
此问题从java.lang.ClassNotFoundException由来,对此异常,它涉及到了java技术体系中的类加载。java的类加载机制是java技术体系中的核心问题。虽说我们和它直接打交道不多,但对其背后的机理有一定的理解有助于我们排查程序中出现的类加载失败等技术问题。 -
类的加载过程:
一个java文件从加载到被卸载整个生命过程,总共要经历5个阶段,jvm将类加载过程分为:加载->连接(验证+准备+解析)->初始化(使用前准备)->使用->卸载- 加载:
首先通过一个类的全限定名来获取此类的二进制字节流;其次将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;最后在java堆中生成一个代表这个类的class对象,作为方法区这些数据的访问入口。总的来说就是查找并加载类的二进制数据。 - 连接:
验证:确保被加载类的正确性;
准备:为类的静态变量分配内存,并将其初始化为默认值;
解析:把类中的符号引用转换为直接引用; - 为类的静态变量赋予正确的初始值
- 加载:
-
类的初始化
- 类什么时候才被初始化?
- 创建某个类的实例,也就是new一个对象
- 访问某个类的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射
- 初始化一个类的子类(会先初始化该子类的父类)
- JVM启动时标明的启动类,即文件名和类名相同的那个类
- 类的初始化顺序:
- 如果该类还没有被加载和连接则先被加载和连接
- 假如这个类存在直接父类,并且这个类还没有被初始化,(注意在类的加载器中,类只能被初始化一次),那就初始化直接的父类(不适用于接口)
- 假如类中存在初始化语句(如static变量和static块),那就依次执行这些初始化语句。
- 总的来说,初始化顺序依次是:(静态变量、静态初始化块)->(变量、初始化块)->构造器; 如果有父类,则顺序是:父类static方法->子类static方法->父类构造方法->子类构造方法
- 类什么时候才被初始化?
-
类的加载
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个这个类的 java.lang.Class对象,用来封装在方法区类的对象。 如:
类加载的最终产品是位于堆区中的Class对象。Class对象封装了在方法区内的数据结构,并且向java程序员提供了访问方法区内的数据结构的接口。加载类的方式有以下几种:
- 从本地系统直接加载
- 通过网络下载.class文件
- 从zip,jar等归档文件中加载.class文件
- 从专有数据库中提前.class文件
- 将java源文件动态编译为.class文件(server)
-
加载器:
jvm的类加载是通过ClassLoader及其子类来完成的,类的层次关系和加载顺序可以由下图来描述:
- 加载器:
-
BootstrapClassLoader(启动类加载器)
负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,加载System.getProperty(“sun.boot.class.path”)所指定的路径或jar。 -
ExtensionClassLoader(标准扩展类加载器)
负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包。载System.getProperty(“java.ext.dirs”)所指定的路径或jar。 -
AppClassLoader(系统类加载器)
负责记载classpath中指定的jar及目录中class -
CustomClassLoader(自定义类加载器)
属于应用程序自身需要定义的ClassLoader,如tomcat,jboss都会根据j2ee规范自行实现。
-
- 类加载器的顺序
- 加载过程会先检查类是否已被加载,检查顺序是自底而上,从Custom ClassLoader到Bootstrap ClassLoader逐层检查,只要某个ClassLoader已加载就视为已加载此类,保证此类被所有ClassLoader加载一次。而加载的顺序是自顶向下。
- 在加载类时,每个类加载器会加载任务给其父类,父类找不到再由自己加载。
- BootstrapLoader(启动类加载器)是最顶级的加载器,其父类加载器为null
- 加载器:
-
继承的加载顺序
下面的例子就是用static块来测试类的加载顺序。static块会在首次加载时执行
class A{
static{
System.out.println("A static");
}
}
class B extends A{
static{
System.out.println("B static");
}
}
class C extends B{
private static D d = new D();
static{
System.out.println("C static");
}
}
class D{
static{
System.out.println("D static");
}
}
public class ExtendTest {
public static void main(String[] args) {
C c = new C();
}
}
在上面的例子中,类C继承B,B继承A,而C有依赖于D。因此当创建C的时候,会自动加载C继承的B和依赖的D,然后B又会加载继承的A。只有A加载完,才能顺利的加载B;BD加载完,才能加载C。这就是类的加载顺序了。
结果为:
A static
B static
D static
C static
- 所有的变量都初始化完,才会执行构造函数
在类的加载过程中,只有类的内部变量创建完,才会执行这个类的构造方法。
class A2{
B2 b2 = new B2();
static{
System.out.println("A static");
}
public A2(){
System.out.println("A2()");
}
}
class B2{
C2 c2 = new C2();
D2 d2 = new D2();
static{
System.out.println("B static");
}
public B2() {
System.out.println("B2()");
}
}
class C2{
static{
System.out.println("C static");
}
public C2() {
System.out.println("C2()");
}
}
class D2{
static{
System.out.println("D static");
}
public D2() {
System.out.println("D2()");
}
}
public class VarTest {
public static void main(String[] args) {
A2 a2 = new A2();
}
}
在上面的例子中,A2里面有B2变量,B2则有C2D2变量。因此类的加载还是先读取到哪个,就执行相应的静态块。
当依赖的对象都定义完,才会执行构造方法:
A static
B static
C static
C2()
D static
D2()
B2()
A2()
- 静态成员与普通成员类的加载区别
在类的加载过程中,静态成员类的对象,会优先加载;而普通成员类的对象则是使用的时候才回去加载。
class A3{
B3 b3 = new B3();
static C3 c4 = new C3();
static{
System.out.println("A3");
}
class B3{
static{
System.out.println("B3");
}
}
class C3{
static{
System.out.println("C3");
}
}
public class StaticTest {
public static void main(String[] args) {
A3 a3 = new A3();
}
}
}
输出:
C3
A3
B3
总结:
第一点,所有的类都会优先加载基类
第二点,静态成员的初始化优先
第三点,成员初始化后,才会执行构造方法
第四点,静态成员的初始化与静态块的执行,发生在类加载的时候。
第四点,类对象的创建以及静态块的访问,都会触发类的加载。
类构造方法的顺序:
class A{
public A() {
System.out.println("A");
}
}
class B extends A{
public B() {
System.out.println("B");
}
}
class C extends B {
private D d1 = new D("d1");
private D d2 = new D("d2");
public C() {
System.out.println("C");
}
}
class D {
public D(String str) {
System.out.println("D "+str);
}
}
public class ExtendTest {
public static void main(String[] args) {
C c = new C();
}
}
输出:
A
B
D d1
D d2
C
结论:
首先调用基类的构造方法,其次调用成员的构造方法,最后调用自己的构造方法
-
什么时候需要类加载
- 第一次使用类信息时加载
- 类的加载原则:延迟加载,能不加载就不加载
-
触发类加载的几种情况:
- 调用静态成员时,会加载静态成员真正所在的类及其父类,通过子类调用父类的静态成员时,只会加载父类而不会加载子类
- 第一次new对象时,加载(第二次再new同一个类时,不需要再加载)
- 加载子类时会先加载父类(覆盖父类方法时所抛出的异常不能超过父类定义的范围 注:如果静态属性有final修饰时,则不会加载,当成常量使用;eg:public static final int a=123; 但如果上边的等式右值改为表达式(且该表达式在编译时不能确定其值)时则会加载类 eg:public static final int a = math.PI 如果访问的是类的公开常量,那么如果编译器在编译时能确定这个常量的值,就不会被加载; 如果编译时不能确定其值的话,则运行时加载。)
-
类加载顺序:
- 加载静态成员、代码块(Object的最先加载);再依次加载到本类的静态成员。同一个类的静态成员、代码块,按代码的顺序加载。
如果其间调用静态方法,则调用时会首先运行静态方法,再继续加载。同一个类里调用静态方法时,可以不理会代码的顺序。 - 加载非静态成员/代码块(实例块再创建对象时才会被加载,而静态成员在不创建对象时可以加载)
先递归的加载父类的非静态成员/代码块;在依次加载到本类的非静态成员。
同一个类里的非静态成员/代码块,按写代码的顺序加载,同一个类里调用方法时,可以不理会写代码的顺序。
但调用属性时,必须主席加载顺序。一半编译不通过,如果能在加载前调用,值为默认初始值(如 null 或0) - 调用构造方法
先递归的调用父类构造方法即上溯下行;默认调用父类空参的,也可在第一行写明调用父类某个参数的。
再一次到本类的构造方法:构造方法内,也可在首行写明调用本类某个其他的构造方法。
需注意得是:如果加载时遇到override成员时,可看做是所需创建的类型赋值给当前类型。
其调用按多态法则:只有非静态方法有多态;而静态方法,静态属性,非静态属性都没有多态。
假设子类override父类所有的成员,包括静态成员,非静态属性和非静态方法。
由于构造子类时会先构造父类;而构造父类时其所用的静态成员和非静态属性是父类的,但非静态方法却是父类的;
由于构造父类时,子类并未加载;如果此时所调用的非静态方法里有成员,则这个成员是子类的;
由于构造父类时,子类并未加载;如果此时所调用的非静态方法中有成员,则这个成员是子类的,且非静态属性是默认初始值的。
- 加载静态成员、代码块(Object的最先加载);再依次加载到本类的静态成员。同一个类的静态成员、代码块,按代码的顺序加载。
-
Java程序在执行过程中,类,对象及其成员加载,初始化的顺序如下:
- 首先加载要创建对象的类及其直接或间接父类
- 再类被加载的同时会将静态成员进行加载,主要包括静态成员变量的初始化,静态语句块的执行,在加载时按代码的先后顺序进行。
- 需要的类加载完成后,开始创建对象,首先会加载非静态的成员,主要包括非静态成员变量的初始化,非静态语句块的执行,在加载时按代码的先后顺序进行。
- 最后执行构造器,构造器执行完毕,对象生成。