编译好的class文件如何被加载到jvm中?相信只要去钻研的小伙伴们都会产生这个疑问,下面我就来谈谈我个人的理解。
首先,官网上是分为这么几个步骤:加载、链接以及初始化。为了方便理解,我这里采用图解来描述。
装载(Load)
这个装载我们并不陌生,因为spring初始化bean之前也会存在这个过程。在装载里面其实主要完成以下三件事情:
- 通过一个类的全限定名获取定义此类的二进制字节流。
说白了就是按照类的全路径去查找该类,并且将类文件转化为二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
这里如果不了解运行时数据区的话,可能一下子没法理解。简单来说就是,类的元信息以及Java类里存在的静态变量或常量等信息都要保存在方法区里,所以需要转化为符合方法区的数据结构。
- 在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。
这里也需要对运行时数据区有了解,举个例子,Object obj=new
Object;这里真实的对象其实存放在Java堆中,而描述类的元信息存放在方法区(这个obj对象是什么类型、由谁创建等)。对象的对象头会有个指针指向方法区,以便随时获取类的元信息。
链接(Link)
从上图中可以看到,链接里又包含:验证、准备和解析。
验证
验证就是保证被加载类的正确性。比如,文件的格式、元数据、字节码、符号引用。
准备
为类的静态变量分配内存,并将其初始化为默认值。
举例:源码里存在 static int num=10;
这里为num分配空间,并且将值初始化为默认值,也就是num=0;
解析
把类中的符号引用转换为直接引用。class文件里都是采用符号描述的,这里需要将符合所代表的含义来转换成具体的操作。
举例:某些符合表示需要开辟一段内存空间,那么根据符合的含义,就需要真的来开辟一段内存空间。
初始化(Initialize)
对类的静态变量,静态代码块执行初始化操作。上面的准备阶段里已经介绍了需要给静态变量初始化默认值,而这里就需要将真实的数据来赋值给变量。
比如:static int num=10;
类加载器ClassLoader
在装载(Load)阶段,其中第1步:通过类的全限定名获取其定义的二进制字节流,需要借助类加载器完成。顾名思义,就是用来装载Class文件的。
但是关于类加载器并不是只有一个,为了分工明确,它会按照不同功能划分为以下四种。
每个类加载器会对应一个区域进行加载。
加载的原则是: 按照自底向上的顺序检查某个类是否已经加载,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个Classloader已经加载了,就表示该类已经被加载,其他Classloader不再继续加载此类。
思考: 4个类加载器已经分工了,为什么还要按照顺序一个个往上检查?
这样做主要是为了保证唯一性。这么来理解:我们比较两个类是否“相等”,只有保证这两个类是由同一个类加载器加载的前提下才有意义。否则,即使这两个类来源于同一个Class文件、被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。
再通俗点解释:同一个class文件,它既被App ClassLoader加载,又被Custom ClassLoader加载,那么我们去instantof的时候,其实是不相等的。而如果出现了这种情况,显然instantof就没有存在的必要了。
所以,我们按照自底向上,依次来判断是否类已经被加载,最终确保类只会被一个类加载器加载,保证它的唯一性。
双亲委派机制
其实上面的加载方式描述的就是一种双亲委派机制,下面来看下它的具体定义。
定义:如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就成功返回。只有父类加载器无法完成此加载任务时,才自己去加载。
优势:Java类随着加载它的类加载器一起具备了一种带有优先级的层次关系。比如,Java中的Object类,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object在各种类加载环境中都是同一个类。如果不采用双亲委派模型,由各个类加载器自己去加载的话,那么系统中会存在多种不同的Object类,这显然就不合理了。