【JVM】JVM类加载机制(类加载过程和类加载器)

什么是JVM?

JVM(Java Virtual Machine)即Java虚拟机。Java程序跨平台特性主要是指字节码文件可以在任何具有Java虚拟机的计算机或者电子设备上运行,Java虚拟机中的Java解释器负责将字节码文件解释成为特定的机器码进行运行。因此在运行时,Java源程序需要通过编译器编译成为.class文件。我们知道,java.exe是java class文件的执行程序,但实际上java.exe 程序只是一个执行的外壳,它会装载jvm.dll(Windows下是jvm.dll,Linux下为libjvm.so),这个动态连接库才是java虚拟机的实际操作处理。

JVM是JRE的一部分。它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM有自己完善的硬件架构,如处理器,堆栈,寄存器等,还具有相应的指令系统。Java语言最重要的特点就是跨平台运行。使用JVM就是为了支持与操作系统无关,实现跨平台。所以Java虚拟机是属于JRE的,而现在我们安装JDK时也附带了安装JRE(当前也可以单独安装JRE)

引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。JVM作用是帮助执行Java字节码的,不同的平台有不同的JVM,这样java源代码经过编译为字节码之后就能在各种平台上运行了,还有内存管理,垃圾回收等底层功能,这样程序员就只需要专注业务实现不用操心这些事情了。

其中,JDK,JRE和JVM三者之间的关系如下图:
在这里插入图片描述

类加载机制

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

类加载的时机

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading),验证(Verification),准备(Preparation),解析(Resolution),初始化(Initialization),使用(Using)和卸载(Unloading)七个阶段。其中验证,准备,解析三个部分统称为连接(Linking),这七个阶段的发生顺序如下图:
在这里插入图片描述
加载,验证,准备,初始化和卸载这五个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班的开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也叫动态绑定或晚期绑定)。

类加载器

(1)启动类加载器:
负责加载JAVA_HOME\lib目录中并且能够被虚拟机识别的类库到JVM内存中,如果名称不符合的类库即使放在lib目录中也不会被加载。该类加载器无法被Java程序直接引用。

存放在JRE的lib目录下jar包中的类(以及由虚拟机参数-Xbootclasspath指定的类)

(2) 扩展类加载器:
该加载器主要是负责加载JAVA_HOME\lib\,该加载器可以被开发者直接使用。

其父类加载器是启动类加载器,负责加载相对次要,但又通用的类,比如存放在JRE的lib/ext目录下jar包中的类(以及由系统变量java.ext.dirs指定的类)

(3)应用类加载器
该类加载器也叫系统类加载器,它负责加载用户类路径(Classpath)上所指定的类库,开发者可以直接可以该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

其父类加载器是扩展类加载器。负责加载应用程序路径下的类。这里的应用程序路径指的是虚拟机参数-cp/-classpath,系统变量java.class.path或环境变量CLASSPATH所指定的路径。默认情况下,应用程序中包含的类便是由应用类加载器加载的。

(4)自定义类加载器(必须继承ClassLoader)

类加载器之间的关系如下图:
在这里插入图片描述
Java虚拟机中,类的唯一性是由类加载器实例以及类的全限定名一起确定的。所以即便是同一串字节流,经由不同的类加载器加载,也会得到两个不同的类。

类加载(Class Loading)过程

1、 加载

双亲委派模型:每当一个类加载器接收到加载请求时,它会先将请求转发给父类加载器。在父类加载器没有找到所有请求的类的情况下,该类加载器才会尝试去加载。

双亲委派机制的优点

  1. 安全,可避免用户自己编写的类动态替换Java的核心类,如java.lang.String。
  2. 避免全限定命名的类重复加载(使用了findLoadClass())判断当前类是否已加载。
在加载阶段,虚拟机需要完成以下三件事情:
1、通过一个类的全限定名来获取定义此类的二进制字节流
2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

相对于类加载过程的其他阶段,一个非数组类的加载阶段(准确地说,是加载阶段中获取类的二进制字节流的动作)是开发人员可控性最强的,因为加载阶段既可以使用系统提供的引导类加载器来完成,也可以由用户自定义的类加载器去完成,开发人员可以通过定义自己的类加载器去控制字节流的获取方式(即重写一个类加载器的loadClass()方法)。

加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式由虚拟机实现自行定义,虚拟机规范未规定此区域的具体数据结构。然后在内存中实例化一个java.lang.Class类的对象(并没有明确规定是在Java堆中,对于HotSpot虚拟机而言,Class对象比较特殊,它虽然是对象,但是存放在方法区里面),这个对象将作为程序访问方法区中的这些类型数据的外部接口。

2、连接

2.1验证

验证是链接阶段的第一步,这一步主要的目的是确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身安全。

验证阶段主要包括四个检验过程:文件格式验证、元数据验证、字节码验证和符号引用验证。

1、文件格式验证
验证class文件格式规范,例如: class文件是否已魔术0xCAFEBABE(咖啡宝贝)开头 , 主、次版本号是否在当前虚拟机处理范围之内等

2、元数据验证
这个阶段是对字节码描述的信息进行语义分析,以保证起描述的信息符合java语言规范要求。验证点可能包括:这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)、这个类是否继承了不允许被继承的类(被final修饰的)、如果这个类的父类是抽象类,是否实现了起父类或接口中要求实现的所有方法。

3、字节码验证
进行数据流和控制流分析,这个阶段对类的方法体进行校验分析,这个阶段的任务是保证被校验类的方法在运行时不会做出危害虚拟机安全的行为。如:保证访法体中的类型转换有效,例如可以把一个子类对象赋值给父类数据类型,这是安全的,但不能把一个父类对象赋值给子类数据类型、保证跳转命令不会跳转到方法体以外的字节码命令上。

4、符号引用验证

2.2准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。

这个阶段中有两个容易产生混淆的知识点,首先是这时候进行内存分配的仅包括类变量(static 修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在java堆中。其次是这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量定义为:
public static int value = 12;

那么变量value在准备阶段过后的初始值为0而不是12,因为这时候尚未开始执行任何java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器()方法之中,所以把value赋值为12的动作将在初始化阶段才会被执行。

上面所说的“通常情况”下初始值是零值,那相对于一些特殊的情况,如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量value就会被初始化为ConstantValue属性所指定的值,建设上面类变量value定义为:
public static final int value = 123;

编译时javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value设置为123。

2.3解析
解析阶段是虚拟机常量池内的符号引用替换为直接引用的过程。

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

直接引用:直接引用可以是直接指向目标对象的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机内存布局实现相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同,如果有了直接引用,那引用的目标必定已经在内存中存在。

虚拟机规范并没有规定解析阶段发生的具体时间,只要求了在执行anewarry、checkcast、getfield、instanceof、invokeinterface、invokespecial、invokestatic、invokevirtual、multianewarray、new、putfield和putstatic这13个用于操作符号引用的字节码指令之前,先对它们使用的符号引用进行解析,所以虚拟机实现会根据需要来判断,到底是在类被加载器加载时就对常量池中的符号引用进行解析,还是等到一个符号引用将要被使用前才去解析它。

解析的动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行。分别对应编译后常量池内的CONSTANT_Class_Info、CONSTANT_Fieldref_Info、CONSTANT_Methodef_Info、CONSTANT_InterfaceMethoder_Info四种常量类型。

1.类、接口的解析
2.字段解析
3.类方法解析
4.接口方法解析

3、初始化

类的初始化阶段是类加载过程的最后一步,在准备阶段,类变量已赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者可以从另外一个角度来表达:初始化阶段是执行类构造器< clinit >()方法的过程。

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

2、使用java.lang.reflect包的方法对类进行反射调用的时候

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

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

在类的初始化里,有一个著名的单例延迟初始化 例子,代码如下:

public class Singleton {
  private Singleton() {}
  private static class LazyHolder {
    static final Singleton INSTANCE = new Singleton();
  }
  public static Singleton getInstance() {
    return LazyHolder.INSTANCE;
  }
}

只有当调用Singleton.getInstance时,程序才会访问LazyHolder.INSTANCE,才会触发对LazyHolder的初始化,从而新建一个Singleton的实例。由于类的初始化是线程安全的,并且仅被执行一次,因此程序可以确保多线程环境下有且仅有一个Singleton实例。

小结

Java虚拟机将字节流转化为Java类的过程。这个过程分为加载,链接以及初始化三大步骤。
1、加载。指将查找字节流,并且据此创建类的过程。加载需要借助类加载器,在java虚拟机中,类加载器使用了双亲委派模型,即接收到加载请求时,会先将请求转发给父类加载器。
2、链接:指将创建成的类合并至Java虚拟机中,使之能够执行的过程。链接还分验证,准备和解析三个阶段。其中,解析阶段是非必须的。
3、初始化:是为标记为常量值的字段赋值,以及执行方法的过程。类的初始化仅会被执行一次,这个特性被用来实现单例的延迟初始化。

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/Sophia_0331/article/details/107149563