三|三 类加载器

目录
1.类什么时候会被初始化?5个条件,3个不被初始化的例子
2.类加载过程
3.类加载器
4.双亲委派模型
1.类什么时候会被初始化?5个条件,3个不被初始化的例子 1.遇到new,getstatic,putstatic,或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这四条指令单最常见的Java代码场景是:使用new关键字实例化对象的时候,读取或设置一个类的静态字段(被final修饰,已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
2.使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先出法其初始化。
3.当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先出法其父类的初始化。
4.当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
5.当使用JDK1.7的动态语言支持时,如果一个Java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic,REF_outStatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
类不被初始化的例子
1.通过子类引用父类的静态字段,子类不会被初始化,只会初始化父类
2.通过数组定义来引用类( main 函数中:A[] a = new A[10],不会触发A的初始化)
3.调用类的静态常量(final static)


2.类加载过程 1.加载
三件事:
①、通过一个类的的全限定名来获取定义此类的二进制字节流
②将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构(将常量池转变成运行时常量池)
③在内存中生成一个代表这个类的java.lang.Class对象,作为方法区着各类的各种数据的访问入口。
相比较于类加载过程的其他阶段,非数组类获取类的二进制字节流的动作是开发人员可控性最强的,因为加载阶段既可以使用系统提供的引导类加载器来完成,也可以由用户自定义的类加载器去完成
,开发人员可以自己重写一个类加载器的loadClass()方法
对于数组类,不通过类加载器创建,由java虚拟机直接创建的。但是数组类的元素类型(去掉所有维度的类型)最终是要通过类加载器去创建。
数组类的创建过程要遵循以下原则:
①、如果数组的组件类型(数组去掉一个维度的类型)是引用类型,就递归加载过程去加载这个组件类型,数组C将在加载该组件类型的类加载器的类名称空间上被标识。
②、如果数组的组件类型不是引用类型(例如int[]数组),java虚拟机将会把数组C标记为与引导类加载器关联
③、数组类的可见性与它的组件类型的可见性一致,如果组件类型不是引用类型,那数组类的可见性将默认为public
加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式由虚拟机自行定义。然后在内存中实例化一个java.lang.Class类的对象 (没有明确规定在java堆中吗、,对于Hotspot来说,Class对象比较特殊,存放在方法区里面)。这个对象作为程序访问方法区中这些类型数据的外部接口。
连接阶段:
2 .验证
验证的目的是确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机的安全。
整体上看,验证阶段大致上会完成下面四个阶段的检验工作:
(1)基于字节流检验
文件格式验证:基于二进制字节流进行的,只有经过这个阶段的验证,字节流才会进入内存的方法区中存储。目的是保证输入的字节流能正确解析并存储于方法区之内
(2)基于方法区的存储结构
元数据验证:对字节码描述的信息进行语义分析,保证不存在不符合JAVA语言规范的元数据信息。
字节码验证:最复杂的一个阶段,对类的方法体进行校验,保证被叫严磊的方法在运行时不会做出还虚拟机的事。目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
符号引用验证:发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在解析阶段中发生。目的是确保解析动作能正常执行。
3.准备
准备阶段是正式为类变量分配内存并设置初始值的阶段,这些变量所使用的内存都在方法区中进行分配。
注意两点:一点是类变量,另一点是初始值“通常情况下”是数据类型的零值。例如“public static int value=https://www.it610.com/article/123”在准备过程value的值为0而不是123,因为这时候尚未开始执行任何java方法,而把value赋值为123的puststatic指令是程序被变异后,存放于类构造器()方法之中,所以把value赋值为123的动作在初始化时候才会进行
特殊:如果类字段的字段属性表中存在ConstacntValue属性,那么准备阶段变量value就会被初始化为所指定的值
public static final int value=https://www.it610.com/article/123;
这是value被赋值为123.
4.解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用和直接引用又有什么关联呢?
符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时可以无歧义的定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中,各种虚拟机实现的内存布局可以各不相同,但是它们能接收的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在java虚拟机规范的Class文件格式中。
直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个嫩详见定位到目标的句柄。直接引用时和虚拟机实现的内存布局相关的,通过一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,被引用的目标必在内存中存在。
三|三 类加载器
文章图片
看上图的常量池,有”//”注释的就是符号引用常量.它在常量池中的信息是通过一个引用值来标识的.其他可以直接获取到的值,它其实是直接指向目标的指针,偏移量或者句柄.
解析这个步骤做的事情通俗一点说,就是把”//”后面的数据拿到.
解析的具体时间没有规定,只要求了在执行anewarraycheckcast getfield getstatic instanceof invokedynamic invokeinterface invokespecial invokestatic invokevirtual ldc multianewarray new putfield putstatic 这十六个用于操作符号引用的字节码指令之前,先对他们所使用的符号引用进行解析
对一个符号引用进行多次解析请求,除了invokedynamic指令以外,虚拟机可以缓存第一次解析的结果,之后再请求解析,可以直接调用避免重复进行。
invokedynamic指令是“动态调用点限定符”动态也就是必须等到程序实际运行到这条指令的时候,解析才进行的。
解析动作主要针对
类或接口解析
假设当前代码所处的类为D,如果要把一个从未解析过的符号引用N解析为一个类或接口C的直接引用,那虚拟机完成这个解析的过程需要以下3个步骤
①:C不是一个数组类型,虚拟机会把代表N的全限定名传递给D的类加载器去加载这个类C。在加载过程中,由于元数据验证、字节码验证的需要,有可能触发其他相关类的加载动作。
②:如果C是一个数组类型,并且数组的元素类型为对象,例如N是“[Ljava/lang/Integer”的形式,那将会按照第1点的规则加载数组元素类型,如果N的描述符是前面那样,需要加载的元素类型就是“java.lang,Integer”,接着由虚拟机生一个代表此数组维度和元素的数组对象。
③:如果上面的步骤没有出现异常,C已经在虚拟机中成为了一个有效的类和接口了,但是解析完成之前还有进行符号引用验证,确保D是否具备对C的访问权限。
字段解析:
要解析字段符号引用,首先要对字段表内字段所属的类或接口的符号引用进行解析,如果解析成功,那这个字段所属的类或接口用C表示,虚拟机规范要求安好如下步骤对C进行后续字段的搜索
自己有就直接用自己的字段的直接引用,随后去接口类里递归去找,随后去父类找,如果都没,抛出异常java.lang.NoSuchMethodError
如果上面的步骤没有出现异常,但是解析完成之前还有进行符号引用验证,确保是否具备对字段的访问权限。
类方法解析:
要解析类方法符号引用,首先要对类方法表中方法所属的类或接口的符号引用进行解析,如果解析成功,那这个方法所属的类或接口用C表示,虚拟机规范要求安好如下步骤对C进行后续类方法的搜索
①:类方法和接口方法符号引用的常量类型定义是分开的,如果在类方法表中发现class_index中索引的C是个接口,直接抛出java.lang.IncompatibleClassChangeError异常
②:如果C本身就包含了简单名称和描述符都与目标相匹配的方法,则返回这个方法的直接引用,查找结束
③:否则,在C的父类中递归查找,如果父类包含了简单名称和描述符都与目标相匹配的方法,则返回这个方法的直接引用,查找结束
④:在类C实现的接口列表及它们的父接口之中递归查找是否有简单名称和描述符都与目标相匹配的方法, 如果存在匹配的方法,说明类C是一个抽象类,这时查找结束,抛出java.lang,AbstractMethodError异常
这个需要这么理解,如果是普通的类去实现某一个接口的方法的话,那么它肯定在第(2)步已经直接返回.如果能执行到第(4)步,则说明C本身的常量池中并没有对应的直接引用.那么只能是说明这个方法是抽象方法.包含抽象方法的类必定是抽象类,所以这里有个结论就是C是抽象类.
⑤:否则查找失败,抛出异常java.lang.NoSuchMethodError
【三|三 类加载器】⑥::如果上面的步骤没有出现异常,但是解析完成之前还有进行符号引用验证,确保是否具备对方法的访问权限。
接口方法解析:
要解析接口方法符号引用,首先要对接口方法表中方法所属的类或接口的符号引用进行解析,如果解析成功,那这个方法所属接口用C表示,虚拟机规范要求安好如下步骤对C进行后续类方法的搜索
①:类方法和接口房符号引用的常量类型定义是分开的,如果在类方法表中发现class_index中索引的C是个类而不是接口,直接抛出java.lang.IncompatibleClassChangeError异常
②:如果C本身就包含了简单名称和描述符都与目标相匹配的方法,则返回这个方法的直接引用,查找结束
③:否则,在C的父接口中递归查找,直到找到java.lang.Object类位置。如果包含了简单名称和描述符都与目标相匹配的方法,则返回这个方法的直接引用,查找结束
④:否则查找失败,抛出异常java.lang.NoSuchMethodError
接口中方法默认都是public的,因此不存在访问权限的事


5.初始化
类加载过程的最后一步。到了初始化阶段,才真正开始执行类中定义的java程序代码。
准备阶段,已经赋过一次系统要求的初始值,而在初始化阶段,就要根据程序员的要求来赋值了。从另一个角度来表达:初始化阶段是执行类构造器clinit()方法的过程。
clinit()一些可能会影响程序运行行为的特点和细节。
①clinit()是由编译器按顺序收集类中所有的类变量和静态语句块中的语句合并产生的。这里注意一点,静态语句块只能访问定义在他前面的类变量,对于定义在他后面的,他只能赋值,而不能访问。

publicclass Test{
static{
i=0; //这句赋值正常编译通过
System.out.print(i); //这句访问就不行
}
static int i=0;
}
②clinit()与类的构造函数不同,他不需要显式地调用父类构造器,虚拟机会保证在子类的()方法执行之前,父类的()已经执行完毕。因此,在虚拟机中第一个被执行的()方法的类肯定是java.lang.Object
③ 由于父类的clinit()先执行,所以父类的静态语句块要优于子类的变量复制操作。
static class Parent{
public static int A=1;
static{
A=2;
}
static class Sub extends Parent{
public static int B=A;
}
publicstaticvoid main(String[] args){
System.out.println(Sub.b);
}
}
④clinit()对于类或接口来说不是必须的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成clinit()方法
⑤接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口和类一样会生成clinit()方法,但是不同的是,执行接口的()方法不需要先执行父接口的()方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时一样不会执行接口的clinit()。
⑥虚拟机会保证一个类的clinit()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程的()方法完毕。


3.类加载器
只有同一个类加载器加载的类才能会相等。相同的字节码被不同的类加载器加载的类不相等。
启动类加载器+其他类记载器(扩展类加载器,应用程序类加载器,自定义类加载器)
自定义类加载器,继承classloader,重写loadClass 或 findClass方法,实例化CLASS对象
重写loadClass 可以破坏双亲委派模型, findClass在保留双亲委派模型的基础上定义自己的类加载器
下图是破坏双亲委派模型的例子:
三|三 类加载器
文章图片
优势:
高度灵活性,通过自定义类加载器实现热部署,代码加密


4.双亲委派模型



三|三 类加载器
文章图片
加载的时候,从下往上
找到不到的时候,再往下扔‘’
优点:越是基础的类越是被上层的类加载器加载,保证了JAVA程序的稳定性。

    推荐阅读