-
类加载
1.1 加载:加载二进制流至内存中,创建Class对象
1.2 链接
验证:保证所加载文件的正确性。
准备:为类中定义的变量分配内存并设置类变量初始值
解析:将符号引用替换为直接引用,该过程一般在初始化之后
1.3 初始化:执行类构造器方法,就是执行类中的静态代码块与静态变量的赋值按照用户编码的顺序,(静态代码块中可以对下面的类变量进行赋值,但是不能输出)
关于 <init>方法和 <client>方法 <client>方法中存放的是当前类中类变量与类实例代码块,按用户编写的顺序存放,在执行类加载初始化阶段的时候会执行该方法,需要注意的是在静态代码块中可以向前引用,即可引用该代码块下面的变量进行赋值,因为在准备阶段以及为类型变量进行赋值说明以及存在了,所以可以进行赋值,但是不能进行访问。<init>方法是在构建对象完成之后执行的方法,其中包含类的构造函数以及实例代码块,实例变量,和实例代码块,实例变量和实例代码块按用户编写顺序,而构造函数在两者之后,同样在实例代码块中可以进行向前引用,在对象被创建之后分配内存,也有一个初始化阶段赋值为默认值。
2 关于方法调用
解析调用:
是一个静态的过程在方法编译期间就可以确定,在解析阶段,指定调用方法的版本(即调用哪个方法),主要有:静态方法,私有方法,等,符号引用直接转换为直接引用。不会延迟到运行期间才调用
分派调用:
分派是多态性的体现,可以分为静态分派和动态分派。重载属于静态分派,而重写则是动态分派
静态分派:
静态分派只会涉及重载(Oveload),而重载是在编译期间确定的,那么静态分派自然是一个静态的过程(因为还没有涉及到Java虚拟机)。静态分派的最直接的解释是在重载的时候是通过参数的静态类型而不是实际类型作为判断依据的。因此在编译阶段,Javac编译器会根据参数的静态类型决定使用哪个重载版本。
动态分派:
动态分派的一个最直接的例子是重写(Override)。对于重写,我们已经很熟悉了,那么Java虚拟机是如何在程序运行期间确定方法的执行版本的呢?
解释这个现象,就不得不涉及Java虚拟机的invokevirtual指令了,这个指令的解析过程有助于我们更深刻理解重写的本质。该指令的具体解析过程如下:
找到操作数栈栈顶的第一个元素所指向的对象的实际类型,记为C
如果在类型C中找到与常量中描述符和简单名称都相符的方法,则进行访问权限的校验,如果通过则返回这个方法的直接引用,查找结束;如果不通过,则返回非法访问异常
如果在类型C中没有找到,则按照继承关系从下到上依次对C的各个父类进行第2步的搜索和验证过程
如果始终没有找到合适的方法,则抛出抽象方法错误的异常
从这个过程可以发现,在第一步的时候就在运行期确定接收对象(执行方法的所有者称为接受者)的实际类型,所以当调用invokevirtual指令就会把运行时常量池中符号引用解析为不同的直接引用,这就是方法重写的本质。
虚拟机动态分派的实现
上面的叙述已经把虚拟机重写与重载的本质讲清楚了,那么Java虚拟机是如何做到这点的呢?
由于动态分派是非常频繁的操作,实际实现中不可能真正如此实现。Java虚拟机是通过“稳定优化”的手段——在方法区中建立一个虚方法表(Virtual Method Table),通过使用方法表的索引来代替元数据查找以提高性能。虚方法表中存放着各个方法的实际入口地址(由于Java虚拟机自己建立并维护的方法表,所以没有必要使用符号引用,那不是跟自己过不去嘛),如果子类没有覆盖父类的方法,那么子类的虚方法表里面的地址入口与父类是一致的;如果重写父类的方法,那么子类的方法表的地址将会替换为子类实现版本的地址。
方法表是在类加载的连接阶段(验证、准备、解析)进行初始化,准备了子类的初始化值后,虚拟机会把该类的虚方法表也进行初始化。
类加载器
启动类加载器 :C++编写
扩展类加载器
应用程序类加载器
双亲委派机制:类加载器之间的关系是一种组合关系,在加载类的时候先交给父类去加载,如果父类加载不到则自己加载。
也可以自定义类加载器继续Classloder类重写 findclass方法/loadclass方法
破坏双亲委派机制:因为如果当启动类加载器加载的类需要扩展类加载器加载的类做支持的时候,就无法加载,所以解决方案就是线程上下文类加载器。