为了弄懂Retrofit源码,我把Java从底层撸了一遍

事情是这样的:

最近在研究Retrofit,相信读过它源码的朋友都知道,里面涉及了大量的反射和注解的调用,尤其是在请求建立的时候 ,使用了Java的动态代理方法,Proxy.nexInstance,由于之前在反射应用这块比较少,就本着打破砂锅问到底的态度查了一下反射的工作原理,为什么进去的时候是一个类,出来的时候就可以运行了?然后我又查到了,要了解反射的工作原理,及需要知道虚拟机类的加载机制,而类的加载机制当中,双亲委派模型和类加载机制的工作过程又是绕不过去的一环。

所以,我一咬牙,一跺脚,决定翻出蓝宝书——《深入理解Java虚拟机》,花了整个周末的时间,把Java的类加载机制进行了深入的研究,同时我还发现,类加载机制还被大量的应用在了Android的热修复领域,所以也就有了今天的这篇文章,希望能够通过我的分享,传播一些Java虚拟机类加载机制方面的知识,如果能帮助到大家,那真是我的荣幸了。

Retrofit源码解析部分我打算从这一篇文章开始分为几个部分,分别进行分享,来达到从表面看本质的效果,接下来,大家就期待我更多的作品问世吧

Java的类加载机制:

Java类加载机制流程:

加载——》验证——》准备——》解析——》初始化——》使用——》卸载

初始化阶段,虚拟机规范严格限定了只有5种情况必须立即对类进行“初始化”(加载、验证、准备要在此之前开始)。初始化一个类时,其静态代码区的代码只会被执行一次(包括创建的对象被回收后,再次创建的情形)

  1. 遇到new、getstatic、putstatic、invokestatic这4条字节码指令时候,如果类没有初始化,需要先对其进行初始化。典型场景是使用new创建对象时,读取或者设置类的静态字段、调用一个类的静态方法时。
  2. 使用java.lang.reflect包的时候对类进行反射调用时候,如果没有进行过初始化,则需要先触发其进行初始化,也就是说,我们调用了Proxy.newInstance的时候,其实对应的类已经初始化了。
  3. 当初始化一个类的时候,用户需要指定一个要执行的主类(即包含main方法的那个类),虚拟机会先初始化这个主类
  4. 当初始化一个类时,发现其父类还没有被初始化,那么就会先初始化其父类。
  5. 在使用JDK1.7时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果是REF_getStatic ,REF_putStatic,REF_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其进行初始化。

 

Java中类的加载过程:

在类的加载过程当中,JVM大致需要完成以下3步动作:

  1. 通过类的全限定名获取此类的二进制字节流
  2. 将这个类的二进制字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的Class对象,作为方法区这个类的各种数据存取的入口

 

而java最牛逼的地方就是在第一点,他没有规定这个类到底应该来自哪里,只要能够通过全限定名能定位到就可以,并且这个过程是放到虚拟机之外去进行的,也就是说,用户可以自由地实现类的加载方式,这就给我们留下了很多的想象空间。

举个例子:这个类就像一个满世界逛悠的人一样,只要我能定位到他,不管是通过手机GSM,4G,5G,哪怕是卫星通信,GPS。。。。。。只要能定位到他个人就可以!!!

然后,高潮来了!来自全世界各地脑洞大开的程序员可是把这一条玩出了花样,像在android领域的热修复技术,就是利用了这点,在类的加载阶段(可能是本地获取,也可能是通过网络获取)去改写类的加载方式,实现了热启动下的bug修复,可谓强悍!

在实际的应用过程当中,就是通过override一个类加载器的loadClass()方法。


连接阶段之1——验证阶段

验证阶段是连接阶段的第一步,也是非常重要的一步。其目的就是为了确保运行在虚拟机上的程序是符合虚拟机要求的,并且不会威胁到虚拟机安全。

验证阶段大致会依次完成以下四个动作:文件格式验证、元数据验证、字节码验证、符号引用验证

对比来说,使用纯粹的JAVA语言来编写的程序是安全的,因为Java本身是不允许数组跨边界访问、强制转换一个未定义的类型等等异常情况的。但我们知道,虚拟机上跑的源程序是可以由任何一个程序转化成的字节码文件转化而来,如果不对字节码文件进行检查的话,就可能对虚拟机造成严重的影响,甚至导致虚拟机的崩溃。

关于虚拟机的验证问题,由于检查规则太多,大家可以去参考《Java虚拟机规范(Java SE7)》。

对于虚拟机的类加载机制来说,验证阶段是一个非常重要,但非必须的过程,注意这句话!因为我们可以利用这点来优化虚拟机的类加载效率。

如果所运行的全部代码都已经被反复使用和验证过,那么在线上运行阶段或者实施阶段我们就可以考虑使用 -Xverify:none参数来关闭大部分的类验证措施,来缩短虚拟机的验证过程,进而提高类加载时间。

 

连接阶段之2——准备阶段

准备阶段是正式为变量分配内存并设置变量的初始值阶段,这些变量的内存分配是在方法区中进行的。

这块需要有两个比较重要的概念区分一下:

这时候进行内存分配的只包括static类型的变量,而不包括实例变量,实例变量将会在初始化的时候进行值分配。举个例子:

public static int a = 123;

这个a值在准备阶段过后的初始值为0而不是123.

但凡是都有例外,例如下面这句:

public static final int a = 123;

这句就是因为在类字段的字段属性中存在constant value属性,所以在准备阶段就会被赋值为123。

简单的说,如果有非final修饰的变量,赋虚拟机中的默认值。有final修饰的变量,则在准备阶段直接赋值。

 

连接阶段之3——解析阶段

类的解析过程简单的说就是将常量池内的符号引用替换为直接引用的过程。那么问题来了,什么是符号引用,什么是直接引用?

符号引用,简单的说就是用符号代表的引用目标,例如a = 50;其实是用a这个符号指向了存有50值的内存空间。那什么是直接引用呢?例如0xabc = 50;其中,0xabc指代的就是内存中的地址,只不过是在符号引用过程当中,使用a字符对内存地址进行了替换。

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符7类符号引用进行

 

初始化:

初始化过程是类加载过程的最后一个阶段,在前面的加载及初始化过程中,除了加载过程用户可以干预以外,其他的步骤都是由虚拟机来主导和控制的。

到了初始化阶段,才开始真正的执行类中定义的Java代码或者说开始执行程序员在类中定义的变量赋值。

这个过程是通过<clinit>()方法来进行处理的。

<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在程序当中出现的顺序所决定的,在静态语句块处理的过程当中,静态语句块只能访问定义在它之前的变量,定义在它之后的变量,可以进行赋值,但不能访问。

<clinit>()方法与构造函数不同,由于虚拟机的特性,虚拟机会保证子类的<clinit>()执行,之前,父类的<clinit>()已经被执行,所以通过这个规定我们可以猜到,第一个被执行<clinit>()语句的肯定是Object。

结合刚刚说到的两点,我们是不是可以得出,父类的静态语句执行肯定要先于子类的静态语句块执行这样的结论?

<clinit>()方法对于类或者接口来说不是必须的,因为如果一个类中没有静态语句块,也没有对变量的赋值操作,那么虚拟机也不会为这个类生成<clinit>()。但有一点需要注意的是,如果是接口有赋值的情况下,虚拟机仍然会正常生成<clinit>(),但不会执行其父类的<clinit>()。

最后,虚拟机内部会保证<clinit>()方法在多线程情景下的线程安全。

 

好了,经历了前面的加载、链接、初始化阶段之后,让我们回过头来再看看类的加载过程,我们知道,类的加载过程如果想要实现一些个性化操作的话是需要我们去override一个类加载器的loadClass方法的,那么我们如何能保证自己load出来的类和虚拟机正常加载出来的类一致呢?

可以说,类的一致性需要类加载器和类本身来一同确定,每一个类加载器都有一个独立的类名称空间。也就是说,两个类是否“相等”的前提是两个类由同一个类加载器进行加载的。

 

双亲委派模型:

从Java虚拟机的角度来看,类加载器主要分为两类:一种是启动 类加载器:Bootstrap Classloader,这个类加载器用c++语言实现,另一种就是所有除启动类加载器之外的类加载器,这些类加载器都由Java来实现,并且都继承自ClassLoader。

从Java开发者的角度来看,后一种类加载器又可以分为两种:

扩展类加载器(Extension ClassLoader):它由sun.misc.Launcher$ExtClassLoader实现,负责加载<JAVA_HOME>\lib\ext目录中的,或者是被java.ext.dirs系统变量指定的路径中的所有类库,并且由开发者可以直接扩展使用

应用程序类加载器(Application ClassLoader):它由sun.misc.Launcher$AppClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader方法的返回值,所以一般也称它为系统类加载器,来负责加载用户类路径classpath上所指定的类库,也是可以由开发者来使用的,并且在默认情况下,它也是默认的系统类加载器

我们的app在运行的时候都是这三种类加载器配合运行的,并且在必要的时候还可以自定义类加载器来加入到类加载过程当中,是时候祭出双亲委派模型的经典画面了

 

 

 

这个模型的工作过程是,如果一个子类的类加载器收到类加载请求,他首先会请求其父类的类加载器进行类加载,直到到了最顶层的类加载器不能处理的时候,他才会通知其子类进行类加载的处理,这样做的优势就是,Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如:Object这个类,根据双亲委派工作模型的运行原理,Objcet类的类加载器在各种类加载器环境中都是同一个类。反之,如果没有采用双亲委派模型来进行,处理,而是由用户来自行决定Object类的加载规则,那么就可能会导致一些未知异常的情况发生。

 

从上面博主的叙述中相信大家一定对类的加载机制和双亲委派机制有了一定的了解,那么我们应该如何去自己实现一个类加载器呢?

 

现在一般的做法是去主动override一个findClass()方法。等等,findClass()?不是应该去override loadClass方法吗?

 

是的,采用loadClass方法没有错,但是他破坏了双亲委派机制的工作链条,而采用findClass的优势就在于,他是工作于一种类似的兜底策略,如果在loadClass里面没有找到目标类的话,就会调用findClass去寻找它,这点,我们通过源码也可以看出来:

 

可以看到,通过loadClass方法传入了我们需要找到的类名,如果在loadClass方法所在的类及其父类中没有找到的话,就会去调用子类的findClass方法,这样既可以保留双亲委派机制,又可以一定程度的扩展,例如热部署、热修复等等。

 

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