一个类加载的过程主要分为类文件——加载——验证——准备——解析——初始化五个过程。那么下面就讲解一下jvm当中类的加载的内幕解析。
类文件实际上就是后缀为.class二进制文件。jvm在加载过程中主要是完成三件事
1)通过一个类的全限定名——包名加类名来获取定义此类的二进制字节流。
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3)JVM内存中生成一个代表该类的java.Lang.Claa对象,作为方法区这个类的各种数据的访问入口。
对于第一点,并没有要求这个二进制的字节流是通过一个什么样的方式来从Class文件中获取的。
主要比较常见的class文件的读取方式:
1)从ZIP包中读取class文件,流行的SpringBoot/SpringCloud框架基本都打包成Jar形式,内嵌了Tomcat,俗称Fat jar,通过java -jar可以直接启动。
2)运行中生成的Class文件,应用最多的就是动态代理技术了,比如CGLIB、JDK动态代理。
那么这个Class文件究竟是有谁加载的呢?正是类加载器。对于任意一个类,如果确定在JVM中的唯一性呢?必须由类加载器和类本身一起确定其唯一性。每一个类加载器都有一个独立的类名称空间。通俗的说:比较两个类是否相等只有在同一个类加载器的前提下才有意义,否则不管两个类是不是来源于同一个Class文件,只要类加载器不一样,那么两个类必定不相等。那么常见的类加载器都有什么呢?我们从双亲委派机制上来分析。结合上图中的加载器之间的层次关系就叫做双亲委派模型。其中Bootstrap ClassLoader用来加载核心类库当中的加载java最核心的库java.*了。ExtClassLoader:Java编写,用来加载扩展库javax. * 和自己所编写的jar包。AppClassLoader:Java编写,加载程序所在目录。自定义ClassLoader:Java编写,定制化进行加载,主要是通过findclass和defineclass这两个方法实现的。
双亲委派模型呢,就要求除了BootStrap ClassLoader外,都需要有对应的父类加载器,不过这些加载器之间的关系一般不是以继承的关系来实现的,通常使用的是组合关系来复用父加载器的代码。
那么双亲委派模型的过程是什么呢?
1)当应用类加载器收到了类加载的请求,会把这个请求委派给它的父类(扩展)加载器完成。
2)当扩展类加载器收到了类加载的请求后,会把这个请求委派给它的父类(引导类)加载器区完成。
3)引导类加载器收到类加载的请求,查找下自己的特定库是否能加载该类,即在rt.jar、tools.jar...包中的类。发现不能呀!返回给扩展类加载器结果。
4)扩展类加载器收到返回结果,查找下自己的扩展目录下是否能加载该类,发现不能啊!返回给应用类加载器结果。
5)应用类加载器收到结果,额!都没有加载成功,那只能自己加载这个类了,发现在ClassPath中找到了,加载成功。
所以一个JVM当中的类加载器都是分层次的,有相应的父子关系。也有三个非常核心的方法,loadclass,findclass,defineclass。其中只有loadclass的方法类型是public的,所以它才是对外服务的接口。其实这个方法就是一个递归循环调用,去判断哪一个类加载器可以加载此类。findclass就是找到.class文件,将其读取到内存中得到byte【】字节码数组,之后通过defineClaas()调用本地方法解析成一个Class对象,本地方法就是由c语言实现的方法,Java自然就是通过JNI机制调用。
讲完类加载机制之后,我们来看一下Tomcat是如何打破双亲委派模型的。
因为classloder是一个抽象类,所以如果你自定义类加载器可以重写findClass()方法,重写findClass()方法还是会按照既定的双亲委派机制运作的。同样的,又因为loadClass()方法是public,所以是允许重写的,Tomcat就是应用了WebAppClassLoader类加载器打破了双亲委派模型。
WebAppClassLoader 类加载器具体实现是重写了 ClassLoader 的两个方法:loadClass() 和 findClass()。其大致工作过程:首先类加载器自己尝试去加载某个类,如果找不到再委托代理给父类加载器,其目的是优先加载 Web 应用自己定义的类。
这也正是一个Tomcat能够部署多个应用实例的根本原因。调用了同步锁,
1.先从本地缓存中寻找该类,也就是Tomcat自带的类加载器webAppClassLoader是否加载过。
2.如果Tomcat类加载器没有加载到该类,就看看系统类加载器的缓存中查找该类。
3.如果系统类加载器中也没有就让extclassloader拓展类加载该类,很关键,其目的是防止Web应用自己的类覆盖JRE的核心类。因为 Tomcat 需要打破双亲委托机制,假如 Web 应用里有类似上面举的例子自定义了 Object 类,如果先加载这些JDK中已有的类,会导致覆盖掉JDK里面的那个 Object 类。这就是为什么 Tomcat 的类加载器会优先尝试用 ExtClassLoader 去加载,因为 ExtClassLoader 会委托给 BootstrapClassLoader 去加载,JRE里的类由BootstrapClassLoader安全加载,然后返回给 Tomcat 的类加载器。这样 Tomcat 的类加载器就不会去加载 Web 应用下的 Object 类了,也就避免了覆盖 JRE 核心类的问题。
4.如果扩展目录也没有,就在本地目录中加载该类,如果还不成功就尝试系统类加载器加载该类,Web 应用是通过Class.forName调用交给系统类加载器的,因为Class.forName的默认加载器就是系统类加载器。如果不行的话就抛出异常ClassNotFoundException。
而在findclass()重写的方法里,主要有三个步骤:
1)先在web应用本地目录下查找要加载的类。
2)如果没有找到,交给父加载器去查找,它的父加载器就是上面提到的系统类加载器AppClassLoader。
3)如果父加载器也找不到这个类,就直接抛出ClassNotFoundException异常。