多个类加载器是如何协同工作的 ?
双亲委派模型双亲委托模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委托给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需要加载的类)时,子加载器才会尝试自己去加载。
使用双亲委托机制的好处是:能够有效确保一个类的全局唯一性,当程序中出现多个限定名相同的类时,类加载器在执行加载时,始终只会加载其中的某一个类。
运行时栈帧结构
局部变量表
操作数栈动态连结方法返回信息 局部变量表是一组变量值的存储空间,存储方法参数和方法内部定义的局部变量
局部变量表的容量以变量槽(Variable Slot,下称 Slot)为最小单位
为了尽可能节省栈帧空间,局部变量中的 Slot 是可以重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法体,如果当前字节码 PC 计数器的值已经超出了某个变量的作用域,那这个变量对应的 Slot 就可以交给其他变量使用
public static void main(String[] args) { byte[] placeholder = new byte[64 * 1024 * 1024]; System.gc(); }
在 System.gc() 运行后并没有回收这 64 MB 的内存。没有回收 placeholder 所占的内存能说得过去,因为在执行 System.gc() 时,变量 placeholder 还处于作用域之内
public static void main(String[] args) { { byte[] placeholder = new byte[64 * 1024 * 1024]; } System.gc();}
placeholder 的作用域被限制在花括号之内
但执行一下这段程序,会发现运行结果如下,还是有 64MB 的内存没有被回收
但在此之后,没有任何局部变量表的读写操作,placeholder 原本占用的 Slot 还没有被其他变量所复用,所以作为 GC Roots 一部分的局部变量表仍然保持着对它的关联。
public static void main(String[] args) { { byte[] placeholder = new byte[64 * 1024 * 1024]; } int a = 0; System.gc();}
内存得到回收
placeholder 能否被回收的根本原因是:局部变量中的 Slot 是否还存在关于 placeholder 数组对象的引用。
操作数栈
java虚拟机的解释执行引擎被称为“基于栈的执行引擎”,其中栈就是值得操作数栈
操作数栈也称为操作栈,通过标准的栈操作—— 压栈和出栈来访问的 操作数栈理论上是独立的,实际上是不完全独立的,栈和栈会进行数据的传递,会把局部变量表部分数据出现重叠。
动态连接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接
方法返回地址
方法调用时通过一个指向方法的指针指向方法的地址,方法返回时将回归到调用处,那个地址是返回地址。
方法调用,解析调用
调用目标在程序代码写好,方法在编译器进行编译时就必须确定下来。这类方法的调用称为解析。
符合这个要求的方法,主要包括:静态方法 和 私有方法。因为这两个方法的特点就决定了他们都不可能通过继承或别的方式重写其他版本
java虚拟机里面提供5条方法调用字节码的指令。
invokestatic:调用静态方法
invokespecial:调用<init>方法、私有方法和父类方法invokevirtual:调用所有的虚方法invokeinterface:调用接口方法,会在运行时在确定一个实现此接口的对象invokedynamic:会在运行时动态解析出调用电限定符所引用的方法,然后再执行该方法
只能被 invokestatic 和 invokespecial 调用的方法,都可以在解析阶段中确定唯一的调用版本,符合这个条件的有静态方法、私有方法、实例构造器、父类方法4类,这些方法称为非虚方法,由于 final 修饰的方法不能被覆盖,也属于非虚方法。与之相反,其他的方法称为虚方法。
1 public class Hello { 2 3 public static void sayHello(){ 4 5 System.out.println("hello"); 6 } 7 8 public static void main(String args[]){ 9 Hello.sayHello();10 }11 12 13 }
invokestatic //Method sayHello() 方法在编译期已经确定
静态分配调用
依赖于静态类型来定位方法执行版本的分派动作(如重载)称为静态分派。
静态分派的典型应用是方法重载
1 public class Demo { 2 3 public void sayHello(short a) { 4 System.out.println("short"); 5 } 6 7 public void sayHello(int a) { 8 System.out.println("int"); 9 }10 11 public void sayHello(long a) {12 System.out.println("long");13 }14 15 public void sayHello(char a) {16 System.out.println("char");17 }18 19 public void sayHello(Character a) {20 System.out.println("Character");21 }22 23 public void sayHello(Object a) {24 System.out.println("object");25 }26 27 public static void main(String[] args) {28 new Demo().sayHello('a');29 }30 31 //Console: char 当不能确定执行哪一个时候,会选择最为匹配的一个来执行32 33 }
动态分配调用
运行期根据实际类型确定方法执行版本的分派过程称为动态分派
1 public class Demo2 { 2 3 static class Parent { 4 public void sayHello() { 5 System.out.println("parent"); 6 } 7 } 8 9 static class Child1 extends Parent {10 public void sayHello() {11 System.out.println("child1");12 }13 }14 15 static class Child2 extends Parent {16 public void sayHello() {17 System.out.println("child2");18 }19 }20 21 public static void main(String[] args) {22 Parent p1 = new Child1();23 Parent p2 = new Child2();24 25 p1.sayHello();26 p2.sayHello();27 28 }29 30 31 } 32 //child1 child2
Invokevirtual 的执行过程
找到操作数栈顶的第一个元素所指向的对象的实际类型
如果在实际类型中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则直接返回这个方法的直接引用,查找过程的结束,如果不通过,抛出异常按照继承关系从下往上依次对实际类型的各父类进行搜索和验证如果始终没有找到,则抛出AbstractMethodError
动态类型语言支持
静态类型的语言在非运行阶段,变量的类型是可以确定的,也就是说变量是有类型的
动态类型语言在非运行阶段,变量的类型是无法确定的,也就是变量是没有类型的,但是值是有类型的,也就是运行期间可以确定变量值的类型
完