深入理解java虚拟机---虚拟机类加载机制

来源:互联网 发布:访问通用顶级域名 编辑:程序博客网 时间:2024/06/06 14:24

1 类加载的时机

1.1 类的生命周期

注:加载,验证,准备,初始化和卸载这5个阶段的顺序是确定的,而解析阶段则不一定,它在某些情况可以在初始化阶段之后。

1.2 必须对类进行初始化的5种情况:

  • 遇到new,getstatic,putstatic,invokestatic这4条字节码指令时
  • 使用java.lang.reflect包的方法对类进行反射调用时
  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化
  • 当虚拟机启动时,用户需要指定一个main类,虚拟机会先初始化这个类
  • 使用jdk1.7的动态语言支持时,如果java.lang.invoke.MethodHandle实例最后的解析结果是REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄

1.3 被动引用

被动引用是指除了上面的其他引用类的方式,这些方式不会触发其初始化。

例子1:

/** * 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量类的初始化 */class ConstClass{static{System.out.println("ConstClass init");}public static final String HELLOWORD = "hello world" ; }public class NotInitialization{public static void main(String[] args) {System.out.println(ConstClass.HELLOWORD);}}
输出
hello world

通过javap得到其对应的class文件为下图所示:

例子2:

/** * 通过子类引用父类的静态变量,不会导致子类初始化 */class SuperClass{static{System.out.println("super Class");}public static int value = 123 ; }class SubClass extends SuperClass{static{System.out.println("sub class");}}public class NotInitialization {public static void main(String[] args){System.out.println(SubClass.value);}}

2 类加载过程

2.1 加载

加载阶段虚拟机需要完成的事情

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

获取二进制字节流的方式

  • 从ZIP包中获取,发展为JAR包和WAR包
  • 从网络中获取,典型应用为Applet
  • 运行时计算生成,最典型应用为动态代理技术
  • 由其他文件生成,典型应用是jsp
  • 从数据库中读取

非数组类的加载阶段可以使用系统提供的引导类来完成,也可以有用户自定义的类加载器来完成(即重新一个类加载器的loadClass()方法)。
数组类本身不通过类加载器创建,而是有java虚拟机直接创建。


2.2 验证

2.2.1 文件格式验证

  • 是否已魔数0xCAFEBABE开头
  • 主,次版本号是否在当前虚拟机处理范围之内
  • 常量池的常量中是否有不被支持的常量类型(检查常量的tag标志)
  • 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
  • CONSTANT_UTF8_info型常量中是否有不符合UTF8编码的数据
  • class文件各个部分及文件本身是否有被删除或附加的其他信息

2.2.2 元数据验证

  • 这个类是否有父类
  • 这个类的父类是否继承了不允许继承的类(final修饰的类)
  • 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
  • 类中的字段,方法是否与父类矛盾(r例如覆盖了父类的final字段,或者出现不符合规则的方法重载)

2.2.3 字节码验证

  • 保证任何时刻操作数栈的数据类型与指令代码序列都能配合工作(例如不会出现如在操作数栈中放置了int类型数据,却以long类型加载到本地变量表中)
  • 保证跳转指令不会跳转到方法体以外的字节码指令上
  • 保证方法体中的类型转换时有效的

2.2.4 符号引用验证

  • 符号引用中通过字符串描述的全限定名是否能找到对应的类
  • 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段
  • 符号引用中的类,字段,方法的访问性(private , public , protected , default)是否可以被当前类访问

2.3 准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量使用的内存都将在方法区中分配,在准备阶段分配的变量为类变量

注:此时并没有执行任何方法,如public static int value = 123 ; 在此时value的值为0而非123。而对于public static final int value = 123 ; 此时的值为123


2.4 解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程

2.4.1 类或接口的解析

假设当前代码所处的类为D,如果要把一个从未解析过的符号引用N解析为一个类或接口C的直接引用。如果C不是数组类型,那虚拟机会把代表N的全限定名直接传递给D的类加载器加载这个类C。如果C是数组类型,那么和上面一样的规则生成数组元素,然后由虚拟机生成数组对象。最后检测一下D是否对C有权限。


2.4.2 字段解析

解析未被解析过的字段符号引用,首先解析其对应class_index下的CONSTANT_Class_info字段中的类或接口引用进行解析,然后依次通过其本身,接口,类等关系从下往上搜索出简单名称和字段描述符都与目标匹配的对应项。然后进行权限验证


2.4.3 类方法解析

类方法解析先解析出类方法表中class_index多属的类或接口的符号引用,然后就是递归寻找其对应的简单名称和描述符相匹配的方法,然后验证权限。


2.4.4 接口方法解析

和类方法差不多,只是不需要验证权限


2.5 初始化

初始化阶段实际是执行类构造器<cinit>()方法的阶段

  • <cinit>()方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的。其中的顺序和源文件的顺序相同。
  • 虚拟机保证<cinit>()方法执行之前,其父类的<cinit>()方法已经执行
  • 虚拟机保证<cinit>()方法在多线程执行时能被正确地加锁,同步
  • <cinit>()方法不是必须的
  • 接口中<cinit>()方法在执行时不会调用父接口的<cinit>()方法,只有父类的变量被调用了才会被初始化

注:静态语句块只能访问其前面的静态变量,对于其后的变量可以赋值但不能访问

2.3 类与类加载器

比较两个类是否"相等",只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的加载器不同,那这两个类就不定不相等。

2.3.1 类加载器的分类

  • 启动类加载器(Bootstrap ClassLoader):这个加载器负责将<JAVA_HOME<\lib目录下,或者被-Xbootclasspath参数指定的路径中的,并且虚拟机识别的类库加载到内存中。
  • 扩展类加载器(Extension ClassLoader):这个加载器负责加载<JAVA_HOME>\lib\ext目录或者被java.ext.dirs系统变量指定的路径中的所有类库
  • 应用程序类加载器(Application ClassLoader):这个类加载器负责加载用户类路径(ClassPath)上指定的类库。这个加载器是getSystemClassLoader()方法的返回值

2.3.2 双亲委派模型

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有加载请求都应该传送到顶层的启动类加载器中,只有父类无法处理请求时,子加载类才会尝试自己加载。

参考

<<深入理解java虚拟机>>
0 0
原创粉丝点击