java类加载过程和类加载器
一:类加载概述
首先我们了解一下类是怎么进入内存的,jvm把class文件加载到内存,然后对文件进行校验,转换解析和初始化变
成可以被虚拟机直接利用的java类型,这就是虚拟机的类加载机制。java类的加载,链接和初始化都是在运行阶段进行
的,这虽然会带来一定的性能开销,却在灵活性上有较大的提高。后面会在类加载的加载过程举出提高灵活性的例子
二:类加载时机
类的加载过程:
首先加载,验证,准备,初始化,使用,卸载等过程的顺序是固定的,在虚拟机中有严格规定,而类的解析的顺
序却是不固定的。他有可能发生在该类的初始化之后,具体我们会在类的解析阶段详细介绍。然后我们了解下类加载
的时机,类的初始化,当且仅当只有五种情况才会进行。
1:遇到new,getStatic,putStatic,invokeStatic的时候,如果类没有进行初始化,会先对类进行初始化。(加载,验
证,准备)自然在初始化之前开始。这四条指令对应的操作是利用new关键字实例化一个对象的时候,读或者写一个静
态变量的时候,(这个静态变量不是final修饰、这个静态变量的值不是编译期间就确定了的,比如在有主函数的类里写
的静态量)调用类的静态方法的时候。
2:用java.lang.reflect进行反射调用的时候,没有初始化类会先进行初始化。
3:当初始化一个类的时候,发现父类未初始化,会先对父类进行初始化。
4:包含主方法(main)的类会进行初始化。
5:如果一个java.lang.invoke.MethodHandle实例最后解析结果REF_getStatic,REF_setStatic,REF_invokeStatic的
方法句柄,并且这个方法的句柄所在的类没有进行初始化,会对该类进行初始化。
注意事项:
1:子类调用父类的静态域(静态变量或者静态方法)不会初始化子类。
2:通过定义类的对象数组,不会初始化这个类。
3:引用类在编译阶段确定的常量池的数据,不会对类进行初始化
三:类加载过程
1:加载:是指在类加载过程中的一个阶段,不要混淆这两个概念。在加载过程中做的工作是:
把class文件中的类加载到内存中,通过类的全限定名来确定类的二进制流。把二进制流转换成方法区的运行时常量池
的数据结构,生成一个class对象,为访问这个类提供一个接口。
这个class对象的来源并没有严格的规定,很多java技术都建立在这一基础之上。
①从zip包中读取,发展成为日后的jar,war的基础。
②从网络中获取,典型应用Applet
③其他文件生成,典型用例jsp
④运行时计算生成,应用最多的就是代理技术,为特点接口生成形式为"*$Proxy"的代理类二进制流。
2:验证:
由于class的来源不确定,所以jvm需要对class文件进行一些列严格的验证,如果验证不通过,那么编译器将拒绝编
译文件。验证阶段会完成四个阶段的验证:
①:文件格式验证:
验证class文件是否以魔数0XCAFEBABE开头,主次版本号的class文件是否在当前jvm的处理范围。
检查常量池中是否有不被支持的常量类型。
指向常量的索引是否指向了不存在的常量或者不符合类型的常量。
查看是否有不符合UTF-8编码的数据等。
②:元数据验证:
这个类是否有父类(除了java.lang.Object类外,所有类都有父类)
这个类是否继承了不允许继承的类(用final修饰的类)
这个类(如果不是抽象类)是否实现了父类或者接口要求实现的方法。
类中的字段,方法是否与父类产生了矛盾(覆盖父类的final字段,或者不符合规则的重写了父类的方法。子类方法名,
参数都与父类相同,但是返回值与父类不同)
③字节码验证:
保证任意时刻操作数栈的数据类型与指令序列可以配合工作,不会出现类似于在操作数栈中的数据,使用时却按照
Long类型加载到本地变量表中。
保证方法体中的类型转换是有效的,父类对象可以变成子类对象(比较常用的用法)。子类对象却不能强转成父类对象。
④:符号引用验证:
符号引用验证是将符号引用变成直接引用过程中进行的验证,这层验证发生在解析阶段。
验证能否通过字符串描述的全定限名找到对应的类
在指定的类中是否能找到符合方法的字段描述符以及简单名称描述的方法和字段。
符号引用中的类,方法,字段的权限是否可以被当前的类访问。
3:准备:
在这个阶段,为类变量(static修饰)初始化空间和赋值,实例变量在new的时候分配在java堆中分配内存,详细过程我
在这里详细介绍了,更点详情点击我的博客链接java对象创建过程
这里需要注意的是:并不是在准备阶段为static变量进行赋值的
比如一条语句:public static int a = 5;在准备阶段过后,a的值为0,并不是5。在类加载的时候a的值才会被赋值为5。
但是这条语句:public static final int a = 5;在准备阶段过后,a的值为5。这时候在准备阶段之后,a的值会直接指向
方法区的常量池中的数据5。还记得上面说过的么?这时候其他类在调用这个类的静态变量a的时候,不会加载这个类。
个人理解是因为a在方法区中存在了符号引用,指向了方法区中的数据5,所以访问a变量不会完成对改类的加载。
4:解析:
在这个阶段主要发生的是符号引用替换为直接引用的过程。什么是符号引用和直接引用呢?
符号引用:是一组符号来描述所引用的目标,可以是任意形式的字面量,只要能够唯一确定引用的目标就ok。符号
引用的地址不一定加载到内存中,所以当发生这个阶段的时候,可能又会去加载其他的类。这是动态链接的一种形式。
符号引用字面量的形式也有一定的要求,在class文件中有严格的约束。
直接引用:直接引用是能直接定位到目标的指针,偏移量,或者简介定位到目标的句柄。可以确定的是,有了直接引
用,代表对象已经加载到内存中。
简而言之就是符号引用是能够确定目标的直接地址。而直接引用是能被我们用来访问目标的工具(比如句柄)。
发生解析的时机是在执行getfields,instanceof,getstatic,putstatic,new等16个用于操作符号引用的字节码之前。
所以虚拟机可以根据需要来决定是在加载时进行符号解析还是在有上述的指令时候进行符号解析。只一个比较灵活的
过程。解析动作主要针对的是类或接口哦,字段,类方法,接口方法,方法类型,方法句柄和调用点进行符号解析。
一:类或者接口的解析:
假设当前所处的类是A,把一个从未解析的符号引用F解析为一个类或者接口(命名为C)的直接引用,会有如下步骤:
①:首先判定C是不是一个数组,不是数组的话会把F代表的全定限名传递给A的类加载器加载他,在加载过程中可
能还会加载类或者接口C的父类或者接口,如果发生解析错误,解析过程宣告失败,编译也就失败。
②:如果C是一个数组类型,并且数组元素的类型是对象,F的描述符信息将会是类似[Ljava/long/Integer]的形式需
要加载的元素就是java.lang.integer的形式。接着jvm生成一个代表此数组维度和元素的数组对象。
③:上述解析没有问题之后,还要判断类A是否有访问类C的权限,如果没有将抛出java.lang.IllegelAccessError异常。
二:字段的解析
首先确定解析引用的类或接口是否成功,否则解析失败。解析完类或接口之后,判断类中是否有简单名称和字段描
述符都与目标相匹配的字段,如果有,返回匹配结果。
否则查看该类的接口中是否有简单名称和字段描述符都与目标相匹配的字段,如果有,返回匹配结果。
否则查看该类父类中是否有简单名称和字段描述符都与目标相匹配的字段,如果有,返回匹配结果。
否则,查找失败,返回java.lang.NoSuchFieldError异常。
找到之后还要看看时候有该字段的访问权限,比如只通过类名访问实例域是不可以的,将抛出
java.lang.IllegelAccessError异常。
类方法的解析:
类方法和接口方法是分开定义的,在class文件中有不同的索引形式,如果发现是一个接口的索引,将抛出
java.lang.IncompatibleClassChangeError异常
如果通过了第一步,检查类C中是否有符合简单名称和描述符与目标相匹配的方法,如果有,直接返回。
否则在C类的父类递归寻找是否有简单名称和描述符与目标相匹配的方法,有则返回。
否则递归类C的接口列表,检查是否有和目标有相同的简单名称和描述符的方法,如果有,则说明类C是一个
抽象类,将抛出java.lang.AbstractMethodError异常。
否则查找失败,将抛出java.lang.NoSuchMethodError异常。
最后,查找成功,验证当前类是否有访问该方法的权限,如果没有权限,将抛出java.lang.IllegelAccessError异常。
接口方法的解析:
如果在接口方法表中文件中发现class_Index是一个类的索引,将抛出java.lang.IncompatibleClassChangeError异常
否则在接口C中查看是否有与目标具有相同简单名称和描述符的方法,如果有,则返回方法的直接引用。
否则,递归向上查询父接口,查看是否有与目标相同的简单名称和描述符的方法,如果有,返回方法的引用,
否则,宣告查找方法失败,将抛出java.lang.NoSuchMethodError异常。
由于接口声明方法都是public,不存在访问权限的问题。
5:初始化
类的初始化阶段是类加载的最后一步,初始化阶段之前,都是jvm再帮我们做工作,到了初始化阶段之后,才到了
我们java代码执行的时候。比如我们重写了类的构造函数,构造函数的执行的时候就是类的初始化的时候。需要注
意的是:在类的初始化过程中,会有一个<clinit>方法先执行,这个方法是由为类变量赋值语句或者static代码块组
成的。<clinit>方法不是必须的,当没有静态代码块,或者类变量初始化的时候,这个方法是不执行了。执行子类的
<clinit>方法,会先执行父类的<clinit>方法,但对于接口来说,执行子接口的<clinit>函数,不会调用父接口的<clinit>
函数。只有当调用父类的静态变量的时候,父接口才会初始化,才会执行<clinit>方法。而且实现类实现接口的时候
也不会调用接口的<clinit>方法。至于具体的java类初始化顺序,请参考我的博客:类的构造函数的执行顺序问题
四:类加载器
类加载器的定义:顾名思义是实现类加载过程的代码模块。完成加载过程中的根据类的全限定名确定类的二进制流,
把二进制流在jvm方法区的常量池生成一个数据结构,创建class文件,提供访问这个类的接口。好吧,把加载的加
载过程又说了一遍。而类加载器就是做这些工作的。
不同类加载器加载的对象必然不同,这里所说的相同是指Class对象的equals(),isInstance(),包括instanceof判定的
情况。
双亲委派模型:
首先我们了解下加载器的种类有哪些(以java开发人员的角度分析):
启动类加载器(Bootstrap ClassLoader):
这个加载器由sun.misc.Launcher$ExtClassLoader实现,负责加载JAVA%HOME/lib下的合法类库,包括rt.jar等,不
合法的类库不会被加载。
扩展类加载器(Extension ClassLoader):
这个加载器由sun.misc.Launcher$ExtClassLoader实现,负责加载JAVA%HOME/lib/etc下的扩展的类库。或者被
java.etc.dirs系统变量指定的类库,可以被加载到内存。
应用程序类加载器(Application ClassLoader):
这个加载器由sun.misc.Launcher$AppClassLoader实现,负责加载类路径下指定的类库,开发者可以直接使用。应用
程序没有自定义类加载器,那么这个加载器将是默认的类加载器。类加载器的关系如下图所示。
工作过程:当一个类加载器收到类的加载要求的时候,他自己不会立即加载这个类,而是把加载请求传递给父类,
每一层都是如此,这样加载请求就会传递给启动类加载器,当启动类加载器无法加载请求的时候,才会交给下一级
加载器加载请求。以此类推。
这样做的原因有两点:
1:这种层次的加载顺序防止类被重复加载。
2:为了保证程序运行的安全性,如果不采用这样的机制系统。我们自己创建了一个Object类,把它放在classpath下。
让我们的自定义的类加载器去加载它,不保证以启动类加载器加载他,那么系统最基本的功能也消失了。