JVM 类加载机制

本文整理自网络和书籍。推荐个很赞的资源:Java虚拟机

  • 深入浅出Java 10的实验性JIT编译器Graal

前言

  当调用java命令运行某个程序的时候,该命令将会启动一个Java虚拟机进程,不管该Java程序有多么复杂,该程序启动了多少个线程,它们都处于该Java虚拟机进程里面,它们都使用该JVM虚拟机进程的内存区。当系统出现以下集中情况的时候,JVM进程将被终止:

  • 程序运行到最后正常结束。
  • 程序运行到使用System.exit()或者Runtime.getRuntime().exit()代码处结束程序。
  • 程序执行过程中遇到未捕获的异常或者错误而结束。
  • 程序所在平台强制结束了JVM进程。

  比如,定义了一个如下的类:

public class TestJVMStatic {
    public static int a = 6;
}

public class ATest {
    public static void main(String[] args) {
        TestJVMStatic a = new TestJVMStatic();
        a.a++;
        System.out.println(a.a);
    }
}

public class BTest {
    public static void main(String[] args) {
        TestJVMStatic b = new TestJVMStatic();
        System.out.println(b.a);
    }
}

  当以此执行ATest和BTest后,a的值分别为7和6。虽然同一个类的所有实例的静态变量共享同一块内存区,但是上述代码两次运行的Java程序分别位于不同的JVM虚拟机进程中,两个JVM之间并不会共享数据。

定义

最好的人,像孩子一样,真诚。像夕阳一样,温暖。像天空一样,宁静。

  当程序主动使用某个类的时候,如果该类未被加载到内存中,则系统会通过加载、连接、初始化三个步骤来对该类进行初始化。如果没有意外,JVM将会连续完成这三个步骤,所有有时也把这三个步骤统称为类加载或者类初始化。

  虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型(创建了一个java.lang.Class对象),这就是虚拟机的类加载机制。

生命周期

  1. 加载(Loading)
  2. 验证(Verification)
  3. 准备(Preparation)
  4. 解析(Resolution)
  5. 初始化(Initialization)
  6. 使用(Using)
  7. 卸载(Unloading)

JVM 类加载机制_第1张图片


  注意:其中验证、准备、解析三个部分统称链接
  JVM 类加载机制_第2张图片
  加载(装载)、验证、准备、初始化和卸载这五个阶段顺序是固定的,类的加载过程必须按照这种顺序开始,而解析阶段不一定(不固定);它在某些情况下可以在初始化之后再开始,这是为了运行时动态绑定特性。值得注意的是:这些阶段通常都是互相交叉的混合式进行的,通常会在一个阶段执行的过程中调用或激活另外一个阶段。
  这里简要说明下Java中的绑定:绑定指的是把一个方法的调用与方法所在的类(方法主体)关联起来,对java来说,绑定分为静态绑定和动态绑定:

  1. 静态绑定:即前期绑定。在程序执行前方法已经被绑定,此时由编译器或其它连接程序实现。针对java,简单的可以理解为程序编译期的绑定。java当中的方法只有final,static,private和构造方法是前期绑定的。
  2. 动态绑定:即晚期绑定,也叫运行时绑定。在运行时根据具体对象的类型进行绑定。在Java中,几乎所有的方法都是后期绑定的。

加载

  加载阶段是“类加载机制”中的一个阶段,这个阶段通常也被称作“装载”,主要完成:

  • 通过“类全名”来获取定义此类的二进制字节流;
    • 可以从Zip包中读取,这很常见,最终成为日后JAR、EAR、WAR格式的基础。
    • 可以从网络获取,常见应用Applet。
    • 运行时计算生成,这种场景使用的最多的就是动态代理技术,在java.lang.reflect.Proxy中,就是用ProxyGenerator.generateProxyClass来为特定接口生成*$Prxoy的代理类的二进制字节流。
    • 由其他格式文件生成,典型场景:JSP应用。
    • 从数据库中读取,这种场景相对少见,有些中间件服务器(如SAP Netweaver)可以选择把程序安装到数据库中来完成程序代码在集群间的分发。
  • 将字节流所代表的静态存储结构转换为方法区的运行时数据结构;
  • 在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。

  相对于类加载过程的其他阶段,加载阶段(准备地说,是在加载阶段中获取类的二进制字节流的动作)是开发期可控性最强的阶段,因为加载阶段可以使用系统提供的类加载器(ClassLoader)来完成,也可以由用户自定义的类加载器完成。开发人员可以通过继承ClassLoader基类来创建自己的类加载器去控制字节流的获取方式。

  加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式有虚拟机实现自行定义,虚拟机并未规定此区域的具体数据结构。然后在java堆中实例化一个java.lang.Class类的对象,这个对象作为程序访问方法区中的这些类型数据的外部接口。加载阶段与链接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,链接阶段可能已经开始,但这些夹在加载阶段之中进行的动作,仍然属于链接阶段的内容,这两个阶段的开始时间仍然保持着固定的先后顺序。

连接

  当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中。类连接又可以分为如下三个阶段:

  • 验证:验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致。
  • 准备:类准备阶段则负责为类的类变量分配内存,并设置默认初始值。
  • 解析:将类的二进制数据中的符号引用替换为直接引用。

验证

  验证是链接阶段的第一步,这一步主要的目的是确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身安全。

  验证阶段主要包括四个检验过程:文件格式验证元数据验证字节码验证符号引用验证

1.文件格式验证

  验证Class文件格式规范,并且能被当前版本的虚拟机处理。

序号 内容
1 Class文件是否以魔数 0xCAFEBABE 0 x C A F E B A B E 开头。
2 主、次版本号是否在当前虚拟机处理范围之内等。
3 常量池的常量中是否有不被支持的常量类型(检查常量tag标志)。
4 指向常量的各种索引值是否有指向不存在的变量或者不符合类型的常量。
5 CONSTANT_Utf8_info型的变量中是否有不符合UTF8编码的数据。
6 Class文件中的各个部分及文件本身是否有被删除的或者附加的其他信息。
7 ……

  这个阶段的验证是基于字节流进行的,经过了这个阶段的验证之后,字节流才会进入内存的方法区中进行存储,所以后面的三个验证阶段全部是基于方法区的存储结构进行的。

2.元数据验证

  这个阶段是对字节码描述的信息进行语义分析,以保证起描述的信息符合Java语言规范要求。
  验证点可能包括如下:

序号 内容
1 这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)。
2 这个类是否继承了不允许被继承的类(被final修饰的)。
3 如果这个类的父类是抽象类,是否实现了起父类或接口中要求实现的所有方法。
4 类中的字段、方法是否与父类产生了矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等)。
5 ……

  第二个阶段的主要目的是对类的元数据信息进行语义校验,保证不存在不符合Java语言规范的元数据信息。

3.字节码验证

  该阶段的主要工作就是进行数据流和控制流分析,这个阶段对类的方法体进行校验分析
  这阶段的任务是保证被校验类的方法在运行时不会做出危害虚拟机安全的行为。如:

  1. 保证访法体中的类型转换有效,例如可以把一个子类对象赋值给父类数据类型,这是安全的,但是把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系、完全不相干的一个数据类型,则是危险和不合法的。
  2. 保证跳转命令不会跳转到方法体以外的字节码命令上。
  3. ……

  由于数据流验证的高复杂性,虚拟机设计团队为了避免将过多的时间消耗在字节码验证阶段,在JDK1.6之后的Javac编译器中进行了一项优化,给方法体的Code属性的属性表中增加了一项名为“StackMapTable”的属性,这项属性描述了方法体中所有的基本块(Basic Block,按照控制流拆分的代码块)开始时本地变量表和操作栈应有的状态,这可以将字节码验证的类型推导转变为类型检查从而节省一些时间。
  当然,理论上StackMapTable也存在错误或者被篡改的可能,所以是否有可能在恶意篡改了Code属性的同时,也生成相应的StackMapTable属性来骗过虚拟机的类型校验则是虚拟机实现时值得思考的问题。

4.符号引用验证

  1. 符号引用中通过字符串描述的全限定名是否能找到对应的类。
  2. 符号引用类中的类,字段和方法的访问性(private、protected、public、default)是否可被当前类访问。
  3. ……

准备:

  准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。这个阶段中有两个容易产生混淆的知识点,首先是这时候进行内存分配的仅包括类变量(static修饰的变量),而不包括实例变量。实例变量将会在对象实例化时随着对象一起分配在java堆中。其次是这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量定义为:

public static int value  = 12;

  那么变量value在准备阶段过后的初始值为0而不是12,因为这时候尚未开始执行任何Java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器()方法之中,所以把value赋值为12的动作将在初始化阶段才会被执行。

  上面所说的“通常情况”下初始值是零值,那相对于一些特殊的情况,如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量value就会被初始化为ConstantValue属性所指定的值,建设上面类变量value定义为:

public static final int value = 123;

  编译时javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value设置为123。

  基本数据类型的零值:

数据类型 零值
int 0
long 0L
short (short)0
char ‘\u0000’
byte (byte)0
boolean false
float 0.0f
double 0.0d
reference null

解析

  解析阶段是虚拟机常量池内的符号引用替换为直接引用的过程。

  符号引用(Symbolic References):符号引用是一组符号来描述所引用的目标对象,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标对象并不一定已经加载到内存中。

  直接引用(Direct References):直接引用可以是直接指向目标对象的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机内存布局实现相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同,如果有了直接引用,那引用的目标必定已经在内存中存在。

  虚拟机规范并没有规定解析阶段发生的具体时间,只要求了在执行anewarrycheckcastgetfieldinstanceofinvokeinterfaceinvokespecialinvokestaticinvokevirtualmultianewarraynewputfieldputstatic这13个用于操作符号引用的字节码指令之前,先对它们使用的符号引用进行解析,所以虚拟机实现会根据需要来判断,到底是在类被加载器加载时就对常量池中的符号引用进行解析,还是等到一个符号引用将要被使用前才去解析它。

  解析的动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行。分别对应编译后常量池内的CONSTANT_Class_InfoCONSTANT_Fieldref_InfoCONSTANT_Methodef_InfoCONSTANT_InterfaceMethoder_Info四种常量类型。

初始化

  类的初始化阶段是类加载过程的最后一步,在准备阶段,类变量已赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者可以从另外一个角度来表达:初始化阶段是执行类构造器()方法的过程。

  在类的初始化阶段,虚拟机负责对类进行初始化。在Java类中对类变量指定初始值有两种方式:

方式
声明类变量时指定初始值。
使用静态初始块为类变量指定初始化值。
public class Test {

    static {
        // 使用静态初始化为变量b指定初始值
        b = 6;
        System.out.println("--------------");
    }

    // 声明变量a时指定初始化值
    static int a = 5;
    static int b = 9;  // 一号
    static int c;

    public static void main(String[] args) {
        System.out.println(Test.b);
    }
}

  输出结果:

--------------
9

  上面代码先在静态初始化块中为b变量赋值,此时类变量b的之为6;接着程序向下执行,执行到一号代码处的时候,这行代码也属于该类的初始化语句,所以程序再次为类b变量赋值。也就是说,当Test类初始化结束后,该类的类变量b的值为9。

  为了验证上述结果,我们修改下b变量的声明位置:

public class Test {

    static int b = 9; // 一号

    static {
        // 使用静态初始化为变量b指定初始值
        b = 6;
        System.out.println("--------------");
    }

    // 声明变量a时指定初始化值
    static int a = 5;
    static int c;

    public static void main(String[] args) {
        System.out.println(Test.b);
    }
}

  输出结果:

--------------
6

初始化步骤

序号 内容
1 假如这个类还没有被加载或者连接,则程序先加载并连接该类。
2 假如该类的直接父类还没有被初始化,则先初始化其直接父类。
3 假如类中有初始化语句,则系统依次执行这些初始化语句。

触发初始化的时机

触发场景 内容
1 遇到newgetstaticputstaticinvokestatic这4条字节码指令时,如果类没有进行过初始化,则需先触发其初始化。生成这4条指令的最常见的Java代码场景是:
  1. 使用new关键字实例化的对象。
  2. 读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候。
  3. 调用类的静态方法的时候。
2 使用java.lang.reflect包的方法对类进行反射调用的时候。
3 当初始化一个类的时候,如果发现其父类还没有进行过初始化、则必须先初始化其父类。
4 JVM启动时,用户指定一个执行的主类(包含main方法的那个类),虚拟机会先初始化这个类。

  在上面准备阶段public static int value = 12; 在准备阶段完成后 value的值为0,而在初始化阶调用了类构造器()方法,这个阶段完成后value的值为12。

  类构造器()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句快可以赋值,但是不能访问。

  类构造器()方法与类的构造函数(实例构造函数()方法)不同,它不需要显式调用父类构造,虚拟机会保证在子类()方法执行之前,父类的()方法已经执行完毕。因此在虚拟机中的第一个执行的()方法的类肯定是java.lang.Object

  由于父类的()方法先执行,也就意味着父类中定义的静态语句快要优先于子类的变量赋值操作。

  ()方法对于类或接口来说并不是必须的,如果一个类中没有静态语句,也没有变量赋值的操作,那么编译器可以不为这个类生成()方法。

  接口中不能使用静态语句块,但接口与类不太能够的是,执行接口的()方法不需要先执行父接口的()方法。只有当父接口中定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也一样不会执行接口的()方法。

  虚拟机会保证一个类的()方法在多线程环境中被正确加锁和同步,如果多个线程同时去初始化一个类,那么只会有一个线程执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕。如果一个类的()方法中有耗时很长的操作,那就可能造成多个进程阻塞。

类加载器(ClassLoader)

  类加载器(ClassLoader)负责将.class文件(可能在磁盘上,也可能在网络上)加载到内存中,并为之生成对应的java.lang.Class对象。

加载过程详细说明

  一般来说,Java虚拟机使用Java类的方式如下:Java源文件在经过Javac之后就被转换成Java字节码文件(.class 文件)。

  类加载器负责读取Java字节代码,当需要使用一个类的时候,Java ClassLoader就会载入class进入内存中,并转换成java.lang.Class类的一个实例。

  每一个这样的实例用来表示一个Java类。实际的情况可能更加复杂,比如Java字节代码可能是通过工具动态生成的,也可能是通过网络下载。

JVM对象标识

  类加载器负责加载所有的类,系统为所有载入内存中的类生成一个java.lang.Class实例。一旦一个类被载入JVM中,同一个类就不会被再次载入了。那么,怎么样才算是“同一个类”呢?

  正如一个对象有一个唯一的标识一样,一个载入JVM中的类也有一个唯一的标志。在Java中,一个类用用其全限定类名(包括包名和类名)作为标识;但在JVM虚拟机中,一个类用其全限类名(包括包名和类名)和其类加载器作为唯一标识。

  例如,如果在pg的包中i有一个Person的类,被类加载器ClassLoader的实例负责加载,则该Person类对应的Class对象在JVM中的表示为(Person、pg、k1)。这意味着两个类加载器加载的同名类:(Person、pg、k1)和(Person、pg、k2)是不同的,它们所加载的类也是完全不同、互不兼容的。

类与类加载器

  类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远远不限于类加载阶段。

  对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟中的唯一性。说通俗一些,比较两个类是否“相等”,只有在两个类是由同一个类加载器的前提之下才有意义,否则,即使这两个类来源于同一个class文件,只要加载它的类加载器不同,那这两个类必定不相等。这里所指的“相等”包括代表类的Class对象的equal方法、isAssignableFrom()isInstance()方法及instance关键字返回的结果。

类加载器分类

JVM 类加载机制_第3张图片

  主要分为Bootstrap ClassLoaderExtension ClassLoaderApplication ClassLoaderUser Defined ClassLoader

加载器 内容
启动类加载器(Bootstrap ClassLoader) 这个类加载器使用C++语言实现,并非ClassLoader的子类。主要负责加载存放在JAVA_HOME/jre/lib/rt.jar里面所有的class文件,或者被-Xbootclasspath参数所指定路径中以rt.jar命名的文件。
扩展类加载器(Extension ClassLoader) 这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JAVA_HOME/lib/ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库。
应用程序类加载器(Application ClassLoader) 这个加载器由sun.misc.Launcher$AppClassLoader实现,它负责加载classpath对应的jar及目录。一般情况下这个就是程序中默认的类加载器。
自定义类加载器(User Defined ClassLoader) 开发人员继承ClassLoader抽象类自行实现的类加载器,基于自行开发的ClassLoader可用于并非加载classpath中(例如从网络上下载的jar或二进制字节码)、还可以在加载class文件之前做些小动作 如:加密等。

类加载机制

  JVM的类加载机制主要有以下三种:

加载机制 内容
全盘负责 所谓全盘负责,就是当一个类加载器负责加载某个Class的时候,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显式使用另外一个类加载器来载入。
双亲委托模型 所谓双亲委托,就是想让parent(父)类加载器试图加载该Class,只有在父类加载器无法加载该类的时候才尝试从自己的类路径中加载该类。
缓存机制 缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class的时候,类加载器先从缓存区搜寻该Class,只有当缓存区中不存在该Class对象的时候,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区中。这就是为什么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。

类加载器加载过程

步骤 内容
1 检测此Class是否载入过(即在缓存区中是否有此Class),如果有则直接进入第8步,否则执行第2步。
2 如果父类加载器不存在(如果没有父类加载器,则要么parent一定是根类加载器,要么本身就是根类加载器),则跳到第4步执行;如果父类加载器存在,则接着执行第3步。
3 请求使用父类加载器去载入目标类,如果成功载入则跳到第8步,否则接着执行第5步。
4 请求使用根类加载器来载入目标类,如果成功载入则跳到第8步,否则跳到第7步。
5 当前类加载器尝试寻找Class文件(从与此ClassLoader相关的类路径中寻找),如果找到则执行第6步,如果找不到则跳到第7步。
6 从文件中载入Class,成功载入后跳到第8步。
7 抛出ClassNotFoundException异常。
8 返回对应的java.lang.Class对象。

双亲委托模型

  上图中所展示的类加载器之间的这种层次关系,就称为类加载器的双亲委托模型。双亲委托模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承的关系来实现,而是使用组合关系来复用父加载器的代码。

public abstract class ClassLoader {  

    private static native void registerNatives();  
    static {  
        registerNatives();  
    }  

    // The parent class loader for delegation  
    private ClassLoader parent;  

    // Hashtable that maps packages to certs  
    private Hashtable package2certs = new Hashtable(11);  
}  

  双亲委托的工作过程:如果一个类加载器收到了一个类加载请求,它首先不会自己去加载这个类,而是把这个请求委托给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成加载请求(它管理的范围之中没有这个类)时,子加载器才会尝试着自己去加载。

  使用双亲委托模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,例如java.lang.Object存放在rt.jar之中,无论那个类加载器要加载这个类,最终都是委托给启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类,相反,如果没有双亲委托模型,由各个类加载器去完成的话,如果用户自己写一个名为java.lang.Object的类,并放在classpath中,应用程序中可能会出现多个不同的Object类,Java类型体系中最基本安全行为也就无法保证。

类加载器SPI

  java.lang.ClassLoader类提供的几个关键方法;

  loadClass:此方法负责加载指定名字的类,首先会从已加载的类中去寻找,如果没有找到;从parent ClassLoader[ExtClassLoader]中加载;如果没有加载到,则从Bootstrap ClassLoader中尝试加载(findBootstrapClassOrNull方法),如果还是加载失败,则抛出异常ClassNotFoundException,在调用自己的findClass方法进行加载。如果要改变类的加载顺序可以覆盖此方法;如果加载顺序相同,则可以通过覆盖findClass方法来做特殊处理,例如:解密,固定路径寻找等。当通过整个寻找类的过程仍然未获取Class对象,则抛出ClassNotFoundException异常。

  如果类需要resolve,在调用resolveClass进行链接。

protected synchronized Class loadClass(String name, boolean resolve)
        throws ClassNotFoundException {
    // First, check if the class has already been loaded
    Class c = findLoadedClass(name);
    if (c == null) {
        try {
            if (parent != null) {
                c = parent.loadClass(name, false);
            } else {
                c = findBootstrapClassOrNull(name);
            }
        } catch (ClassNotFoundException e) {
            // ClassNotFoundException thrown if class not found
            // from the non-null parent class loader
        }
        if (c == null) {
            // If still not found, then invoke findClass in order
            // to find the class.
            c = findClass(name);
        }
    }
    if (resolve) {
        resolveClass(c);
    }
    return c;
}

  findLoadedClass此方法负责从当前ClassLoader实例对象的缓存中寻找已加载的类,调用的为native方法。

protected final Class findLoadedClass(String name) {
    (!checkName(name))
    return null;
    urn findLoadedClass0(name);
}

private native final Class findLoadedClass0(String name);

  findClass此方法直接抛出ClassNotFoundException异常,因此要通过覆盖loadClass或此方法来以自定义的方式加载相应的类。

protected Class findClass(String name) 
    throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}

  findSystemClass 此方法是从sun.misc.Launcher$AppClassLoader中寻找类,如果未找到,则继续从BootstrapClassLoader中寻找,如果仍然未找到,返回null。

protected final Class findSystemClass(String name)
        throws ClassNotFoundException {
    ClassLoader system = getSystemClassLoader();
    if (system == null) {
        if (!checkName(name))
            throw new ClassNotFoundException(name);
        Class cls = findBootstrapClass(name);
        if (cls == null) {
            throw new ClassNotFoundException(name);
        }
        return cls;
    }
    return system.loadClass(name);
}

  defineClass此方法负责将二进制字节流转换为Class对象,这个方法对于自定义类加载器而言非常重要。如果二进制的字节码的格式不符合jvm class文件格式规范,则抛出ClassFormatError异常;如果生成的类名和二进制字节码不同,则抛出NoClassDefFoundError;如果加载的class是受保护的、采用不同签名的,或者类名是以java.开头的,则抛出SecurityException异常。

protected final Class defineClass(String name, byte[] b, 
                                     int off, int len,
                                     ProtectionDomain protectionDomain)
        throws ClassFormatError {
    return defineClassCond(name, b, off, len, protectionDomain, true);
}

// Private method w/ an extra argument for skipping class verification
private final Class defineClassCond(String name,
                                       byte[] b, int off, int len,
                                       ProtectionDomain protectionDomain,
                                       boolean verify)
        throws ClassFormatError {
    protectionDomain = preDefineClass(name, protectionDomain);

    Class c = null;
    String source = defineClassSourceLocation(protectionDomain);

    try {
        c = defineClass1(name, b, off, len, protectionDomain, source,
                verify);
    } catch (ClassFormatError cfe) {
        c = defineTransformedClass(name, b, off, len, protectionDomain, cfe,
                source, verify);
    }

    postDefineClass(c, protectionDomain);
    return c;
}

  resolveClass此方法负责完成Class对象的链接,如果链接过,则直接返回。

常见异常

  ClassNotFoundException这是最常见的异常,产生这个异常的原因为在当前的ClassLoader中加载类时,未找到类文件。

  NoClassDefFoundError 这个异常是因为 加载到的类中引用到的另外类不存在,例如要加载A,而A中盗用了B,B不存在或当前的ClassLoader无法加载B,就会抛出这个异常。

  LinkageError该异常在自定义ClassLoader的情况下更容易出现,主要原因是此类已经在ClassLoader加载过了,重复的加载会造成该异常。

附录

  • 领沃实验室成员——Notzuonotdied
  • 深入理解Java虚拟机-JVM高级特性与最佳实践
  • Java虚拟机学习 - 类加载器(ClassLoader)
  • Java ClassLoader
  • 深入分析Java ClassLoader原理
  • The basics of Java class loaders
  • Class ClassLoader
  • My class loader hates me and wants to slow me down
  • The JVM Architecture Explained
  • JVM Explained
  • 《疯狂Java讲义》

你可能感兴趣的:(JVM)