前言:最近两个月公司实行了996上班制,加上了熬了两个通宵上线,状态很不好,头疼、牙疼,一直没有时间和精力写博客,也害怕在这样的状态下写出来的东西出错。为了不让自己荒废学习的劲头和习惯,今天周日,也打算写一篇博客,就算是为了给自己以前立的flag(每个月必须写几篇博客)的实现。那么本次博客的主题我选择了java的类加载过程的探究以及双亲委派机制模型以及它被破坏的场景,搞清楚这个对于我们理解java的类加载过程以及面试中都是很有必要的。
本篇博客的目录
一:类加载器
二:类加载的过程和阶段
三:双亲委派机制
四:双亲委派机制被破坏
正文
一:类加载器
1.1:类加载器的解释
类加载器是什么?在平时的开发过程中,我们会定义各种不同的类,这些类最终都会被类加载加载到jvm中,然后再解析字节码运行。如果非得给类加载器一个定义,那么它是这样的:通过一个类的全限定名来获取描述此类的而二进制字节流,这个动作是在java虚拟机外部实现的,实现这个动作的代码模块称为'类加载器';这句话乍听有些抽象,其实不难理解。拿现实中的栗子来比拟的话,比如我们去用电脑光驱放光碟这个过程:光碟就是我们写的类,光驱就是类加载器,只有通过光驱加载之后,光碟上的内容才会被解析,我们才能在屏幕上看到光碟上放入的内容。另外,对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这句话什么意思呢?就是说如果两个类在你写的内容是一模一样的,但是只要他们是由不同的类加载器加载的,那么这两个类就是不同的!
二:类加载过程
类加载一共分为七个过程,他们的具体的顺序是:加载->验证->准备->解析->初始化,接下来我们来一一介绍这些过程:
2.1:加载
类加载过程中,虚拟机需要完成以下三件事:
(1)通过一个类的全限定名来获取定义此类的二进制字节流
(2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
(3)在内存中生成一个代表此类的java.lang.class的对象,作为方法区的这个类的访问入口
对于我们第一印象可能是二进制字节流是从class文件中获取的,但是其实并不是这样。设计者在对类的字节流获取上并没有做出明确的约束。一个类的全限定名并不一定是从class文件中获取的,而有可能是从jar、war、ear、网络中、运行时(比如动态代理、反射技术)、jsp、数据库等,正是由于这样的开放式设计,所以java才能在如此多的平台上大放溢彩。换言之,如果java设定只有从class文件中获取的话,那么java的使用场景就会大受限制,比如反射技术就无法实现,jsp就无法直接从servlet中获取。当获取类的二进制字节流后,虚拟就按照虚拟机所需的格式存储在方法区之中,然后在内存中实例化一个class对象,这个对象将作为程序访问方法区的这些类型的外部入口。
2.2:验证
2.2.1:文件格式的验证
该验证阶段主要是保证输入的字节流能正确的解析并存储于方法区之内,格式上符合描述一个java类型信息的要求,主要的目的是保证输入的字节流能正确的解析并存储于方法区之内,该阶段的验证主要基于二进制字节流进行的 ,主要包含以下的验证:
①:是否以魔数开头②:主、次版本号是否早当前虚拟机的处理范围之内③:常量池的常量中是否有不被支持的常量类型
③:指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
④:class文件中各个部分以及文件本身是否有被删除的活附加的其他信息
2.2.2:元数据的验
这个阶段主要是保证字节码描述的信息符合java语言规范,这个阶段可能包含的验证点如下:
①:这个类是否有父类 ②这个类的父类是否继承了不允许被继承的类(比如被final修饰的类)
②:如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
③:类中的字段、方法是否与父类产生矛盾
该阶段主要是对类的元数据信息进行语义验证,保证不存在不符合java语言负担的元数据信息
2.2.3:字节码验证
①保证任意时刻的操作数栈的数据类型和指令代码序列都能配合工作,不会出现java类型的错误基本类型加载
②:控制跳转,保证跳转指令不会跳转到方法体以外的字节码指令上
③:保证方法体重的类型转换是有效的,比如在强制转换的过程中,只能将父类对象转换为子类对象,而不能将子类对象转换为父类对象。比如(Person peson =(Person)method.getObject(String inputParam)),但是无法实现(Object obj =(Object)method.getPerson(String inputParam))这就是java中的强制类型的转换过程控制发生在此时
2.2.4:符号引用的验证
①:符号引用中通过字符串描述的全限定名是否能找到对应的类
②:在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段
③:符号引用中的类、字段、方法中的访问性(private、protected、public、default)是否可以被当前类访问
这个阶段如果找不到的类会抛出java.lang.NosuchMethodError、java.lang.IllegalAcessError、java.lang.NoSuchFieldError等异常
2.3:准备
该阶段会正式为类变量分配内存并设置类变量初始值的阶段,这个阶段只会初始化类变量(静态字段)而不会初始化实例变量。比如以一个字段 public static Long value = 1235L;在实例化的过程中,初始化字段的初始值是0而不是1235L,但是注意一点:对于常量类或者枚举,会实例化对应的值:比如public static final Integer num = 45; 那么在准备阶段,会将num直接初始化为45,而不是0
2.4:解析
解析阶段是将常量池中的符号引用替换为直接引用的过程,解析动作主要是针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号进行解析.当进行字段解析的时候,首先会按照继承关系从下往上递归搜索各个接口和它的父接口,如果接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。如果不是java.lang.object的话,将会按照继承关系从下往上递归搜索其父类,如果查找到了与目标相匹配的字段,则返回这个字段的直接引用。
如果找不到,就会抛出java.lang.NoSuchFieldError异常,如果查找过程中会对这个字段进行权限验证,如果发现不具备这个字段的访问权限,将会抛出java.lang.IiieagalAccessError异常!
2.5:初始化
在准备阶段,类变量(静态字段)已经赋值过一次系统要求的初始化值,二在初始化阶段,就开始根据代码中指定的值去初始化变量或者其他资源,初始化阶段是执行类构造器方法的过程。在初始化阶段,会通过执行类构造器<clinit>()方法的过程,cliint()方法与构造方法还不是完全相同的,它不需要显式的调用父类构造器,虚拟机会保证子类的clinit()方法在执行前,父类的clinit()方法已经执行完毕,因此在虚拟机中第一个被执行的clinit方法一定是java.lang.Object。
注意:clinit方法对于类或者接口来说都不是必须的,如果一个类没有静态语句块,有没有对变量的赋值操作,那么编译器可以不为这个类生成clinit方法
接口和类都有可能生成clinit()方法;虚拟机会保证在多线程环境下,clinit方法也只会执行一次,而不会执行多次。
三:双亲委派机制
3.1:类加载器的分类
3.1.1:启动类加载器
这个加载器主要负责将存放在<JAVA_HOME>的lib目录下的,或者被--Xbootclasspath参数所指定的路径中的,并且被虚拟机识别的(比如rt.jar).名字不符合的类库即使放在lib目录下也会被加载。
3.1.2:扩展类加载器
这个加载器主要负责加载存放在<JAVA_HOME>/lib/ext目录下的java类库,或者而被java.ext.dirs系统变量所指定的路径的所有类库,开发者可以直接使用扩展类加载器
3.1.3:应用程序加载器
这个类加载器负责加载用户类路径上所指定的类库,如果程序中没有定义过自己的类加载器,那么一般情况下这个就是程序中默认的类加载器。
3.2:双亲委派机制
指的是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,(每一个层次的类加载器都是如此)。只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己加载完成。
3.3:双亲委派机制的好处
3.3.1:java类随着它的加载器一起具备了一种带有优先层级的层次关系,维护基础类环境的稳定和高效的运转。例如类:java.lang.object,它存放在rt.jar中。如果没有双亲委派机制,那么如果程序员自定义了一个叫做java.lang.object的类,并且放在程序的classPath模型下,那么系统将会出现多个不同的object类,java最基础的行为也就无法得到保证,程序也会混乱一片。
3.3.2:双亲委派机制的实现
双亲委派机制的实现比较简单,主要的原理就是在类加载过程中,首先检查请求的类是否已经在被加载过了,如果没有就调用父类的加载器进行加载,如果父类加载器为null(不存在),就默认使用启动类加载器作为父类加载器,如果父类加载失败,就会抛出classNotFoundException类,再调用自己的findClass方法进行加载。
四:3次破坏双亲委派机制
4.1:第一次被破坏
第一次发生在jdk1.2发布之前,由于双亲委派模型在jdk1.2之后才被引入,而类加载器和抽象类java.lang.ClassLoader则在jdk1.0时代已经存在,意思就是设计这个东西出来的时候1.0的jdk无法满足双亲委派模型(当时也并没有考虑到),那么java的jdk设计者就为java.lang.classLoader添加了新的protected方法的findClass(),在1.0时代,classLoader只有一个loadClass()方法,而在1.2之后,findclass()方法的主要目的就是就是进行自身的类加载。
4.2:双亲委派模型的缺陷
双亲委派模型很好的解决了各个类加载的基础类的统一问题,但是假如基础类要回调用户的代码怎么办呢?而在JNDI(Java Naming and Directory Interface,Java命名和目录接口))服务中它的代码由启动类加载器去加载,但JNDI的目的就是对资源进行集中管理与资源,它需要会调用由独立厂商实现并部署在应用程序的classPath下的JNDI接口的提供者的代码,但是启动类加载器又不认识这些代码,因此双亲委派此刻就无法完成了。
如何解决这个问题?
线程上下文类加载器,这个类加载器可以通过java.lang.Thread类的setContextClassLoader方法进行设置,如果创建线程时未设置,它将会从父线程中继承一个。有了上下文类加载器,JNDI就可以通过父类加载器去请求子类加载器去完成类加载器的动作,这实际上已经违背了双亲委派模型的设计初衷,但是这也是无可奈何的事情。java中的涉及SPI的东西,比如JDBC、JAXB、JBI等加载工作都采用了这种方式!
4.3:代码热替换、模块热部署
为了达到java代码的热更新替换技术,OSGI模型经过一系列角逐,最终成了行业的标准。它的实现模块化部署的时候直接阿静一个程序模块(Bundle)连同类加载器一起换掉以实现代码的热部署,在OSGI下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为更复杂的网状结构,在OSGI的实际加载过程中,只有开头符合双亲委派机制,其余的类查找都在平级的类加载器中进行加载。
五:总结
本篇博客主要介绍了类加载机制和它的加载过程,以及对双亲委派机制对于java的基础平台的重大意义,如何理解类加载机制并实现在java开发平台中类加载的过程对于我们实际的开发代码都是一门内功的修炼,只有修炼好了内功,才能在java编程的路上越走越远。本篇博客的设计的开发代码比较少,都是一些关于概念的理解。在开发过程中,我们也是不仅仅只注重写代码,修炼内功也是必不可少的一部分。