深入java虚拟机读书笔记——类加载与方法调用中的分派机制

来源:互联网 发布:apache cxf 教程 编辑:程序博客网 时间:2024/05/21 18:36

类的生命周期

  • 加载->验证->准备->解析->初始化->卸载->使用
  • 验证+准备+解析统称为链接
  • 解析过程不确定何时开始,其它的开始顺序固定
  • 这只是开始顺序,不是执行完顺序

类的初始化前提

有且只有以下情况会进行类的初始化(请注意,是类的初始化,不是对象的初始化。)
1. 实例化对象,调用静态方法,读取或设置一个静态字段(非final),官方说法是,遭遇new,getstatic,putstatic,invokestatic这四个字节码指令时
2. 当初始化一个类时,发现其父类未初始化
3. java反射对类进行调用时
4. 含有main方法的类,虚拟机运行时要先初始化这个类
5. 当使用jdk1.7的动态语言支持时,java.lang.invoke.MethodHandle的实例解析出来的结果是REF_getStatic,REF_putStatic,REF_invokeStatic时,这个方法句柄所对应的类没有初始化时,要初始化这个类。(相当于就是MethodHandle解析出来是个静态方法,或者操作静态变量的方法的意思,关于MethodHandle的使用,随便找篇博客看看就了解了,我看的是这个:http://blog.csdn.net/aesop_wubo/article/details/48858931)

类加载过程

  • 加载:该过程最为简单,过程如下:

    1. 通过类的全限定名获得类的二进制字节流,注意,此处写的是二进制字节流,不一定就是从class文件中读取出的,只要是二进制字节流都可以,比如从jsp文件中生成字节流。
    2. 将字节流转换为可存储在方法区的数据结构
    3. 在内存中生成类的Class对象。
  • 验证:用于验证类的字节流是否合法,正如我们所知,类的字节流就是一个二进制数据,不一定非要有java编译器编译而成,而且人为也是可以修改类的字节码文件的,所以jvm虚拟机在加载过程中肯定要验证类的字节流是否合法,是否会对虚拟机造成坏的影响,具体内容我就不记录了,没啥意义,太多了。

  • 准备:为类变量(静态变量)分配内存并进行初始化

  • 解析:将常量池中的符号引用解析成直接引用,符号引用其实就是一个字面量,只要能无歧义地找到被引用的目标即可。符号引用引用的对象不一定加载到了内存中,直接引用引用的对象是一定加载到了内存中的。

四种常见解析情况

  • 接口或类的解析:当程序运行时,需要将一个未解析过的符号引用N解析成它所代表的类D的直接引用,有以下情况

    1. 当类D不是数组类型,就用类加载器去加载这个类,这个加载过程中就还要执行前面所说的验证等其它类加载动作,如果其中有一个步骤出错,那么解析失败。
    2. 当类D是数组类型,并且数组的元素是对象,则根据1加载元素类型的类,然后虚拟机再创建一个此类的数组类型
    3. 1或2成功,则虚拟机内已存在D,但还需严重程序对其的访问权限,无权则抛出java.lang.IllegalAccessError异常
  • 字段的解析

    1. 在类中能找到匹配的字段返回直接引用,结束解析
    2. 如果1为找到,且类继承了接口,从父接口中逐级寻找,找到了返回直接引用,结束解析
    3. 如果2未找到,且类有父类,非Object类,那么查找逐级查找父类,找到了返回直接引用,结束解析
    4. 3也未找到,抛出java.lang.NoSuchFieldError异常
  • 类方法解析(注意,类方法和接口方法的符号引用的常量类型是分开的)

    1. 在类中匹配方法
    2. 在父类中匹配方法
    3. 在类与父类实现的接口列表中匹配方法,匹配成功,说明类是抽象类,抛出java.lang.AbstractMethodError
    4. 否则,抛出java.lang.NoSuhcMethodError异常
  • 接口方法的解析(和上面的差不多,只不过判断是不是抽象方法的那一步不需要了)

  • 初始化:执行类构造器的clinit方法,clinit方法是虚拟机自动收集类中的静态块已经静态变量的赋值操作生成的,所以一个类可以没有clinit方法

双亲委派模式

概述:类在虚拟机中的唯一性是靠类与加载类的类加载器共同确定的,同样的类被不同的类加载器加载,表现在虚拟机里就会是两个不同的类。
双亲委派模式就是为了保证java类的唯一性,简单来说就是类加载器收到加载类的请求时,均首先将其交给父加载器处理(注意,除了最顶层的Boostrap ClassLoader,所有的类加载器都应该有其父加载器,而且父加载器一般由组合来实现,而非继承),直到请求到Boostrap ClassLoader为止,如果附加载器无法处理,子加载器才会尝试自己进行加载。

方法的分派

静态分派:先看示例代码:

static void t(A obj){System.out.println("A");}static void t(B obj){System.out.println("B")}B extends A;A obj = new B();t(obj);

输出是B,这就是java的重载,依赖的就是静态分派,

A obj = new B();

中A是静态类型,B是动态类型,静态类型在编译期可以确定,动态类型需要在运行时确定,java重载是依靠静态类型来定位方法的,所有依靠静态类型来定位方法的执行版本的分派动作属于静态分派。当然,编译器会根据语法来推断最为合适的方法版本

void test(char c);void test(int c);

当我调用test(‘q’)时,会调用参数类型为char的test方法,如果我注释掉参数类型为char的test方法,那么会调用int型,这是因为发生了自动类型转换。
动态分派:同样也是先看示例代码:

class A{  void t(){System.out.println("A");}}class B extends A{  @Override  void t(){System.out.println("B");}}A obj = new A();A obj1 = new B();obj.t();obj.t();

结果是输出

AB

在书中,作者通过阅读字节码发现,在obj与obj1调用t方法时,指令与参数是完全一样的,但是调用方法的invokevirtual指令执行时是按照以下步骤来的

  1. 找到操作数栈顶的第一个元素所指的对象的实际类型
  2. 如果在实际类型中找到了与常量中的描述符以及简单名称都相同的方法,就进行访问权检查,通过就返回方法的直接引用,结束查找,不通过则抛出java.lang.IllegalAccessError的异常
  3. 在实际类型中没找到,就从去父类找,并且重复2
  4. 在最顶层父类中都没有找到,抛出java.lang.AbstractMethodError异常

这种通过实际类型来确认方法的分配叫动态分派

阅读全文
0 0
原创粉丝点击