JVM类加载机制

  • 类加载过程
  • 类加载器
  • 双亲委派模型

一、类加载过程

1、概述

JVM类加载机制:虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可被虚拟机直接使用的java类型的过程。

2、类加载过程

一般来说,我们把类加载过程主要分成三个步骤:加载、连接和初始化,其中连接又分为三个部分:验证、准备和解析,整个生命周期七个阶段如图所示:

JVM类加载机制_第1张图片
下面分别来介绍上面的几个阶段:

(1)加载

JVM类加载阶段主要完成了三件事:

  • 通过的类的全限定名(包名+类名)来获取定义此类的二进制字节流(Class文件),而获取的途径可以通过jar包、war包、数据库、网络等方式。
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,这里只是转化了数据结构,并未合并数据。
  • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口,这个对象比较特殊,它存放在方法区中,并未规定存放在堆内存中。

(2)连接

当类被加载之后,系统会为之生成一个对应的Class对象,接着将会进入链接阶段,链接阶段负责把类的二进制数据合并到JRE中,其具体分为下面三个阶段:

a、验证

是连接阶段的第一步,其目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全,验证主要又分为四个阶段:

  • 文件格式验证:验证字节流是否符合Class文件格式的规范,以及能否被当前版本的虚拟机处理。
  • 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合java语言规范的要求。
  • 字节码验证:对类的方法体进行效验分析,保证被效验类的方法在运行时不会做出危害虚拟机安全的事件。
  • 符号引用:对类自身以外(如常量池中的各种符号引用)的信息进行匹配性效验,该验证发生在将符号引用转化为直接引用的时候,即解析阶段。

b、准备

为类的静态变量在方法区分配内存,并赋默认初始值(0或null),例如static int a = 100,这个静态变量a就会在准备阶段赋默认值0。

需要注意的是,对于一般成员变量是在类实例化的时候,随着对象一起分配在堆内存当中,对于静态常量b(static final int b = 100)在准备阶段会直接赋值100,而对于静态变量,这个操作是在初始化阶段进行的。

c、解析

虚拟机将常量池内的符号引用替换为直接引用的过程。其中符号引用是指以一组符号来描述所引用的目标,它可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可,其与虚拟机实现的内存布局无关;而直接引用是指直接指向目标的指针、相对偏移量或是一个能够间接定位到目标的句柄,它与虚拟机实现的内存布局相关。

(3)初始化

类加载过程的最后一步,除了加载阶段,用户可以通过自定义类加载器参与,其它阶段都完全由虚拟机主导和控制,到了初始化阶段才真正开始执行java代码。类初始化的主要工作是为静态变量赋值程序设定的初始值,这与准备阶段的区别在于,准备阶段只是为静态变量赋值默认值,例如,static int a = 100,静态变量在准备阶段赋值默认值0,在初始化阶段赋值程序设定的值100。

在虚拟机规范中,规定了有且只有五种情况必须对类进行初始化:

  • 遇到new创建实例、getstatic读取静态字段值、putstatic设置静态字段值和invokestatic调用静态方法时。
  • 使用java.lang.reflect包的方法对类进行反射调用的时候。
  • 当初始化一个类的时候,若发现其父类还未初始化,需先触发其父类的初始化。
  • 虚拟机启动的时候,用户需要指定一个执行的主类,虚拟机会先初始化它。
  • 使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、RE_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行初始化,则需要先触发其初始化。

(4)案例说明

public class Test1 {
    public static int value1;
    public static int value2 = 0;
    public static Test1 test1 = new Test1();
    public Test1(){
        value1++;
        value2++;
    }
    public static Test1 getTest1(){
        return test1;
    }
}

public class Test2 {
    public static Test2 test2 = new Test2();
    public static int value1;
    public static int value2 = 0;
    public Test2(){
        value1++;
        value2++;
    }
    public static Test2 getTest2(){
        return test2;
    }
}

测试结果:

        Test1 test1 = Test1.getTest1();
        Log.d(TAG,"Test1 value1:"+test1.value1);
        Log.d(TAG,"Test1 value2:"+test1.value2);

        Test2 test2 = Test2.getTest2();
        Log.d(TAG,"Test2 value1:"+test2.value1);
        Log.d(TAG,"Test2 value2:"+test2.value2);
        Test1 value1: 1
        Test1 value2: 1
		Test2 value1: 1
		Test2 value2: 0

我们可以看到两个类的打印结果不一样,在这里对两个类的加载过程做下说明:

Test1类的加载过程:
1.首先执行Test1 test1 = Test1.getTest1();
2.类的加载:加载Test1类
3.类的验证
4.类的准备:为静态变量分配内存,设置默认值,即设置value1、value2的默认值为0,test1的默认值为null
5.类的初始化:为静态变量设置程序中设定的值,首先执行public static int value2 = 0,所以value2的值为0,value1程序中没有设定值,所以还是默认值0;然后执行public static Test1 test1 = new Test1(),因为在Test1的构造函数中value1++,value2++,所以value1和value2的最终结果都为1

Test2类的加载过程:
1.首先执行Test2 test2 = Test2.getTest2();
2.类的加载:加载Test2类
3.类的验证
4.类的准备:为静态变量分配内存,设置默认值,即test2的默认值为null,value1、value2的默认值为0。
5.类的初始化:为静态变量设置程序中设定的值,首先执行public static Test2 test2 = new Test2(),因为Test2的构造函数中value1++、value2++,所以value1和value2的值为1,然后执行public static int value2 = 0,所以value2的值为0,因为程序中没有设定value1的值,所以value1和value2的最终结果是1和0

二、类加载器

JVM提供了三种类加载器:

1、启动类加载器

最顶层的类加载器,由C++实现,是虚拟机自身的一部分,负责加载存放在JAVA_HOME\lib目录中,或被-Xbootclasspath参数所指定路径中的、且可被虚拟机识别的类库,不能被java程序直接引用。

2、扩展类加载器

由java实现,独立于虚拟机外部,能被java程序直接引用,负责加载JAVA_HOME\lib\ext目录中的、或被java.ext.dirs系统变量所指定的路径中的所有类库。

3、应用程序类加载器

又称之为系统类加载器,是默认的类加载器,可以通过getSystemClassLoader()获取,负责加载用户路径上所指定的类库。

注意:除了JVM提供的三种类加载器外,用户还可以自定义类加载器,类加载器的关系图如下:

JVM类加载机制_第2张图片

三、双亲委派模型

1、工作原理

如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,请求最终到达最顶层的启动类加载器中,如果父类加载器能够完成加载任务,就成功返回,否则子加载器才会自己尝试去加载。

2、优点

类会随着它的类加载器一起具备带有优先级的层次关系,可保证java程序的稳定运行;实现简单,所有实现代码都集中在java.lang.ClassLoader的loadClass()中。

你可能感兴趣的:(java)