Java虚拟机之类加载机制

Java虚拟机之类加载机制

导语:在上一篇博客中我们对Java虚拟机的内存分配和回收进行了介绍和分析,本篇博客将主要针对Java虚拟机的类加载机制,包括类的生命周期、加载过程、类加载器以及双亲委派模型等做详细的介绍

虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制;类是在运行期间第一次使用时动态加载的,而不是一次性加载所有类,因为如果一次性加载类,会占用很多的内存


类的声明周期

当程序主动使用某个类时,如果该类还未被加载到内存中,则JVM会通过加载、连接、初始化3个步骤来对该类进行初始化

在这里插入图片描述

类从被加载到虚拟机内存中开始,到卸载出内存为止,整个生命周期包括:加载 -> 验证 、准备 ->、解析 -> 初始化 -> 使用 -> 卸载7个阶段,其中验证、准备、解析3个部分统称为连接


类加载过程

加载过程主要包含了加载、验证、准备、解析和初始化这 5 个阶段,我们可以暂时先忽略使用以及卸载这两个阶段

加载指的是将类的class文件读入到内存,并为之创建一个java.lang.Class对象,也就是说,当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象,这里要注意加载只是类加载的一个阶段,加载并不是类加载

---->加载

  • 通过类的完全限定名称获取定义该类的二进制流
  • 将该字节流表示的静态存储结构转化为方法区的运行时结构
  • 在内存中生成一个代表该类的 Class 对象,作为方法区中该类各种数据的访问入口

二进制字节流可以从以下方式中获取:

  • 从本地文件系统加载class文件
  • 从JAR、WAR、EAR等包加载class文件
  • 通过网络加载class文件(典型例子applet)
  • 把一个Java源文件动态编译(例如动态代理技术),并执行加载

---->连接(验证、准备、解析)

当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中;连接又可分为如下3验证、准备、解析三个阶段

验证

验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致;确保了 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全,其主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证:

  • 文件格式验证
    验证字节流是否符合Class文件格式规范,并能被当前虚拟机所加载处理
  • 元数据验证
    对字节码描述的信息进行语义分析,分析是否符合Java语言规范
  • 字节码验证
    分析数据流和控制,确定语义的合法性、逻辑性;主要针对元数据验证后对方法体的验证,保证类的方法在运行时正常
  • 符号引用验证
    针对符号引用转换为直接引用,确定访问类型等涉及到引用的情况,保证引用一定会访问到,避免出现类无法访问的问题

准备

  • 类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存
  • 实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中;实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次

解析

将常量池的符号引用替换为直接引用的过程,其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定

---->初始化

初始化是为类的静态变量赋予正确的初始值,准备阶段和初始化阶段看似有点矛盾,其实是不矛盾的,如下:

如果类中有语句:private static int a = 10,它的执行过程是首先字节码文件被加载到内存后,先进行链接的验证这一步骤,验证通过后准备阶段,给a分配内存,因为变量a是static的,所以此时a等于int类型的默认初始值0,即a=0,然后到解析,之后到初始化这一步骤时,才把a的真正的值10赋给a,此时a=10


类初始化时机

主动引用

虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列五种情况必须对类进行初始化(加载、验证、准备都会随之发生):

  • 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则必须先触发其初始化。最常见的生成这 4 条指令的场景是:使用 new 关键字实例化对象的时候;读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)的时候;以及调用一个类的静态方法的时候
  • 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化
  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类
  • 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化

被动引用

以上 5 种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。被动引用的常见例子包括:

  • 通过子类引用父类的静态字段,不会导致子类初始化

     System.out.println(SubClass.value);  // value 字段在 SuperClass 中定义
    
  • 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法

     SuperClass[] sca = new SuperClass[10];
    
  • 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。

     System.out.println(ConstClass.HELLOWORLD);
    

类与类加载器

  • 两个类相等,需要类本身相等,并且使用同一个类加载器进行加载。这是因为每一个类加载器都拥有一个独立的类名称空间
  • 两个类相等,需要类本身相等,并且使用同一个类加载器进行加载。这是因为每一个类加载器都拥有一个独立的类名称空间

类加载器分类

  • 类加载器负责加载所有的类,其为所有被载入内存中的类生成一个java.lang.Class实例对象,一旦一个类被加载如JVM中,同一个类就不会被再次载入了。正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识
  • 在Java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识

从 Java 虚拟机的角度来讲,只存在以下两种不同的类加载器:

  • 启动类加载器(Bootstrap ClassLoader),使用 C++ 实现,是虚拟机自身的一部分
  • 所有其它类的加载器,使用 Java 实现,独立于虚拟机,继承自抽象类 java.lang.ClassLoader

从 Java 开发人员的角度看,类加载器可以划分得更细致一些:

  • 启动类加载器(Bootstrap ClassLoader)此类加载器负责将存放在 <JRE_HOME>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可
  • 扩展类加载器(Extension ClassLoader)这个类加载器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将 <JAVA_HOME>/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器
  • 应用程序类加载器(Application ClassLoader)这个类加载器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此一般称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器

双亲委派模型(类加载机制)

JVM类加载机制主要由如下三种:

  • 全盘负责:所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
  • 双亲委派:所谓的双亲委派,则是先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
  • 缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。修改了Class后,必须重新启动JVM,程序所做的修改才会生效

双亲委派模型

在这里插入图片描述
1.工作过程

一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载

2.优点

  • 使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一
  • 例如 java.lang.Object 存放在 rt.jar 中,如果编写另外一个 java.lang.Object 并放到 ClassPath 中,程序可以编译通过。由于双亲委派模型的存在,所以在 rt.jar 中的 Object 比在 ClassPath 中的 Object 优先级更高,这是因为 rt.jar 中的 Object 使用的是启动类加载器,而 ClassPath 中的 Object 使用的是应用程序类加载器。rt.jar 中的 Object 优先级更高,那么程序中所有的 Object 都是这个 Object

3.实现

以下是抽象类 java.lang.ClassLoader 的代码片段,其中的 loadClass() 方法运行过程如下:先检查类是否已经加载过,如果没有则让父类加载器去加载。当父类加载器加载失败时抛出 ClassNotFoundException,此时尝试自己去加载

public abstract class ClassLoader {
    // The parent class loader for delegation
    private final ClassLoader parent;

    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }

    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }
}

结语:到此关于Java虚拟机的类加载机制就介绍到这,在此博客中之前也介绍了关于JVM的其他知识,如果想更多的了解JVM,请大家多多支持关注

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章