前言
前面学习了虚拟机的内存结构、对象的分配和创建,但对象所对应的类是怎么加载到虚拟机中来的呢?加载过程中需要做些什么?什么是双亲委派机制以及为什么要打破双亲委派机制?
类的生命周期
类的生命周期包含了如上的7个阶段,其中验证、准备、解析统称为连接 ,类的加载主要是前五个阶段,每个阶段基本上保持如上顺序开始(仅仅是开始,实际上执行是交叉混合的),只有解析阶段不一定,在初始化后也有可能才开始执行解析,这是为了支持动态语言。
加载
加载就是将字节码的二进制流转化为方法区的运行时数据结构,并生成类所对象的Class对象,字节码二进制流可以是我们编译后的class文件,也可以从网络中获取,或者运行时动态生成(动态代理)等等。
那什么时候会触发类加载呢?这个在虚拟机规范中没有明确定义,只是规定了何时需要执行初始化(稍后详细分析)。
验证
这个阶段很好理解,就是进行必要的校验,确保加载到内存中的字节码是符合要求的,主要包含以下四个校验步骤(了解即可):
- 文件格式校验:这个阶段要校验的东西非常多,主要的有下面这些(实际上远远不止)
- 是否以魔数0xCAFEBABE开头。
- 主、次版本号是否在当前Java虚拟机接受范围之内。
- 常量池的常量中是否有不被支持的常量类型(检查常量tag标志)。
- 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。
- CONSTANT_Utf8_info型的常量中是否有不符合UTF-8编码的数据。
- Class文件中各个部分及文件本身是否有被删除的或附加的其他信息。
- 。。。。。。
- 元数据校验:对字节码描述信息进行语义分析。
- 这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)。
- 这个类的父类是否继承了不允许被继承的类(被final修饰的类)。
- 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法。
- 类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等)。
- 。。。。。。
- 字节码校验:确保程序没有语法和逻辑错误,这是整个验证阶段最复杂的一个步骤。
- 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似于“在操作栈放置了一个 int 类型的数据,使用时却按 long 类型来加载入本地变量表中”这样的情况。
- 保证任何跳转指令都不会跳转到方法体以外的字节码指令上。
- 保证方法体中的类型转换总是有效的,例如可以把-个子类对象赋值给父类数据类型,这是安全的,但是把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系、完全不相干的一个数据类型,则是危险和不合法的。
- 。。。。。。
- 符号引用验证:这个阶段发生在符号引用转为直接引用的时候,即实际上是在解析阶段中进行的。
- 符号引用中通过字符串描述的全限定名是否能找到对应的类。
- 在指定类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段。
- 符号引用中的类、字段、方法的可访问性( private、 protected. public、 )。
- 是否可被当前类访问。
- 。。。。。。
准备
该阶段是为类变量(static)分配内存并设置零值,即类只要经过准备阶段其中的静态变量就是可使用的了,但此时类变量的值还不是我们想要的值,需要经过初始化阶段才会将我们希望的值赋值给对应的静态变量。
解析
解析就是将常量池中的符号引用替换为直接引用的过程。符号引用就是一个代号,比如我们的名字,而这里可以理解为就是类的完全限定名;直接引用则是对应的具体的人、物,这里就是指目标的内存地址。为什么需要符号引用呢?因为类在加载到内存之前还没有分配内存地址,因此必然需要一个东西指代它。这个阶段包含了类或接口的解析、字段解析、类方法解析、接口方法解析,在解析的过程中可能会抛出以下异常:
- java.lang.NoSuchFieldError:找不到字段
- java.lang.IllegalAccessError:不具有访问权限
- java.lang.NoSuchMethodError:找不到方法