赞
踩
从类被加载到虚拟机中开始,到卸载出内存为止,整个生命周期包括 加载,验证,准备,解析,初始化,使用,卸载七个阶段。验证,准备,解析这三个阶段统称为连接。
加载,验证,准备,初始化这个五个阶段顺序是确定的,类加载过程必须按这种顺序开始(只是开始,不是进行或者完成,开始之后,可以交叉混合进行,通常会在一个阶段执行过程中,激活或者调用另外一个阶段),但解析阶段不一定,解析在某些情况下是在初始化之后才开始的(为了支持java的动态绑定)。
什么时候开始类加载过程的第一个阶段,虚拟机规范中没有进行强制约束,由虚拟机自由把握。
有四种情况规定,如果类没有初始化,必须进行类初始化:
有且只有这四种场景被称为对一个类进行主动引用。除此之外,所有的引用类的方式,都不会触发初始化,称为被动引用。
三个被动引用的例子:
加载阶段,完成三件事情:
对于规范的三点要求并不具体,例如获取二进制字节流的来源就可以多种多样。加载阶段是开发期可控性最强的阶段,因为可以使用系统提供的类加载器,也可以使用用户自定义的类加载器(从而用户可以控制字节流的获取方式)
验证是连接的第一步,这一阶段目的是确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机的安全。
java语言本身是相对安全的语言,使用java代码不能访问数组边界以外的数据,将对象转型为没有实现的类型,跳转到不存在的代码行之类,以外编译期间会报错。但是,字节码的来源不一定是java代码编译而来,可以通过任何途径 产生class文件。
虚拟机规范对这个阶段的验证限制和规范显得十分笼统,但一般会完成四个阶段的检验:
准备阶段正式为类变量分配内存并设置变量初始值,这些内存都将在方法区内分配。
类变量:被static修饰的变量,不包括实例变量,实例变量是对象实例化时被分配到java堆中。
变量初始值:是指通常情况下变量的 “零值”,例如 Public static int a=123,准备阶段,这个变量值为 0,因为此时没有执行任何java方法,给类变量赋值的 putstatic指令是在程序被编译后,存放于类构造器< client >方法中的,这个赋值操作只有在初始化阶段才进行。
上面说的是通常情况,那么特殊情况就是 类字段字段属性表中存在 ConstantValue属性,准备阶段就会赋予指定的值。 例如 public static final int a=124,这样编译时,就会为a变量生成 ConstantValue属性。
解析阶段是虚拟机将常量池的符号引用替换为直接引用的过程。
符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要能无歧义的定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标不一定已经加载到内存中了。
直接引用:直接指向目标的指针,相对偏移量或者能间接定位到目标的句柄。直接引用与内存相关,有了直接引用,说明引用目标已经在内存中实现了。符号引用相同的,在不同虚拟机实例上,翻译出来的直接引用一般不同。
虚拟机规范中并未规定解析阶段发生的时机。只是要求对一些操作符号引用的字节码指令之前,先对他们所使用的符号引用进行解析,例如 checkcast,new,getstatic,putstatic等等。所以这个会根据需要来判断,是类加载时就解析常量池的符号引用,还是符号引用将要被使用时被解析,这个都不确定。
对于同一个符号引用进行多次解析请求是很常见的事情,虚拟机实现可能会对第一次解析结果进行缓存(运行时常量池中记录直接引用,把常量表示为已解析状态),避免解析动作重复进行。虚拟机需要保证是在同一个实体中,所以一个符号引用已经成功解析过了,后续引用解析就一直成功。如果第一次解析失败,后续其他指令对这个符号的解析,也应该收到同样的异常。
解析动作主要针对 类或接口,字段,类方法,接口方法四类符号引用进行。具体解析过程自行查阅。
虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”的动作放到虚拟机外部实现,以便用户程序自己决定如何获取所需要的类。实现这个动作的代码模块成为 “类加载器”
全限定类名:就是类名全称,带包路径的用点隔开,例如: java.lang.String。
即全限定名 = 包名+类名
非全限定名:类名
类加载器的作用不止存在于类加载阶段,比如任意一个类,都需要加载它的类加载器和类本身来确定其在java虚拟机中的 唯一性。例如,同一个class文件,加载它的类加载器不同,它就不相等。这里说的相等,包括类的 equals方法,isAssignableFrom方法,isInstance方法返回的结果。如果我们用自定义的类加载器去加载类,然后实例化这个类,调用 instanceof 方法,去比较系统默认加载器加载的类(直接写类名或者全限定类名,就是使用系统默认加载器),那么返回就是false。
从java虚拟机的角度讲,有两种加载器:启动类加载器(c++实现,是虚拟机的一部分)和其他类加载器(java实现,继承于抽象类 java.lang.ClassLoader)。
从java开发人员的角度,类加载器的分类更细致一点。
我们的应用程序是由这三种类加载器相互配合进行加载的。如果有必要,可以加入自定义的加载器。
这类加载器中的层次关系,称为类加载器的双亲委派模型。处理顶层的启动类加载器外,其余类都有自己的“父类加载器”,这种父子关系不是通过继承实现的,而是通过组合实现。
该模型由JDK1.2期间被引入,不是强制性约束模型,可以被破坏。
工作过程:如果一个类加载器收到类加载请求,首先不会尝试自己加载这个类,而是把这个请求委派个父类加载器去完成,每一层次都是如此,因此所以加载请求都会传送到启动类加载器,只有父类反馈无法加载这个类(抛出 ClassNotFoundException),子类加载器才会尝试自己去加载(调用findClass()方法)。
优点: java类随着类加载器一起,具备了优先级层次关系。例如Obkect类,无论是哪一个加载器加载它,包括自定义加载器,最终都是由启动类加载器加载的,在虚拟机中属于同一个类。这样就避免了虚拟机中出现多个Object类(不是实例,是类),使得java类型体系中,一些最基础的行为无法得到保证。
注意:实现双亲委派的代码都集中在 ClassLoader的locadClass()方法中,先检查类是否已经被加载,没有则调用父类加载器的loadClass()方法。父类加载器为空,则默认启动类为父类加载器。父类加载失败,抛出异常,则调用自己的findClass()方法进行加载。
java世界大部分加载器都遵循双亲委派模型,但是也有例外。下面是该模型三次大规模 被破坏的情况。
(1)JDK1.2之前,没有双亲委派模型,但是类加载器和抽象类 java.lang.ClassLoader在JDK1.0就存在,那时用户自定义类加载器就是复写 loadClass()方法。JDK1.2为了向前兼容,添加了一个 findClass()方法,这里可以写自己的类加载逻辑。
(2)双亲委派模型解决了基础类统一的问题,越上层的类由上层加载器加载,但是一些场景需要基础类调用用户代码(上层类加载器不认识这些用户代码),所以有了线程上下文类加载器。
(3)用户程序对动态性的追求,例如:热替换,模块热部署。这样导致一些加载规则不在符合双亲委派模型,具体内容自行查阅。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。