类加载机制(卷一)

类加载机制。

加载时机

类的生命周期。其中3、4、5统称连接(Linking)。

1
2
3
4
5
6
7
8
9
10
11
st=>start: 开始
op1=>operation: 加载(Loading)
op2=>operation: 验证(Verification)
op3=>operation: 准备(Preparation)
op4=>operation: 解析(Resolution)
op5=>operation: 初始化(Initialization)
op6=>operation: 使用(Using)
op7=>operation: 卸载(Unloading)
e=>end: 结束

st->op1->op2->op3->op4->op5->op6->op7->e

有且只有以下5种情况,称为主动引用,虚拟机需要立即开始类的初始化阶段。

  1. 收到new、getstatic、putstatic、invokestatic指令时,如果该类未进行初始化则进行。
  2. 使用反射对类进行调用的时候,如果该类未进行初始化则进行。
  3. 对一个类进行初始化,如果父类未进行初始化,如果父类未进行初始化则进行。(如果是接口,并没有该要求。)
  4. 遇到main方法所在的那个类,先对该类进行初始化则进行。
  5. jdk1.7中的动态语言支持时。

而被动引用如通过数组定义引用类、子类引用父类静态字段都不会触发类的初始化。

类加载过程

类加载过程具体分为5个,分别为加载、验证、准备、解析、初始化。

0x00 加载阶段

  • 通过类的全限定名获取二进制字节流。
  • 该字节流的静态存储结构转换为运行时数据结构。
  • 在内存中生成该类的java.lang.Class对象,作为方法区中该类各数据的访问入口。存储在方法区中。

0x01 验证阶段

该阶段确保Class文件格式符合虚拟机要求,起到保护虚拟机自身安全。虽然Java语言本身相对安全,如果发生语法错误,大部分可以在编译阶段检测。但是由于Class文件来源可以是任意的,甚至可以直接使用十六进制编辑器直接编写,所以,需要该步骤保证本身安全。

大致分为以下四个检测内容:文件格式、元数据、字节码、符号引用。

  1. 文件格式:该检测在二进制字节流时进行,通过验证后,字节流才进入内存的方法区存储。
  2. 元数据:保证符合Java语言规范。如(所有类都有父类,除了Object;是否继承了final修饰的类等等)
  3. 字节码:确保程序语义是合法、符合逻辑的。jdk1.6之后添加StackMapTable表,只需要检测该表的属性是否合理即可。
  4. 符号引用:确保解析动作能正常完成。

0x10 准备阶段

准备阶段是为类变量(被static修饰)分配内存并设置类变量初始值(零值),使用的内存在方法区中申请。例如,public static int a = 123;在准备阶段只会设置为零值0,而在初始化阶段才会设置为123。在一些特殊情况下,如类字段中属性表含有ConstantValue,那么在准备阶段就会赋值为123,加final即可。

0x11 解析阶段

解析是将常量池中的符号引用替换为直接引用的过程。

  • 符号引用:一组符号用来描述所引用的目标,可以是任何形式的字面量。
  • 直接引用:可以是直接指向目标的指针、相对偏移量、间接定位的句柄。
  1. 类或者接口的解析:如果类或接口(C)不是数组类型,将符号引用(N)的全限定名传递给当前所在类(D)的类加载器去加载C;如果C是数组类型,且元素类型为对象,就按前者规则去加载,并由虚拟机生成一个代表此数组维度和元素的数组对象。
  2. 字段解析:先去解析字段所属的类或者接口的引用(C_class_info),然后按C字段本身、C实现的接口、C的父类的顺序去解析,如果没有,抛出NoSucdFieldError。如果找到引用,再进行字段权限认证,失败抛出IllegalAccessError。
  3. 类方法解析:前面方法和字段解析相同,类方法和接口方法符号引用的常量定义是分开的,如果在类方法表中发现C这个类是接口,直接报错。否则按类C本身、父类、实现的接口的顺序查找,找不到抛NoSuchMethodError。最后进行方法访问权限验证,,失败抛出IllegalAccessError。
  4. 接口方法解析:前面方法和字段解析相同,类方法和接口方法符号引用的常量定义是分开的,如果在接口方法表中发现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方法。