类的加载,我的理解就是将类的二进制字节码文件加载到内存中,并通过解析字节码中的常量池、类字段、类方法等信息,在jvm方法区中构建出该类的模板,并在堆区创建一个对象实例作为方法区这个类的各种数据访问入口,在jvm运行期间能够通过这个类的模板信息来调用类的静态变量、方法等。 其加载过程的话,主要分为加载、链接(验证、准备、解析)、初始化三个步骤。
1、首先加载,就是查找类的全限定类名,将类的二进制字节码文件加载到jvm的内存中,将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,并为之在堆区创建一个实例对象,作为方法区这个类的数据访问入口。当然,在加载前还需要进行验证操作,即检查字节码文件格式,看是否遵循jvm的规范。比如是否以魔数开头等。验证通过后,该类的二进制信息便会被加载到内存。
2、加载到方法区后需要验证,即检查类的语义、字节码验证、符号引用验证,看是否符合规范。
3、当验证完毕之后,就开始准备阶段,这一步主要是对类的静态变量分配内存并附上默认值。(注意:final修饰的静态变量在编译阶段就会分配,准备阶段是显示赋值,并且此阶段也不会为实例变量分配初始化)
4、然后便是解析阶段,即将符号引用转变为直接引用,得到类、字段、方法等在内存中的指针或者偏移量。
5、最后便是初始化,这个阶段主要是为类的静态变量进行显示赋值。即执行类构造器cinit方法。即执行类变量的赋值动作和静态语句块(static{}块),虚期机会保证在子类的< clinit>()方法执行之前, 父类的< clinit >()方法已经执行完毕。如果一个类中没有静态语句块,也没有对变量的赋值操作, 那么编译器可以不为这个类生成< clinit>()方法。
需要注意的是:接口与类不同的是, 执行接口的< clinit>()方法不需要先执行父接口的< clinit>()方法。只有当父接口中定义的变量被使用时, 父接口才会被初始化。 另外, 接口的实现类在初始化时也一样不会执行接口的< clinit>()方法。另外,虚拟机会保证一个类的< clinit>()方法在多线程环境中被正确地加锁和同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的< clinit>()法,其他线程部需要阻塞等待,直到活动线程执行< clinit>()方法完毕。如果在一个类的< clinit>()方法中有耗时很长的操作, 那就可能造成多个进程阻塞, 在实际应用中这种阻塞往往是隐蔽的。