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_HOMElib目录中并且能被虚拟机识别的类库到JVM内存中,如果名称不符合的类库即使放在lib目录中也不会被加载。该类加载器

无法被Java程序直接引用。

2)扩展类加载器Extension ClassLoader:

  该加载器主要是负责加载JAVA_HOMElibext,该加载器可以被开发者直接使用。

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。

  加载和连接阶段是交叉进行的

原文地址:https://www.cnblogs.com/huigelaile/p/10826975.html