JVM类加载机制-类加载过程

过程

cmd-markdown-logo


加载

  • 根据全限定名获取类的二进制字节流,从jar,war,ear等格式文件读取
  • 字节流转为方法去的运行时数据结构
  • 生成class对象,用此对象打开方法区的入口

验证

  • 现在所有的class文件(这里指的是引用,还没有到实际的对象创建)都已经转为数据结构,存放在方法区
  • 如果去掉验证这个步骤考虑下结果:随便写了一个java文件,内容可以是123456,这种数据对于JVM完全不认识,所以这个验证就是来看文件的内容是否是JVM认可的,就像web页面中的注册登录一样,如果没有验证,账号密码都不用输入就可以注册成功,那数据库岂不是混乱了
文件格式验证

class文件其实是一个16进制文件

cmd-markdown-logo

  1. 文件的开头就是“魔数”,用来标记这是一个jvm认可的文件,魔数的值一般是0xCAFEBABE
  2. 而后是主,次版本号是否可以被当前jvm处理,如果环境上是jdk1.6,而自己的ide是1.8编译,那jar启动时会报错
  3. 然后是方法区的引用,例如:类型是class的”org/fenixsoft/clazz/TestClass” 那当前类就是TestClass,如果在当前的class文件中还有类似的引用,/org/fenixsoft/clazz/TestClassTwo,那在整个验证环节*必须要找到一个类名叫做TestClassTwo的引用加载进方法区,不然throw ClassNotFoundeClass,更细致的可以使用javap工具查看class字节码的内容,文件中会明确区分引用是class还是引用other class还是method
  4. 常量池的常量是否被支持
元数据验证
  1. 类是否是父类
  2. 是否继承了不允许继承的类 (final)
  3. 非抽象类,是否实现了父类和接口所有的方法
  4. 字段,方法是否和父类矛盾(覆盖父类final, 非法重载)

总结一下上面的两个步骤:

文件格式验证就是验证文件是不是JVM认识的文件,把所有的引用写入到方法区。

元数据验证就是看看方法区的引用是不是有语法错误,这里是引用只负责方法区层面(类,常量)的验证
字节码验证

这里验证的是方法体的内容
  1. 验证方法体内部的数据类型是否正常,如(int num = 0L这样的错误)
  2. 保证不会跳转到方法体以外
  3. 保证类型转换是否正常 (里氏替换原则)

ps:

由于方法体的庞大,1.6之后增加了StackMapTable属性来提高整体效率。

简单理解下StackMapTable:如果没有StackMapTable那JVM每次需要检查每一行代码,那把这个过程放在java编译成class的阶段,用不同的类型来计数,把这个字节码的验证前置,jvm实际验证的时候只看计数的结果,这样来提升效率

符号引用验证
  1. 文件格式验证阶段有一个*,说的是所有验证阶段结束后看引用是否存在。符号引用验证就是验证阶段的最后,他会做所有引用的匹配,[引用 <-> 类, 引用 <-> 方法, 引用 <-> 字段]
  2. 检查访问性,private, protected, public, default是否可以被访问


总结四个步骤:

文件格式验证:验证class文件是否是jvm可识别的文件 元数据验证:验证方法区层面的语法 (类,常量)
字节码验证:验证方法体内部的语法 (类型转换) 符号引用验证:验证引用是否存在,访问性

四个阶段全部通过则该文件可以进行分配内存。

准备

准备阶段是正式为“类变量”分配内存并设置初始值的阶段。

1
2
3
4
5
public static int value = 123;
//在这个阶段value = 0
public static final int value = 123;
//在这个阶段value = 123
//javac 赋值给value的123

除了final之外,所有的类型都会给初始值

1
2
3
reference = null
boolean = false
int = 0


解析

解析之前先了解两个词:

  • 符号引用:刚才在”文件格式验证”阶段,我们得到了类中的所有信息(类似org/abc/Test),使用的词是“引用”,它无法跟内存交互,只是用来相互定位类,方法,常亮等信息;这个“引用”就是符号引用。
  • 直接引用:既然符号引用只是来互相定位,那么直接引用就是把定位的信息,真正和内存挂钩,指向目标的指针,相对偏移量,句柄。

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

解析其实是另一种验证,验证阶段是建立在符号引用的表面验证;而解析是符号引用转换为直接引用过程中的二次验证


初始化

  • 初始化是真正一行一行走代码,来分配内存,走static代码块
  • 初始化阶段是执行类构造器的阶段 <clinit>()
  • <clinit>()是自动收集类中的所有类变量和赋值动作和静态代码块

参考

文章参考 《深入理解Java虚拟机》