Java类加载的过程
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。
对于第一阶段的加载,Java虚拟机规范中没有强制约束。但是初始化只有五种情况:
- 用new关键字是花花对象,调用和设置类的静态字段,调用一个类的静态方法。
- 使用反射对类进行调用,如果类没有初始化会先将其初始化。
- 但初始化一个类时,发现其父类没有初始化,会先初始化其父类
- 但虚拟机启动时,用户需要指定一个要执行的主类。
- 当使用jdk1.7的胴体语言支持时。
这五种场景中的行为称为主动引用,所有被动引用都不会触发初始化。
- 通过子类引用父类的静态字段,不会导致子类初始化。
- 通过数组定义类引用类,不会触发此类的初始化
- 常量不会触发类的初始化(final修饰的变量在编译阶段就被存入调用类的常量池中)。
接口的加载和过程和类的加载过程有一些不同,接口只有在真正使用父接口的时候才会初始化父接口。
加载阶段
- 通过类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的java.lang.class的对象,作为方法区这个类的各种数据的访问入口。
数组类本身不通过类加载器创建,它是由Java虚拟机直接创建的。数组类的元素类型最终是由类加载器创建的。
- 如果数组的组件类型是引用类型,就使用上述的加载过程去创建这个类型
- 如果不是引用类型虚拟机就会把数组标记为与引导类加载器管理
- 如果组件类型不是引用类型,则数组的默认可见性为public。
加载完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中。Class对象虽然是一个对象,但是存放在方法区里。
验证阶段
验证是连接阶段的第一步,主要保证Class文件的字节流中包含的信息符合虚拟机的要求。验证阶段有四个阶段的校验动作:
- 文件格式校验:
- 是否以魔数开头
- 主次版本号是否在虚拟机处理范围内
- 常量池中是否有不被支持的常量类型
- 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
- CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据
- Class文件中各个部分及文件本身是否有被删除的或附加的其他信息
- 元数据校验:
- 验证当前类是否有父类
- 是否继承了final类
- 如果不是抽象类是否实现了所有方法
- 类中的字段、方法是否与父类产生矛盾
- 字节码校验:
- 保证任意时刻操作数栈的数据类型与指令代码都能配合工作
- 保证跳转指令不会跳转到防反弹以外的字节码指令上
- 保证方法体重的类型转换是有效的
- 符合引用校验:
- 符合引用中通过字符串描述的全限定名是否能找到对应的类
- 在指定类中是否存在符合方法的字段描述符以及简单名称锁描述的方法和字段
- 符合引用中的类、字段、方法的访问性是否可被当前类访问
准备阶段
准备阶段是正式为类变量分配内存并设置初始化值的阶段。这些变量所使用的内存都在方法区中分配。这时候分配的内存仅包括类变量(static修饰),实例变量将在对象实例化的时候一起分配到堆中。通常情况下初始值为数据类型的零值,赋值操作在初始化阶段才会执行。
public static int value = 1; //初始值为0
public static boolean value = true; //初始值为false
public static char value = 'a'; //初始值为'\u0000'
如果字段属性存在常量属性则准备阶段直接赋值。
public static final int value = 1; //直接赋值为1
解析阶段
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
- 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定加载到内存中。
- 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不相同。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符合引用。
初始化阶段
类初始化阶段是执行类构造器<clinit>()方法的过程。
- <clinit>()方法是编译器自动收集类中的所有类变量的赋值动作和静态语句块(statis{})中的语句合并产生的。
- <clinit>()方法与类的构造函数不同,它不需要显示的调用父类构造器,虚拟机会保证在子类的<client>()方法执行之前,父类的<client>()方法已经执行完毕。
- 由于父类的<clinit>()方法先执行,父类的静态语句块要优先于子类的变量赋值操作。
- <clinit>()方法对于类和接口来说并不是必须的,如果类没有静态语句块,也没有变量的赋值操作,那么编译器就不会生成<clinit>()方法。
- 如果接口有变量初始化赋值操作,接口与类一样都会生成<clinit>()方法。接口不需要先执行父类的<clinit>()方法,接口的实现类也不需要先执行接口的<clinit>()方法。
- 在多线程环境中,虚拟机也会保证一个类的<clinit>()方法被正确的加锁、同步。
类加载过器
类加载器是Java运行环境的一部分,负责动态加载Java类到Java虚拟机的内存空间。类通常是按需加载的,即第一次使用该类时才加载。JVM有三个默认的类加载器
- 引导(BootStrap)类加载器,负责将放在<JAVA_HOME>\lib目录中或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库加载到内存中。
- 扩展(Extensions)类加载器,由sun.misc.Launcher$ExtClassLoader实现,负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库。
- 系统(Application)类加载器,负责加载用户类路径上所指定类库。可以直接使用(ClassLoader.getSystemClassLoader()获取)
类加载器直接的关系是双亲委派模型。要求除了顶层加载器外,其余的类加载器都应当有直接的父类加载器。类的父子关系是以组合关系类实现的。
获取类加载器:
//系统类加载器
ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(appClassLoader);
//扩展类加载器
ClassLoader extClassLoader = appClassLoader.getParent();
System.out.println(extClassLoader);
//启动类加载器
ClassLoader bsClassLoader = extClassLoader.getParent();
System.out.println(bsClassLoader);
输出:
sun.misc.Launcher$AppClassLoader@2626b418
sun.misc.Launcher$ExtClassLoader@4617c264
null
系统类加载器可以直接获取,扩展类加载器的也可以使用,不过启动类加载器是由C++实现的,逻辑上是不存在的,所以为null。
其他
Java魔数:SUN公司规定每个Class文件都必须以一个word(4个字节)开始,它的唯一作用是确定这个文件是否为一个能被虚拟机接收的Class文件。用十六进制打开Class文件就可以看到Class文件的魔数是0XCAFEBABE。
主次版本号:魔数的后续的内容就是一个word的长度来表示生产的class文件的版本号,版本号分为主版本号和次版本号,高版本JDK可以向下兼容以前的Class文件,但不能运行以后版本的Class文件。
编译器版本 | 十六进制版本号 | 十进制版本号 |
---|---|---|
JDK1.9 | 0X35 | 53 |
JDK1.8 | 0X34 | 52 |
JDK1.7 | 0X33 | 51 |
JDK1.6 | 0X32 | 50 |
JDK1.5 | 0X31 | 49 |
JDK1.4 | 0X30 | 48 |
JDK1.3 | 0X2F | 47 |
JDK1.2 | 0X2E | 46 |
JDK1.1 | 0X2D | 45 |
常量池的tag项说明
常量类型 | 值 |
---|---|
CONSTANT_Class | 7 |
CONSTANT_Fieldref | 9 |
CONSTANT_Methodref | 10 |
CONSTANT_INterfaceMethodref | 11 |
CONSTANT_String | 8 |
CONSTANT_Integer | 3 |
CONSTANT_Float | 4 |
CONSTANT_Long | 5 |
CONSTANT_Double | 6 |
CONSTANT_NameAndType | 12 |
CONSTANT_Utf8 | 1 |
CONSTANT_MethodHandle | 15 |
CONSTANT_MethodType | 16 |
CONSTANT_InvokeDynamic | 18 |