虚拟机类加载机制

                                                        虚拟机类加载机制

1.什么是类的加载机制?

虚拟机把描述类的数据从class文件中存储到内存中,并对数据进行校验、转换解析、初始化的操作,最后形成可以被虚拟机直接使用的java类型,这就是类的加载机制。

2.什么时候类被加载?

类被加载到虚拟机后,到被卸载出内存,整个生命周期包括加载、校验、准备、解析、初始化、使用、卸载七个阶段。

这七个阶段发生顺序如图:

其中加载、验证、准备、初始化、卸载这五个顺序是固定的,类加载过程必须按照这个顺序按部就班的开始,解析阶段则不一定,有些情况下可以再初始化阶段后开始,这是为了支持java语言的运行时绑定。

什么时候虚拟机开始加载操作,并没有什么约束,但是初始化阶段有四种情况必须立即对类进行初始化:

      (1) 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new            关键字实例化对象的时候,读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。

      (2) 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。

      (3) 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。

      (4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。

只有上述四种情况会触发初始化,也称为对一个类进行主动引用,除此以外,所有其他方式都不会触发初始化,称为被动引用

被动引用的例子:

package cn.yqg.test;

public class Test2 {
  static {
      System.out.println("super");
  }
 public static int value=6;
  
}

package cn.yqg.test;

public class Test3 extends Test2{
  static {
      System.out.println("sub");
  }
}
package cn.yqg.test;

public class Test4 {
  public static void main(String[] args) {
    System.out.println(Test3.value);
}
}

上面的结果只会输出super,不会对子类进行初始化,对于静态字段,只有直接定义这个字段的类才会被初始化。

还有两种常见情况不会触发此类的初始化,一种是通过数组定义来引用类,不会触发此类的初始化,代码如下:

public static void main(String args[]){
          SuperClass[] sca=new SuperClass[10];
}

另一种情况是常量在编译阶段存入调用类的常量池中,本质是没有直接引用到定义常量的类,因此不会触发类的初始化。

public class ConstClass{
          static{
                   Sysotem.out.println("Const init!");
          }
          public static final String HELLOWORLD="helloword";
}

public class Test{
          public static void main(String args[]){
                    System.out.println("ConstClass.HELLOWORLD");
          }
}

上面代码运行后,没有出现Const init!,这是因为在编译阶段将常量的值存储到了常量池中,

对常量ConstClass.value 的引用实际都被转化为Test类对自身常量池的引用,这两个类被编译成class后不存在任何联系。

 3.类加载的过程

类加载的全过程为加载、校验、准备、解析、初始化五个阶段。

3.1加载

加载阶段虚拟机主要完成三件事:

1)通过一个类的全限定名来获取定义此类的二进制字节流。

2)将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构。

3)在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区数据的访问入口。

3.2校验

校验阶段主要确保Class文件中的字节流包含的信息符合当前虚拟机的要求,并且不会危害到虚拟机自身的安全。

3.3准备

准备阶段是为了对类变量分配内存并设置类变量初始值的阶段,这些内存都在方法区中分配。这里的类变量初始值指数据的零值,假设定义一个类变量

public static int a=123;

那么准备阶段后内存中的值是0,不是123。

但是如果上面被定义为

public static final int a=123;

在准备阶段就把a的值赋值为123。

3.4解析

解析阶段是将虚拟机常量池中的符号引用替换为直接引用的过程,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_iofo、CONSTANT_Methodref_info等类型的常量出现,那么直接引用和符号引用有什么关联哪?

1)符号引用:用一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可。符号引用与虚拟机的内存布局无关,引用的目标不一定加载到内存中。

2)直接引用:直接引用可以是直接指向目标的指针,相对偏移量或者一个句柄,直接引用与虚拟机实现的内存布局是相关的,如果有了直接引用,在内存中一定存在引用的直接目标。

3.5初始化

到了初始化阶段,才真正开始执行类定义的代码,在准备阶段,类变量已经赋值过系统要求的默认值,在初始化阶段,类变量的值才会赋值为程序中的值。

初始化阶段是执行<clinit>()方法的过程,过程特点:

1)<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序所决定。 

2)<clinit>()方法与类的构造函数不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕,因此在虚拟机中第一个执行的<clinit>()方法的类一定是java.lang.Object。 

3)由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。 

4)<clinit>()方法对于类或者接口来说并不是必需的,如果一个类中没有静态语句块也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。 
5)接口中可能会有变量赋值操作,因此接口也会生成<clinit>()方法。但是接口与类不同,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也不会执行接口的<clinit>()方法。 
6)虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁和同步。如果有多个线程去同时初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其它线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,那么就可能造成多个进程阻塞。

4.类加载器

4.1类加载器类型

类加载器主要有三种,分别为启动类加载器、扩展类加载器、应用程序类加载器,这三种是系统提供的,我们也可以自己定义类加载器。

     1) Bootstrap ClassLoader : 将存放于<JAVA_HOME>lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar 名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用

     (2) Extension ClassLoader : 将<JAVA_HOME>libext目录下的,或者被java.ext.dirs系统变量所指定的路径中的所有类库加载。开发者可以直接使用扩展类加载器。

     (3) Application ClassLoader : 负责加载用户类路径(ClassPath)上所指定的类库,开发者可直接使用。

4.2双亲委派模型

双亲委派模型的工作过程是:如果一个类加载器收到加载类的请求,他首先不会在自己尝试加载这个类,而是委派给父类去加载这个类,每一层的类加载器都是如此,如果父类无法完成加载类的请求,子类才回去尝试自己去加载。

使用双亲委派模型有一个好处就是java类随着加载器有了一种优先级的层次关系,如类java.lang.Object类存放在rt.jre中,无论哪一个类加载器都要加载这个类,最终委派给启动类加载器去加载,因此Object类在所有的类加载器中都是同一个类,相反,如果没有使用双亲委派模型,由各个类加载器自行加载的话,如果用户写了一个java.lang.Object类,并将程序放在ClassPath中,那么系统将出现多个不同的Object类,程序就会变得一片混乱。

原文地址:https://www.cnblogs.com/zzuli/p/9407144.html