加载时机
类的生命周期。其中3、4、5统称连接(Linking)。
1 | st=>start: 开始 |
有且只有
以下5种情况,称为主动引用,虚拟机需要立即开始类的初始化阶段。
- 收到new、getstatic、putstatic、invokestatic指令时,如果该类未进行初始化则进行。
- 使用反射对类进行调用的时候,如果该类未进行初始化则进行。
- 对一个类进行初始化,如果父类未进行初始化,如果父类未进行初始化则进行。(如果是接口,并没有该要求。)
- 遇到main方法所在的那个类,先对该类进行初始化则进行。
- jdk1.7中的动态语言支持时。
而被动引用如通过数组定义引用类、子类引用父类静态字段都不会触发类的初始化。
类加载过程
类加载过程具体分为5个,分别为加载、验证、准备、解析、初始化。
0x00 加载阶段
- 通过类的全限定名获取二进制字节流。
- 该字节流的静态存储结构转换为运行时数据结构。
- 在内存中生成该类的java.lang.Class对象,作为方法区中该类各数据的访问入口。存储在方法区中。
0x01 验证阶段
该阶段确保Class文件格式符合虚拟机要求,起到保护虚拟机自身安全。虽然Java语言本身相对安全,如果发生语法错误,大部分可以在编译阶段检测。但是由于Class文件来源可以是任意的,甚至可以直接使用十六进制编辑器直接编写,所以,需要该步骤保证本身安全。
大致分为以下四个检测内容:文件格式、元数据、字节码、符号引用。
- 文件格式:该检测在二进制字节流时进行,通过验证后,字节流才进入内存的方法区存储。
- 元数据:保证符合Java语言规范。如(所有类都有父类,除了Object;是否继承了final修饰的类等等)
- 字节码:确保程序语义是合法、符合逻辑的。jdk1.6之后添加StackMapTable表,只需要检测该表的属性是否合理即可。
- 符号引用:确保解析动作能正常完成。
0x10 准备阶段
准备阶段是为
类变量
(被static修饰)分配内存并设置类变量初始值(零值)
,使用的内存在方法区中申请。例如,public static int a = 123;在准备阶段只会设置为零值0,而在初始化阶段才会设置为123。在一些特殊情况下,如类字段中属性表含有ConstantValue,那么在准备阶段就会赋值为123,加final即可。
0x11 解析阶段
解析是将常量池中的符号引用替换为直接引用的过程。
- 符号引用:一组符号用来描述所引用的目标,可以是任何形式的字面量。
- 直接引用:可以是直接指向目标的指针、相对偏移量、间接定位的句柄。
- 类或者接口的解析:如果类或接口(C)不是数组类型,将符号引用(N)的全限定名传递给当前所在类(D)的类加载器去加载C;如果C是数组类型,且元素类型为对象,就按前者规则去加载,并由虚拟机生成一个代表此数组维度和元素的数组对象。
- 字段解析:先去解析字段所属的类或者接口的引用(C_class_info),然后按C字段本身、C实现的接口、C的父类的顺序去解析,如果没有,抛出NoSucdFieldError。如果找到引用,再进行字段权限认证,失败抛出IllegalAccessError。
- 类方法解析:前面方法和字段解析相同,类方法和接口方法符号引用的常量定义是分开的,如果在类方法表中发现C这个类是接口,直接报错。否则按类C本身、父类、实现的接口的顺序查找,找不到抛NoSuchMethodError。最后进行方法访问权限验证,,失败抛出IllegalAccessError。
- 接口方法解析:前面方法和字段解析相同,类方法和接口方法符号引用的常量定义是分开的,如果在接口方法表中发现C是接口,直接报错。否则按接口C本身、父接口的顺序查找,如果没找到,抛NoSuchMethodError;由于接口默认为public,不需要进行权限验证。
0x100 初始化阶段
初始化真正开始执行类定义的Java代码(字节码)。以下是clinit的一些细节。
- clinit是编译器自动收集该类的所有类变量和静态语句块合并生成的。
- 收集的顺序为源代码中出现的顺序。
- 静态语句块中
只能访问
定义在其之前的变量。 - 定义在静态语句块之后的类变量,
可以赋值,不能访问
。 - clinit
不需要
显式调用父类clinit,虚拟机会保证父类clinit优先于子类clinit(接口除外),第一个clinit必定为Object。 - 意味着父类中的静态语句块优先于子类类变量赋值操作。
- clinit不是必须的,如果类中无类变量且无静态语句块。
- 虚拟机保证类的clinit在多线程环境下会正确的加锁、同步。如果多个线程同时去初始化一个类,只有一个类会执行,其他线程必须阻塞等待,如果clinit耗时长,就可能造成多个进程长时间阻塞,较为隐蔽。
类加载器
通过一个类的全限定名去获取该类的二进制字节流。
- 比较两个类是否相等:由同一个类加载器加载的前提下才有意义。
双亲委派机制
- 启动器加载器(Bootstrap ClassClader)
- 扩展类加载器(Extension ClassLoader)
- 应用程序类加载器(Application ClassLoader),默认类加载器。
- 推荐重写findClass方法,而不是复写loadClass方法。