深入理解Java虚拟机Day4(虚拟机执行子系统)
7.4 类加载器
1 类与类加载器
每个都有个自己专门的类加载器。只要加载某两个类的类加载器不同,那这两个类就必定不相等。这里所指的“相等”,包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括了使用instanceof关键字做对象所属关系判定等各种情况。
2 双亲委派模型
图7-2中展示的各种类加载器之间的层次关系被称为类加载器的“双亲委派模型(Parents Delegation Model)”。双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。不过这里类加载器之间的父子关系一般不是以继承(Inheritance)的关系来实现的,而是通常使用组合关系来复用父加载器的代码。
JDK 9之前的Java应用都是由这三种类加载器互相配合来完成加载的。也称为双亲委派模型。双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
使用双亲委派模型来组织类加载器之间的关系,一个显而易见的好处就是Java中的类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都能够保证是同一个类。反之,如果没有使用双亲委派模型,都由各个类加载器自行去加载的话,如果用户自己也编写了一个名为java.lang.Object的类,并放在程序的ClassPath中,那系统中就会出现多个不同的Object类,Java类型体系中最基础的行为也就无从保证,应用程序将会变得一片混乱。
!!!!总结:注意上面的图,其实正常情况下,我们写的类都是由应用程序类加载器加载的,除非你自己写自定义类加载器。一个类只有一个类加载器。所以虚拟机中是允许出现多个同名的类,但是是由不同的类加载器加载的,所以这里必须是你手动写一个类加载器,也就是用你自己的方法加载,否则只用系统中的类加载器,是不能有多个同名的类的(系统加载一个,你加载一个)。使用双亲,是为了更好的分层,启动是启动类,扩展是扩展类,应用程序是应用程序类,并且启动类是最高级的,该有的层次感都很强。还有种的理解:这种设计有个好处是,如果有人想替换系统级别的类:String.java。篡改它的实现,但是在这种机制下这些系统的类已经被Bootstrap classLoader加载过了,所以并不会再去加载,从一定程度上防止了危险代码的植入。(这样就是,系统类早就加载了,你要加载就不行,就只能用你自己的类加载器,而你自己的又是子类,优先级不高,所以父类先加载,父类就把源码加载,这样你就改不了源码)
3 破坏双亲委派模型
历史上有3次破坏了。
第1次破坏,刚开始大家都是自己写加载器的,为了兼容这些部分,才破坏了。解决方式:为了兼容这些已有代码,只能在JDK 1.2之后的java.lang.ClassLoader中添加一个新的protected方法findClass(),并引导用户编写的类加载逻辑时尽可能去重写这个方法,而不是在loadClass()中编写代码。1.2之前都是重写loadClass(),后来就建议重写findClass()。
第2次破坏:双亲委派是有缺陷的,虽然双亲委派很好地解决了各个类加载器协作时基础类型的一致性问题(越基础的类由越上层的加载器进行加载),基础类型之所以被称为“基础”,是因为它们总是作为被用户代码继承、调用的API存在,但程序设计往往没有绝对不变的完美规则,如果有基础类型又要调用回用户的代码,就出问题了。(如JDBC.解决方法是弄一个上下文类加载器,父类去请求子类的加载器去加载,这样就破坏了双亲委派)
第3次破坏:是由于用户对程序动态性的追求而导致的。说白了就是希望Java应用程序能像我们的电脑外设那样,接上鼠标、U盘,不用重启机器就能立即使用,鼠标有问题或要升级就换个鼠标,不用关机也不用重启。对于个人电脑来说,重启一次其实没有什么大不了的,但对于一些生产系统来说,关机重启一次可能就要被列为生产事故。使用的技术是OSGi。
7.5 Java模块化系统
第8章 虚拟机字节码执行引擎
8.1 概述
8.2 运行时栈帧结构
每一个栈帧都包括了局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息。
1.局部变量表
一组用于存放方法参数和方法内定义的局部变量的内存区域。
注意:如果一个方法运行完后,这个表中的数据不会立即被回收,除非等到下一次复用,或者赋值为null。初始化时,没有为类变量赋值是没有问题的,会是一个默认值,但是局部变量必须赋值。
2.操作数栈
操作数栈(Operand Stack)也常被称为操作栈,它是一个后入先出(Last In First Out,LIFO)栈。
3 动态连接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。
4 方法返回地址
在方法退出之后,都必须返回到最初方法被调用时的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层主调方法的执行状态。
5 附加信息
8.3 方法调用(暂时没重点学)
方法调用不等于方法执行
1.解析
所有方法调用的目标方法在Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用,这种解析能够成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。换句话说,调用目标在程序代码写好、编译器进行编译那一刻就已经确定下来。这类方法的调用被称为解析(Resolution)。
2 分派
虚拟机在分派这一过程中,找到了对的方法。比如重载时,到底是谁,重写时,到底是哪个方法。
查看17道真题和解析