Java虚拟机类加载机制

Java虚拟机类加载机制

技术杂谈小彩虹2021-08-16 23:33:00110A+A-

        所谓的JVM的类加载机制是指JVM把描述类的数据从.class文件加载到内存,并对数据进行校验、转换解析、和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这就是 JVM 的类加载机制。
Java语言中的加载、连接、初始化都是在运行期完成的,这样虽然对性能会有影响,但是却十分灵活。Java语言的动态扩展性很强,其原因就是依赖于Java运行期动态加载和动态连接的特性,动态加载是指我们可以通过预定义的类加载器和自定义的类加载器在运行期从本地、网络或其他地方加载.class文件;动态连接是指在面向接口编程中,在运行时才会指定其时机的实现类。 image

1. Java类的生命周期:

  • 加载
    • 在加载阶段有三个步骤:
    1. 通过一个类的全限定名获取定义此类的二进制字节流
    2. 将二进制字节流所代表的静态存储结构转换为方法去中的运行时数据结构
    3. 在内存中生成一个代表此类的java.lang.Class的对象,作为方法去中这个类的访问入口
    • 在这个阶段,有两点需要注意:
    1. 并没有规定从哪里获取二进制字节流。我们可以从.class静态村中文件中获取,也可以从zip、jar等包中读取,可以从数据库中读取,也可以从网络中获取,甚至我们自己可以在运行时自动生成。
    2. 在内存中实例化一个代表此类的java.lang.Class对象之后,并没有规定此Class对象是方到Java堆中的,有些虚拟机就会将Class对象放到方法区中,比如HotSpot。
  • 验证
    • 验证是连接阶段的第一个步骤,验证的目的是为了确保.class文件中的字节流所包含的信息是复合当前虚拟机的要求,并且不会危害到虚拟机自身的安全的。
    • 验证主要包括四个方面的验证:文件格式验证、元数据验证、字节码验证和符号引用验证。
      1. 文件格式验证:主要验证二进制字节流数据是否复合.class文件的规范,并且该.class文件是否在本虚拟机的处理范围之内(版本号验证)。只有通过了文件格式的验证之后,二进制的字节流才会进入到内存中的方法去进行存储。而且只有通过了文件格式验证之后,才会进行后面三个验证,后面验证都是基于方法区的存储结构进行的。
      2. 元数据验证:主要是对类的元数据信息进行语义检查,保证不存在不符合Java语义规范的元数据信息。
      3. 字节码验证:字节码验证是整个验证中最复杂的一个过程,在元数据验证中,验证了元数据信息中的数据类型做完校验后,字节码验证主要对类的方法体进行校验分析,保证被校验的类的方法不会做出危害虚拟机的行为。
      4. 符号引用验证:符号引用验证发生在连接的第三个阶段解析阶段中,主要保证解析过程可以正确地执行。符号引用验证时类本身引用的其他类的验证,包括:通过一个类的全限定名是否可以找到对应的类,访问的其他类中的字段和方法是否存在,并且访问行是否合适等。
  • 准备
    • 在准备阶段所作的工作就是,在方法去中为类Class对象的类变量分配内存并初始化类变量,有三点需要注意:
      1. 在方法区中分配内存的只有类变量(被static修饰的变量),而不包括实例变量,实例变量会将更随着对象在Java堆中为其分配内存
      2. 初始化变量的时候,是将类变量初始化为其类型对应的值0值,比如有如下类变量,在准备阶段完成之后,val的值是0而不是123,为val复制为123,实在最后要将的初始化阶段之后。
      3. 对于常量,其对应的值会在编译阶段就存储在字段表的ConstantValue属性当中,所以在准备阶段结束之后,常量的值就是ConstantValue所指定的值了,比如如下,在准备阶段结束之后,val的值就是123了。
  • 解析
    • 解析是将符号引用解析为直接引用的过程,符号引用是指在.class常量池中存储的CONSTANT_Class.info、CONSTANT_Fieldref_info等常量,直接引用则是直接指向目标的指针、相对偏移量或是一个能简介定位到目标的句柄、如果有了直接引用,那引用的目标就必定已经在内存中了。对于解析有一下3点需要注意:
      1. 虚拟机规范中并未规定解析阶段发生的具体时间,指规定了在执行newarray, new, putfield, putstatic, getfield, getstatic等16个指令之前,对他们所使用的符号引用进行解析。所以虚拟机可以在类被加载器加载之后就进行解析,也可以在执行这几个指令之前才进行解析。
      2. 对同一个符号引用进行多次解析式很常见的事,除invokedynamic指令意外,虚拟机事项可以对第一次解析的结果进行缓存,以后解析相同的符号引用时,只要取缓存的结果就可以了。
      3. 解析动作主要对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行解析
  • 初始化
    • 类的初始化阶段才是真正开始执行类中定义的Java程序代码。初始化说白了就是调用类构造器()的国政,在类的构造器中会为类变量初始化定义的值,会执行静态代码块中的内容。下面将介绍几点和开发者关系较为紧密的注意点
      1. 类构造器()是由编译器自动收集类中出现的类变量、静态代码块中语句合并产生的,收集的顺序是在源文件中出现的顺序决定的,静态代码块可以访问出现在静态代码块之前的类变量,出现的静态代码块之后的类变量,只可以赋值,但是不能访问,比如如下代码
      public class Demo {
          private static String before = "before";
          static {
                  after = "after";                    // 赋值合法
                  System.out.println(before);         // 访问合法,因为出现在 static{} 之前
                  System.out.println(after);          // 访问不合法,因为出现在 static{} 之后
          }
          private static String after;
      }
      
      1. () 类构造器和()实例构造器不同,类构造器不需要显示的父类的类构造,在子类的类构造器调用之前,会自动的调用父类的类构造器。因此虚拟机中第一个被调用的 () 方法是 java.lang.Object 的类构造器
      2. 由于父类的类构造器优先于子类的类构造器执行,所以父类中的 static{} 代码块也优先于子类的 static{} 执行
      3. 类构造器() 对于类来说并不是必需的,如果一个类中没有类变量,也没有 static{},那这个类不会有类构造器 ()
      4. 接口中不能有 static{},但是接口中也可以有类变量,所以接口中也可以有类构造器 {},但是接口的类构造器和类的类构造器有所不同,接口在调用类构造器的时候,如果不需要,不用调用父接口的类构造器,除非用到了父接口中的类变量,接口的实现类在初始化的时候也不会调用接口的类构造器
      5. 虚拟机会保证一个类的 () 方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只有一个线程去执行这个类的类构造器 (),其他线程会被阻塞,直到活动线程执行完类构造器 () 方法
  • 使用
  • 卸载

2. 类加载器

2.1 类加载器

        类加载器是完成"通过一个类的全限定名获取这个类的二进制字节流"的工作,类加载器是独立于虚拟机之外存在的。 对于每一个类,都需要加载这个类的类加载器和类本身来确认这个类在 JVM 虚拟机中的唯一性,每个类加载器都有独立的类名称空间。换句话说,比较两个类是否“相等”,必须是在这两个类是由同一个类加载器加载的前提下比较,如果两个类是由不同的类加载器加载的,即使它们两个来自于同一个 .class 文件,那这两个类也是不同的。

2.2 类加载器的分类

        从不同的角度看,加载器可以有不同的分类方式。

  1. 从java虚拟机角度来看,存在两种不同的类加载器

    • 一种是启动类加载器(Bootstrap ClassLoader),这个类是由C++语言实现的,是虚拟机本身的一部分。
    • 另一种是除了启动类加载器之外,所有的其他类加载器,是由Java语言实现的,是独立于Java迅即之外的,比那个且全部继承自抽象类java.lang.ClassLoader
  2. 从Java开发人员的角度来看,可以分为以下3中类加载器

    • 启动类加载器(Bootstrap ClassLoader):加载 <JAVA_HOME>\lib 目录下和 -Xbootclasspath 参数所指定的可以被虚拟机识别的类库到内存中
    • 扩展类加载器(Extension ClassLoader):加载 <JAVA_HOME>\lib\ext 目录中的和 java.ext.dirs 系统变量所指定的路径中的类库加载到内存中
    • 应用程序类加载器(Application ClassLoader):加载用户类路径上所指定的类库,开发者可以直接使用这个类加载器,是默认的类加载器。

2.3 双亲委派模型

image

对于上图所示的这种关系呢,我们就称之类类加载器的双亲委派模型。在双亲委派模型中,除了顶层的 Bootstrap ClassLoader 之外,其他的类加载器都有自己的父加载器。
双亲委派模型的工作流程是这样的,如果一个类加载器收到了一个加载类的请求,会首先把这个请求委派给自己的父加载器去加载,这样的所有的类加载请求都会向上传递到 Bootstrap ClassLoader 中去,只有当父类加载器无法完成这个类加载请求时,才会让子类加载器去处理这个请求。
使用双亲委派模型的好处就是,被加载到虚拟机中的类会随着加载他们的类加载器有一种优先级的层次关系。比如,开发者自定义了一个 java.lang.Object 的类,但是你会发现,自定义的 java.lang.Object 永远无法被调用,因为在使用自定义的类加载器去加载这个类的时候,自定义的类加载器会将加载请求传递到 Bootstrap ClassLoader 中去,在 Bootstrap ClassLoader 中会从 rt.jar 中加载 Java 本身自带的 Java.lang.Object,这个时候加载请求已经完成,找到了这个类,就不需要自定义的 ClassLoader 去加载用户路径下的 java.lang.Object 这个类了。
双亲委派模型对于 Java 程序的稳定运行十分重要,实现却非常简单

首先会判断是否已经加载过此类了,如果已经加载过就不用再加载了 如果没有加载过,则调用父类加载器去加载 若父类加载器为空,则默认使用启动类加载器作为父类加载器加载 若父类加载器加载未能成功会抛出 ClassNotFoundException 的异常 再调用自己的 findClass() 方法进行加载 如下代码所示:

protected synchronized Class<?> loadClass(String name, boolean resolve) throw ClassNotFoundException {
    Class c = findLoadedClass(name);
    if(c == null){
        try{
            if(parent != null){
                c = parent.loadClass(name, resolve);
            } else {
                c = findBootstrapClassOrNull(name);
            }
        } catch (ClassNotFoundException e){

        }
        if(c == null){
            c = findClass(name);
        }
    }
    if(resolve){
        resolveClass(c);
    }
    return c;
}

点击这里复制本文地址 以上内容由权冠洲的博客整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!

支持Ctrl+Enter提交

联系我们| 本站介绍| 留言建议 | 交换友链 | 域名展示
本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除

权冠洲的博客 © All Rights Reserved.  Copyright quanguanzhou.top All Rights Reserved
苏公网安备 32030302000848号   苏ICP备20033101号-1
本网站由 提供CDN/云存储服务

联系我们