老马的JVM笔记(四)----虚拟机类加载机制

Class文件现在是有了,JVM怎么根据Class文件把内容加载出来?接口或类都要在这里被加载。

4.1 类加载时机

(!)类的生命周期:

+-----------------------------------------------------------------------------------------------------------------------------+

|  加载(loading) -->  验证(verification) --> 准备(preparation) --> 解析(resolution) -->   |

|  初始化(initialization) --> 使用(using) --> 卸载(unloading)                                               | 

+-----------------------------------------------------------------------------------------------------------------------------+

4.1.1 加载

在遇到new,getstatic,putstatic,invokestatic指令时,如果该类没有被初始化过,那就先初始化。final修饰的变量会被放在常量池,这个不重要。

当触发反射机制,调用reflect包时,也要将该类初始化。

在初始化一个类时,如果其父类没有被初始化过,初始化。这回扯出来一个家谱,那就都初始化。且最先初始化辈分大的。但接口不然,用到哪个祖宗加载哪个祖宗,没用的不加载。

在虚拟机启动时最先初始化包含main方法的类,主类。

Methodhandler实例最后的解析结果为REF_getStatic...等方法句柄,并且这个方法句柄对应的类没有被初始化,需要先触发它的初始化。(*)

除以上之外,就算“用”了该类,也不会加载他。比如其实是访问其父类属性,没有显示new一个该类的对象,那只会加载父类,不会加载该类。

加载阶段,虚拟机通过类的权限定名获取类的字节流,将字节流的静态存储结构转化为方法区的运行时数据结构,最后在内存中生成一个Class对象用于访问该类。这是说加载类,不是加载对象。是在可能用到什么类时,获取内容好可以使用其中的属性和方法。加载数组类就是一层层扒下来,直到把到可用的引用类型或基本变量类型。

4.1.2 验证

主要验证Class文件中的信息是否符合虚拟机的要求。正常Java语言啥问题没有,但是Class文件是编译出来的,会有问题。都验证啥?

1.文件格式:0xCAFEBABE,版本号,常量池中常量类型是不是都被虚拟机支持...很多。反正是虚拟机查不是你查。

2.元数据:查父类,家谱上是否有不允许继承的类,是否实现了接口所需要的所有方法,是否与长辈们有字段矛盾冲突...

3.字节码:检查字节码的数据流,控制流,用于确认语义的合法合理性。包含的方面很多,控制流是否跳转到方法外,类型是否合逻辑。这要求的东西很多,而且涉及到path coverage问题,不可能全走完。所以虚拟机提供了StackMapTable属性,用于记录一些“会安全”的代码块来节省时间。

4.符号引用:验证引用该类以外的各种信息的引用。是否能访问到对应的类,在该类中是否存在想要访问的方法和变量,该类中想访问的变量与方法是否可以被访问...

4.1.3 准备

准备阶段需要给类中的变量开辟内存,并且给变量初步赋初始值。初步赋值的意思是先让值占个位,都用0或null或其他占上,而不是赋给他字面量。

4.1.4 解析

将符号引用替换成直接引用。

符号引用(Symbolic References):标记要引用的目标,用什么都可以,只要能标识到目标就可以,暂时不需要把引用目标加载到内存中。

直接引用(Direct References):直接指向引用的目标,已经在内存中存在。

解析目标:类或接口,字段,类的方法,接口的方法,方法类型,方法句柄,调用点限定符。

1.类或接口:非数组,即引用类的加载器加载被引用类(A类中B引用C类,A加载器加载C)。与前文类似,加载的时候要加载一个家谱,每个都要成功才可以。如果是数组,就一层层扒,扒到基本类型或可以直接用的。

2.字段:先解析该字段所在的类或接口C。如果C中直接有目标字段,直接返回直接引用;如果没有就在实现的接口里一个一个找,接口再一辈一辈找,直到找到目标字段;如果找不到就开始查家谱,从父类开始一辈辈地向上查找;如果再没有,那就是真没有,失败返回。找到了,如果不能访问,也失败返回。如果同一字段同时出现在接口和父类中出现,或者同时在多个接口中出现,拒绝编译。

3.类方法:思路不同。先解析该类或接口C。如果在类方法表中发现C是接口,直接抛异常(毕竟是类方法解析);如果“类"C中存在目标方法,返回直接引用;如果没有,就先找家谱,查到有目标方法,返回直接引用;如果家谱里没有,去接口里找,接口里也是一辈一辈找;再没有那就是真没有,返回失败。

4.接口方法:又一个思路。先解析该类或接口C。为什么总说类或接口,不能直接分开呢?那肯定是不能了。class_index里类和接口放一起了。如果解析出是类,报错;如果在C接口中找到目标方法,返回直接引用;如果没有,如父接口中找,一辈一辈找;如果再没有就失败返回。接口里的方法一定是public的,权限一定没问题。

4.1.5 初始化

这是正经变量初始化。在准备阶段,我们已经给变量开辟了空间,所以在这里其实已经有了给变量的内存和初始赋值,所以即使是在声明变量前发生了变量赋值,也是合理的,前提是要发生在静态块中,因为类会先编译静态区(static{})。但这种不合常识的行为,只有赋值,不能访问。当然这种静态块用得也不多。

4.2 类加载器

自写类加载器,就是用java语言自己代替jvm的“根据全限定名找到对应类的字节流”。两个类相同的前提就是要由同一个加载器加载出来。Java虚拟机支持两种类加载器,一种是启动类加载器,Bootstrap ClassLoader,是虚拟机自己的加载器,用C++实现。另一种就是自己用Java实现的类加载器,需要继承java.lang.ClassLoader。

细致一点,Java程序使用到的类加载器有三种:启动类加载器,扩展类加载器(Extension ClassLoader),应用程序类加载器(Application ClassLoader)(程序中的默认类加载器)。如果自己实现类加载器,自定义的类加载器需要依赖应用程序类加载器->扩展类加载器->启动类加载器。这里的依赖不算继承,算组合(composition),复用父类加载器的代码。这个链条关系叫双亲委派模型(Parents Delegation Model)。意思就是我不行,我得找我爹。在加载一个类的时候,加载器会层层找上级,直到找到启动类加载器。祖宗说不行,那就向下查找,直到找到一个能加载该类的长辈。这样的好处是如果一个类很多加载器都可以加载,我们可以保证他被最祖宗的一个加载出来,不会产生歧义。这是一个规定,不是什么死要求,不这样也不是不行。所以这样的问题也就是,如果我想写一个基础类的加载器,因为太基础了,所以我的加载器根本没法被使用。一些已有框架就想办法突破了这种规定。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章