java虚拟机(二)--类加载机制和双亲委派模型
一、类的生命周期
加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)
七个阶段,加载(装载)、验证、准备、初始化和卸载这五个阶段顺序是固定的,类的加载过程必须按照这种顺序开始
验证、准备、解析统称为连接阶段。连接阶段主要的目的是确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟
机自身安全。
1.1、加载:
1、通过类的全限定名获取定义此类的二进制流
2、将二进制流中的静态存储结构转化为方法区的运行时数据结构
3、内存中生成一个代表此类的java.lang.class对象,作为方法区这个类的各种数据的访问入口
内存中实例化一个java.lang.Class对象(并没有明确规定是在java堆中,对于HotSpot,Class对象比较特殊,虽然是对象,但是存放在方法区中)
1.2、验证:
主要是四个检验过程:文件格式验证、元数据验证、字节码验证和符号引用验证。
1.3、准备:
为类变量分配内存并设置类变量初始值(零值),这些变量所使用的内存都在方法区中进行分配
假如一个类变量的定义为:public static int value = 123;
这时候初始值时0而不是123,把value赋值123是在执行putstatic指令后,在初始化阶段进行的,这个指令放到<clinit>方法中。
如果是常量,javac会生成Constantvalue属性,在准备阶段会根据Constantvalue的设置将value赋值为123
1.4、解析:
解析阶段是虚拟机常量池内的符号引用替换为直接引用的过程。
符号引用:
1.类和方法的全限定名
2.字段的名称和描述符
3.方法的名称和描述符。
以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要能定位到目标。和jvm实现的内存布局无关
直接引用:
直接指向目标的指针、相对偏移量或者是间接定位到目标的句柄,和jvm实现的内存布局相关
1.5、初始化:
类的初始化阶段是类加载过程的最后一步,在准备阶段,类变量已赋过一次系统要求的零值,而在初始化阶段,则是根据程序员自定义去初始化
类变量和其他资源,初始化阶段是执行类构造器<clinit>()的过程。
有且只有以下五种情况下初始化过程会被触发执行:
1.遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需先触发其初始化。生成这4条指令的最
常见的java代码场景是:使用new关键字实例化对象、读取或设置一个类的静态变量(被final修饰、已在编译器把结果放入常量池的静态字段除外)
的时候,以及调用类的静态方法的时候。
2.使用java.lang.reflect包的方法对类进行反射调用的时候
3.当初始化一个类的时候,如果发现其父类还没有进行过初始化、则需要先出发其父类的初始化
4.jvm启动时,用户指定一个执行的主类(包含main方法的那个类),虚拟机会先初始化这个类
在上面准备阶段 public static int value = 12; 在准备阶段完成后 value的值为0,而在初始化阶调用了类构造器<clinit>()方法,这个阶段完
成后value的值为12。
5.使用JDK1.7动态语言支持的时候,如果一个java.lang.invoke.MethodHandler实例最后的解析结果PEF_getStatic,PEF_putStatic,
PEF_invokeStatic的方法句柄,这个方法句柄对应的类的没有初始化,要触发初始化。
二、类加载器:
JVM设计者把类加载的加载阶段中的“通过'类全限定名'来获取定义此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用
程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”。
2.1、双亲委派模型:
从虚拟机的角度来说,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),该类加载器使用C++语言实现,属于虚
拟机自身的一部分。另外一种就是所有其它的类加载器,这些类加载器是由Java语言实现,独立于JVM外部,并且全部继承自抽象类java.lang.
ClassLoader。
2.2、类和类加载器:
对于任何一个类,都需要由加载它的类加载器和这个类来确立其在JVM中的唯一性。也就是说,两个类来源于同一个Class文件,并且被同一个
类加载器加载,这两个类才相等。
从Java开发人员的角度来看,大部分Java程序一般会使用到以下三种系统提供的类加载器:
1)启动类加载器Bootstrap ClassLoader:
负责加载JAVA_HOME\lib目录中并且能被虚拟机识别的类库到JVM内存中,如果名称不符合的类库即使放在lib目录中也不会被加载。该类加载器
无法被Java程序直接引用。
2)扩展类加载器Extension ClassLoader:
该加载器主要是负责加载JAVA_HOME\lib\ext,该加载器可以被开发者直接使用。
3)应用程序类加载器Application ClassLoader:
该类加载器也称为系统类加载器,它负责加载用户类路径(Classpath)上所指定的类库,开发者可以直接使用该类加载器,如果应用程序中
没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
我们的应用程序都是由这三类加载器互相配合进行加载的,我们也可以加入自己定义的类加载器。这些类加载器之间的关系如下图所示:
如上图所示的类加载器之间的这种层次关系,就称为类加载器的双亲委派模型(Parent Delegation Model)。该模型要求除了顶层的启动
类加载器外,其余的类加载器都应当有自己的父类加载器。子类加载器和父类加载器不是以继承(Inheritance)的关系来实现,而是通过组合
(Composition)关系来复用父加载器的代码。
双亲委派模型的工作过程为:
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的加载
器都是如此,因此所有的类加载请求都会传给顶层的启动类加载器,只有当父加载器反馈自己无法完成该加载请求(该加载器的搜索范围中没有
找到对应的类)时,子加载器才会尝试自己去加载。
2.3、自定义类加载器:
若要实现自定义类加载器,只需要继承java.lang.ClassLoader 类,并且重写其findClass()方法即可。java.lang.ClassLoader 类的基本
职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个Java类,即java.lang.Class类的一个
实例。除此之外,ClassLoader还负责加载Java应用所需的资源,如图像文件和配置文件等,ClassLoader中与加载类相关的方法如下:
方法 说明
getParent() 返回该类加载器的父类加载器。
loadClass(String name) 加载名称为 二进制名称为name 的类,返回的结果是 java.lang.Class 类的例。
findClass(String name) 查找名称为 name 的类,返回的结果是 java.lang.Class 类的实例。
findLoadedClass(String name) 查找名称为 name 的已经被加载过的类,返回的结果是 java.lang.Class 类的实例。
resolveClass(Class<?> c) 链接指定的 Java 类。
PS:
1、如果不想打破双亲委派模型,那么只需要重写findClass()即可。
2、如果想打破双亲委派模型,那么就重写loadClass()。
2.5、被动引用:
1、对于静态字段,只有直接定义这个字段的类才会被加载,所以通过子类SubClass.value调用父类SuperClass的value属性,子类不会被加载,
除非在SubClass内部去调用。
2、通过数组定义引用类,不会出发这个类的初始化。
3、常量在编译的时候就会保存到调用类的常量池中,本质上没有引用定义常量的类,因此不会出发定义常量类的初始化而接口在初始化的
时候,不会要求其父接口的全部init,只有在真正使用父接口的时候(例如引用接口中定义的常量)才会init。
加载和连接阶段是交叉进行的