在看这篇类加载机制之前建议大家有一些关于class文件结构的知识,免得后面有些概念不好理解。
在阅读这篇之前可以先看一下:class文件的结构
如需转载请注明出处,类的加载机制。
首先我们来了解一下类的生命周期:
图1 类的生命周期那么一个类何时加载?
答案是:不一定。
对于类何时加载Java虚拟机规范没有强制约束,可以由具体实现来决定。
但是Java虚拟机严格规范了类何时初始化。
类初始初始化条件:当且仅当以下5种情况时必须对类进行初始化。
这个当且仅当,确实可以说是一个严格的规范了。
一个类何时初始化?
1,遇到new、getstatic、putstatic或invokestatic、这4条字节码指令时,如果类没有被初始化,则需要先触发其初始化。
(1)new:使用new关键字实例化对象的时候。
(2)getstatic或putstatic:读取或设置一个类的静态字段(不包括被final关键字修饰的变量,被final修饰、已在编译期把结果放入常量池的静态字段字段除外)。
(3)invokestatic:调用一个类的静态方法。
2,使用java.lang.reflect包的方法对类进行进行反射调用时。
3,子类初始化时。(如果一个类的在初始化时父类还没有初始化,则先初始化父类)
4,虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
5,当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandler实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄时。
下面介绍几种类不初始化的情况,被动引用。
被动引用
1,通过子类引用父类的静态字段,不会导致子类初始化。
图2 被动引用例一2,通过数组定义来引用类,不会触发此类的初始化。
图3 被动引用例二3,常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类初始化。
引用其他类的final static变量后会把被引用类的值在编译器写入引用这个值的类的常量池,在编译后两个类就没有任何关系了,所以引用final static变量不会导致类的初始化。
图4 被动引用例三注意:接口与类不同,接口在初始化时,不要求其父类全部完成初始化,只有在真正使用到父类接口的时候(如引用了接口中定义的常量)才会初始化。
下面介绍类的加载过程,类的生命周期包括:加载、验证、准备、解析、初始化、使用
、卸载。
类的加载过程
1,加载
这里多说一句,类加载(Class Loading)和加载(Loading)是两个概念,希望大家不要混淆。另外Class对象对于我们常用的HotSpot虚拟机是放在方法区中,而我们平时new的实例对象是在堆上分配空间。
类加载阶段,虚拟机要完成以下3件事。
(1)通过类的全限定名获取定义此类的二进制流。
(2)将流的静态结构转化为方法区的运行时数据结构。
(3)在内存中生成一个代表这个类的java.lang.Class对象。(对于HotSpot虚拟机,Class对象是存放在方法区里的。)
2,验证
验证:确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身安全。
验证阶段大致完成下列4个动作。
1)文件格式验证:对检查格式、版本。
(1)是否以0xCAFEBABE开头。(检查魔数)
(2)主、次版本号是否在当前虚拟机处理范围之内。(JVM对于编译的class文件可以向下兼容,但是不能向上兼容,所以这里需要检查当前这个class文件是否在当前版本JVM处理范围之内。)
(3)常量池的常量中是否有不被支持的常量类型。(检查常量tag标志)
(4)指向常量的各种所以中是否有指向不存在的常量或不符合类型的常量。
(5)CONSTANT_Utf8_info型常量是否有不符合UTF8编码的数据。
(6)Class文件中各个部分及文件本身是否有被删除的或者附加的其他信息。
(7)等等……
以上验证完成后,字节流才会进入内存的方法区中进行存储。
2)元数据验证:对字节码描述进行语义分析。
(1)这个类是否有父类。(除了Object都应该有父类)
(2)这个类的父类是否继承了不允许被继承的类。(被标记为final的类)
(3)如果这个类不是抽象类,是否实现了父类或接口中要求实现的方法。
(4)类中字段、方法是否与父类产生矛盾。(例如覆盖了父类的final字段不符合规则的方法重载。)
(5) 等等……
3)字节码验证
(1)保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不 会出现int类型被按照long类型加载。
(2)保证指令不会跳转到方法体以外的字节码指令上。
(3)保证方法体中类型转换是有效的,例如不能把父类型转换成子类型。
(4)等等……
4)符号引用验证
(1)符号引用中通过字符串描述的全限定名是否能找到对应的类。
(2)在指定类中是否存在符合方法的字段描述以及简单名称所描述的方法和字 段。
(3)符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可被当前类访问。
(4)等等……
3,准备
准备阶段:
1, 正式为类变量分配内存,内存在方法区中进行分配。
2, 设置类变量的初始值。
首先注意两个方面:
1, 类变量是指static修饰的变量,而不是实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。
2, 这里说的初始值通常情况下是数据类型的零值,假设: public static int value = 123; 初始值是0,而不是123,赋值123的动作会在初始化阶段才会执行。(图5是java类型的零值表)
图5 java类型零值表public static finalint value = 123;
这种情况,变量有static final修饰的时候,在准备阶段虚拟机就会将value设置为123。
4,解析
解析:将常量池(class文件的常量池)内的符号引用替换为直接引用。
下表是符号引用和直接引用的区别:
| 引用类型 | 存在形式 | 引用目标位置 |
|---|---|---|
| 符号引用 | 任何形式字面量,无歧义定位即可 | 不一定已经加载到内存中 |
| 直接引用 | 直接指向目标的指针、相对偏移量或者间接定位到目标的句柄。 | 如果有了直接引用,那引用的目标必定已经存在在内存中。 |
5,初始化
初始化阶段才真正开始执行类中定义的Java程序代码(或者说字节码)。
初始化过程是执行类构造器<clinit>()方法的过程。
下面介绍<clinit>():
1,<clinit>()方法自动收集类中所有类变量(static)的赋值动作和静态语句块中的语句合并产生的,编译器收集顺序由代码出现顺序决定。
2,<clinit>()方法与类构造函数(实例构造器<init>()方法)不同,虚拟机会保证父类的<clinit>()执行完毕后再执行子类的<clinit>()。
因此虚拟机中第一个被执行的<clinit>()方法的类是?
答案是:Object类。
3,先看一段代码,输出结果为?
图6 <clinit>()方法执行顺序由于父类<clinit>()方法先执行,所以父类静态代码块优先于子类赋值语句执行,答案应该输出2。
4,<clinit>()对于类或接口不是必需的,如果一个类没有静态语句块,也没有对静态变量赋值操作,则编译器可以不为这个类创建<clinit>()方法。
5,接口中不能有静态块,但是可以有静态变量,所以也有<clinit>()方法,但与类不同,只有当父接口的变量被调用时才会执行父类的<clinit>()方法。同样,接口的实现类在初始化时也不会执行接口的<clinit>()方法。
6,虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的加锁、同步,如果多线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞。(同一个类加载器下,一个类型只会初始化一次。)
以上就是有关类加载的一些内容,欢迎如有问题欢迎留言,或发邮件Rick.Hsu@hotmail.com。