Java虚拟机之类加载器

此内容基本引用《深入理解Java虚拟机:JVM高级特性与最佳实践》一书,将书中内容进行了简要的摘选概括,如有不恰当或者误处,请留言,谢谢!

类加载的时机

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的生命周期包括了:加载、验证、准备、解析、初始化、使用、卸载。这七个阶段。其中验证、准备和解析三个部分统称为连接。如下图。
在这里插入图片描述
加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类的加载过程必须按照这种顺序开始,而解析不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定。这些阶段不是一个完成再开启另一个,而是通常是相互交叉混合式进行的,在一个阶段执行的过程中激活另外一个阶段。
什么时候开启类加载的第一个阶段:加载,这个虚拟机规范没有进行强制约束,但是对于初始化阶段,有且只有四种情况会立即对类进行初始化:

  1. 遇到new、getstatic、putstatic或invokestatic这4条字节码命令时,对应的Java代码场景就是:使用new关键字实例化对象、读取一个静态字段、设置一个静态字段、调用一个类的静态方法,已被final修饰过的静态字段除外。
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有初始化,就执行初始化。
  3. 当初始化一个类的时候,如果发现父类没有初始化,则需要先触发其父类的方法。(接口的初始化不会初始化父接口,只有真正使用父接口才会初始化父接口)
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含有main方法的类),虚拟机会先初始化这个主类。

类加载的过程

  • 加载:加载阶段需要将外部的二进制字节流按照特定的格式转化存储在方法区中,此阶段需要完成三件事情:

    1. 通过一个类的全限定名来获取定义此类的二进制字节流。
    2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
    3. 在Java堆里生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。
  • 验证:此阶段的目的在于确保二进制字节流符合class文件的存储格式。可以通过参数-Xverify:none参数关闭大部分类验证措施,验证包含四个阶段的检验过程:

    1. 文件格式验证:此阶段主要验证字节流是否符合class文件格式的规范,是否能被当前版本的虚拟机处理,保证字节流能正确解析并存储于方法区中,之后的验证就会基于方法区的存储结构进行。
    2. 元数据验证:主要是对类的元数据信息进行语义校验,保证不存在不符合Java语言规范的元数据信息,例如这个类是否有父类、这个类的父类是否继承了不允许被继承的类、如果这个类不是抽象类,是否实现了其父类中要求实现的所有方法等等。
    3. 字节码验证:此阶段将对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为,例如在操作栈中放置一个int类型的数据,使用时却按照long类型来加载入本地变量表、保证跳转指令不会跳转到方法体以外的字节码命令上、保证方法体中类型转换是有效的,如子类对象赋值给父类类型等。
    4. 符号引用验证:此校验发生在符号引用转化为直接引用的时候,这个转化动作在解析阶段中发生,目的是保证解析动作能正常执行,校验内容如:符号引用中的字符串全限定名能否找到对应的类、在指定类中是否存在引用的对应方法或字段、符号引用的字段和方法是否能被当前类访问等等。
  • 准备:目的是为类变量分配内存并设置类变量的初始值,这些内存都在方法区中进行分配,内存分配只给类变量进行分配,不包括实例变量,赋值规则为 static int i=3;这里只将i赋值为0,真正赋值操作在初始化的阶段,特殊情况下,如果该字段中的字段属性表有ConstantValue属性,那么此阶段value值就会被初始化为ConstantValue属性所指定的值,也就是 public static final int value = 5;那么value的值会直接赋值为5。

  • 解析:目的是将常量池内的符号引用替换为直接引用,虚拟机没有规定解析阶段发生的具体时间,只要求在执行anewarray、checkcast、getfield、getstatic、instanceof、invokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual、ldc、ldc_w、multianewarray、new、putfield、putstatic这16个字节码命令之前执行解析动作。第一次的解析结果会被缓存(除invokedynamic),以减少重复解析。

    1. 符号引用和直接引用区别:
      • 符号引用:符号引用是以一组符号来描述引用的目标,符号可以使任何形式的字面量,只要能准确定位到目标即可。这个目标并不一定要已经加载到内存中。
      • 直接引用:直接应用是可以直接指向目标的指针、相对偏移量或这个目标的句柄。也就是说如果是直接引用,那么这个目标必定已经要在内存中存在。也就是这个目标需要被加载进内存。
    2. 解析的具体内容包括:①类或接口的解析;②字段解析;③类方法解析;④接口方法解析
  • 初始化:目的是为了对类变量进行真正的赋值动作和执行静态语句块中的代码,实际上虚拟机会自动收集类变量赋值和静态语句块的代码合并产生clinit()方法,从而执行clinit()方法(此方法与类构造器不同),虚拟机保证执行子类的clinit()方法前执行父类的clinit()方法(接口除外,只有父接口中定义的变量被使用时父接口才会执行clinit()方法)。

类加载器

虚拟机设计的时候将“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到了java虚拟机外部去实现,让应用程序自己决定如何去获取所需要的类。成为了java语言流行的重要原因之一。

  • 类加载器的层次结构
    我们的应用程序通常是由3种类加载器互相配合进行加载的,如果有必要,还可以加入自己定义的类加载器,下面我们来依次介绍这几种加载器。

    1. 启动类加载器(Bootstrap ClassLoader):此类加载器是用C++实现的,是虚拟机自身的一部分,因此启动类加载器无法被java程序直接引用,它负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的jar包加载到虚拟机内存中,虚拟机是按照文件名识别jar包的,如rt.jar,名字不符合的jar包即使放入lib目录也不会加载。
    2. 扩展类加载器(Extension ClassLoader):这个加载器是java实现的,它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有jar包,开发者可以直接使用扩展类加载器。
    3. 应用程序类加载器(Application ClassLoader):这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,它扶着加载用户类路径(classpath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
  • 类与类加载器
    每一个类加载器,都拥有一个独立的类名称空间,也就是说比较两个类是否相等,只有这两个类是由同一个类加载器加载的前提下才有意义,只要加载一个同一个类的类加载器不同,那么这两个类就必定不相等。
    总结:两个类相等,一、类的完整类名必须一致。二、加载这个类的类加载器必须相同。

  • 全盘负责特点:当一个ClassLoader加载一个类的时候,除非显示的去用另外一个ClassLoader,则该类所依赖及引用的类都将由这个ClassLoader载入。

  • 双亲委派模型:类加载器之间其实有对应的父子关系。
    在这里插入图片描述
    如图所示,除了启动类加载器外,其余的类加载器都有自己的父类加载器,不过类加载器之间是使用组合关系来复用福类加载器的代码。
    双亲委派模型是在jdk1.2期间被引入到java中的,它并不是一个强制性的约束模型,而是推荐的一种类加载实现方式。

    1. 双亲委派模型原理:如果一个类收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求转发给父类加载器去完成,直到传送到顶层的启动类加载器,当父类加载器无法完成加载请求(它的搜索范围没有找到该类)时,子类加载器才会去尝试加载。

    2. 双亲委派模型好处:Java类随着它的类加载器一起带有了优先级的层次关系,如类java.lang.Object,它存放在rt.jar中,无论哪个类加载器要加载这个类,最后都会将请求转发到启动类加载器进行加载,从而保证了Object类在程序的各种类加载器环境都是一个类,如果没有使用双亲委派模型的话,当用户编写一个java.lang.Object类,由默认加载器加载的话,系统中就会出现多个类加载器的Object类,最后就会造成应用程序可能被破坏。

    3. 双亲委派模型代码实现

      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) {
                  long t0 = System.nanoTime();
                  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.
                      long t1 = System.nanoTime();
                      c = findClass(name);
                      // 在父类加载器无法加载的时候,在调用自身findClass方法进行类加载
                      // this is the defining class loader; record the stats
                      sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                      sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                      sun.misc.PerfCounter.getFindClasses().increment();
                  }
              }
              if (resolve) {
                  resolveClass(c);
              }
              return c;
          }
      }
      
  • 破坏双亲委派模型:双亲委派模型并不是一个强制性的约束模型,而是java设计者推荐开发者的类加载实现方式,但是历史上主要出现过3次规模较大的被破坏的情况。

    1. 第一次破坏:因为双亲委派模型在JDK1.2之后才引入的,而类加载器在JDK1.0就存在,因此为了兼容之前的版本,ClassLoader新加入了findClass()方法,将loadClass()中写入双亲委派的代码,但是以前的自定义类加载器目的就是重写loadClass()方法,因此破坏了双亲委派模型。
    2. 第二次破坏:由于这个模型的自身缺陷问题,导致如果启动类加载器中加载的类(如rt.jar中的代码)需要调用应用程序的代码,但是启动类加载器的加载范围内却不能找到这些应用程序代码,从而产生了问题,Java设计团队只好引入了一个线程上下文类加载器,它可以通过java.lang.Thread类的setContextClassLoaser()方法进行设置,如果创建线程的时候没有设置,它会从父类线程中继承一个,如果应用程序范围都没有设置过的话,这个类加载器默认就是应用程序类加载器(ApplicationClassLoader),有了这个之后,在加载的类中就可以显式获取这个类加载器来进行应用程序代码的加载,其实直接使用getSystemClassLoader()获取应用程序类加载器也是可以的,但是不同的服务自身的类加载器可能不同,所以才引入这个线程上下文类加载器,例如JDBC、JNDI都是采用这种方式。
    3. 第三次破坏:第三次破坏是由于用户对程序动态性的追求导致的,例如:代码热替换、模块热部署。在OSGI模块化的标准下,每一个Bundle都有自己的类加载器,当需要更换一个Bundle时,就把Bundle和类加载器一起换掉,类加载器也不再是双亲委派模型的树状结构,而是变成了网状结构,加载流程也发上了改变。
阅读更多
想对作者说点什么?
下载

深入Java虚拟机_002_深入详解JVM之类加载器深度剖析、根、扩展及系统类加载器

12-09
深入Java虚拟机_002_深入详解JVM之类加载器深度剖析、根、扩展及系统类加载器
下载

Java虚拟机

04-17
北京圣思园深入Java虚拟机_001_深入详解JVM之类加载器深度剖析、类的主动使用、被动使用\

博主推荐

换一批

没有更多推荐了,返回首页

开发者调查
AI开发者大会日程曝光
全场课程、电子书5折起
注册
友情链接:大学生交友app叫沙漠  免费的交友软件app  香港人用什么交友app  听声音交友的app  恋交友app是真的吗  国内交友软件app排行  什么app视频交友  适合中年的交友app  语爱交友app是真的吗  语爱交友app是真的吗  免费交友软件app排行  交友app  熟女交友app  推荐兴趣交友app  同性恋交友app有哪些  女同拉拉交友app  恋爱交友app  目前人数最多的交友app  陌生交友亚洲app  免费的聊天交友app