第四章 虚拟机类加载机制

时间:2021-09-08
本文章向大家介绍第四章 虚拟机类加载机制,主要包括第四章 虚拟机类加载机制使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

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

  Java语言里面,类型的加载、连接和初始化过程都是在程序运行期间完成的,这种策略虽然会令类加载时稍微增加一些性能开销,但是会为Java应用程序提供高度的灵活性,Java里天生可以动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特点实现的。例如,如果编写一个面向接口的应用程序,可以等到运行时再指定其实际的实现类;用户可以通过Java预定义的和自定义类加载器,让一个本地的应用程序可以在运行时从网络或其他地方加载一个二进制流作为程序代码的一部分,这种组装应用程序的方式目前已广泛应用于Java程序之中。从最基础的Applet、JSP到相对复杂的OSGi技术,都使用了Java语言运行期类加载的特性。

  1. 类加载时机

  图7-1中,加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定( 也称为动态绑定或晚期绑定) 。
   

  虚拟机规范则是严格规定了有且只有6种情况必须立即对类进行“ 初始化” ( 而加载、验证、准备自然需要在此之前开始):

  1) 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:

  • 使用new关键字实例化对象的时候
  • 读取或设置一个类的静态字段( 被final修饰、 已在编译期把结果放入常量池的静态字段除外)的时候
  • 调用一个类的静态方法的时候。

  2) 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。

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

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

  5) 当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

  6) 当一个接口中定义了JDK8新加入的默认方法(被default关键字修饰的接口方法)时,如果这个接口的实现类发生了初始化,该接口要在其之前被初始化。

  除此之外,所有引用类型的方式都不会触发初始化,称为被动引用。  

package org.fenixsoft.classloading;
/**
* 被动使用类字段演示一:
* 通过子类引用父类的静态字段, 不会导致子类初始化
**/
public class SuperClass {
    static {
        System.out.println("SuperClass init!");
    } 
    public static int value = 123;
} 
public class SubClass extends SuperClass {
    static {
        System.out.println("SubClass init!");
    }
} /
**
* 非主动使用类字段演示
**/
public class NotInitialization {
    public static void main(String[] args) {
        System.out.println(SubClass.value);
    }
}                        

  上述代码运行后,除value值外,只会输出“SuperClass init!”。对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化。

package org.fenixsoft.classloading;
/**
* 被动使用类字段演示二:
* 通过数组定义来引用类, 不会触发此类的初始化
**/
public class SuperClass {
    static {
        System.out.println("SuperClass init!");
    } 
    public static int value = 123;
} 

public class NotInitialization {
public static void main(String[] args) {
        SuperClass[] sca = new SuperClass[10];
    }
}    

  上述代码运行后,并不会输出“SuperClass init!”。通过数组定义来引用类,不会触发此类的初始化。

package org.fenixsoft.classloading;
/**
* 被动使用类字段演示三:
* 常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化
**/
public class ConstClass {
    static {
        System.out.println("ConstClass init!");
    } 
    public static final String HELLOWORLD = "hello world";
} 
/**
* 非主动使用类字段演示
**/
public class NotInitialization {
    public static void main(String[] args) {
       System.out.println(ConstClass.HELLOWORLD);
    }
}        

  第三章提到过,static final修饰的常量会存入调用类的常量池中,没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。

  2. 类加载过程

  2.1 加载

  在加载阶段,Java虚拟机完成三件事情:

  1) 通过一个类的全限定名来获取定义此类的二进制字节流。

  2) 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

  3) 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

  由于并没有明确定义从哪里以及如何获取此类的二进制字节流,出现了各种各样的获取方式,很多重要的java基于建立在这一基础之上,例如:

  • 从ZIP压缩包中读取,这很常见,最终成为日后JAR、EAR、WAR格式的基础。
  • 从网络中获取,这种场景最典型的应用就是Web Applet。
  • 运行时计算生成,这种场景使用得最多的就是动态代理技术,在java.lang.reflect.Proxy中,就是用了ProxyGenerator.generateProxyClass()来为特定接口生成形式为“*$Proxy”的代理类的二进制字节流。
  • 由其他文件生成,典型场景是JSP应用,由JSP文件生成对应的Class文件。
  • 从数据库中读取,这种场景相对少见些,例如有些中间件服务器(如SAP Netweaver)可以选择把程序安装到数据库中来完成程序代码在集群间的分发。
  • 可以从加密文件中获取,这是典型的防Class文件被反编译的保护措施,通过加载时解密Class文件来保障程序运行逻辑不被窥探。

  2.2 验证

  验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求。Class文件并不一定只能由Java源码编译而来,在二进制编辑器中敲出的伪Class文件也可以通过Java虚拟机加载。为了避免载入有错误或有恶意企图的字节码流而导致整个系统受攻击甚至崩溃,Java虚拟机会对字节码进行验证。

  1) 文件格式验证  验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。

  2) 元数据验证      对字节码描述的信息进行语义分析。

  3) 字节码验证      通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的。

  4) 符号引用验证   该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。 

  2.3 准备

  准备阶段正式为类中定义的变量(静态变量)分配内存并设置类变量初始值的阶段,通常情况下,静态变量的初始值为零值。如果是final修饰的静态变量,如 public static final int value = 123,类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量值就会被初始化为ConstantValue属性所指定的初始值。

   

  2.4 解析

  解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程。符号引用以一组符号来描述所引用的目标;直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。

  2.5 初始化

  初始化时机在第一章,序言介绍过,这里不再赘述。

  3. 类加载器

  Java虚拟机设计团队有意把类加载阶段中的通过一个类的全限定名来获取描述该类的二进制字节流这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为类加载器Class Loader) 。

  3.1 类与类加载器

  对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性。

  3.2 双亲委派模型

  JDK 9 之前的类加载器双亲委派模型如下图所示:  

   

  • 启动类加载器Bootstrap Class Loader):这个类加载器负责加载存放在<JAVA_HOME>\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,而且是Java虚拟机能够识别的(按照文件名识别,如rt.jartools.jar,名字不符合的类库即使放在lib目录中也不会被加载) 类库加载到虚拟机的内存中。 
  • 扩展类加载器Extension Class Loader):负责加载<JAVA_HOME>\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库。 是一种Java系统类库的扩展机制,JDK的开发团队允许用户将具有通用性的类库放置在ext目录里以扩展Java SE的功能,在JDK9之后,这种扩展机制被模块化带来的天然的扩展能力所取代。
  • 应用程序类加载器(Application Class Loader) : 这个类加载器由sun.misc.Launcher$AppClassLoader来实现。它负责加载用户类路径(ClassPath) 上所有的类库,开发者同样可以直接在代码中使用这个类加载器。如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类

  双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。不过这里类加载器之间的父子关系一般不是以继承(Inheritance)的关系来实现的,而是通常使用组合(Composition)关系来复用父加载器的代码。

  如果一个类加载器收到了类加载的请求,它首先不把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。

  使用双亲委派模型的一个好处是Java中的类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,存放在rt.jar中,无论哪个类加载器要加载这个类,都是委派给最顶端的启动类加载器进行加载,因此Object类在程序中的各种类加载器环境中都能保证是一个类。

  双亲委派很好地解决了各个类加载器协作时基础类型的一致性问题(越基础的类由越上层的加载器进行加载),基础类型之所以被称为基础,是因为它们总是作为被用户代码继承、调用的API存在。可是如果有基础类型需要调用用户代码,该怎么办呢?

  一个典型的例子便是JNDI服务,JNDI现在已经是Java的标准服务,它的代码由启动类加载器来完成加载(在JDK 1.3时加入到rt.jar的),肯定属于Java中很基础的类型了。但JNDI存在的目的就是对资源进行查找和集中管理,它需要调用由其他厂商实现并部署在应用程序的ClassPath下的JNDI服务提供者接口(Service Provider InterfaceSPI)的代码,
动类加载器是绝不可能认识、加载这些代码的。JDK提供了java.util.ServiceLoader类,以META-INF/services中的配置信息,辅以责任链模式,给SPI的加载提供了一种解决方案。

  4. Java模块化系统

  JDK 9中引入的Java模块化系统(Java Platform Module SystemJPMS不仅仅像之前的JAR包那样只是简单地充当代码的容器,除了代码外,Java的模块定义还包含以下内容:

  • 依赖其它模块的列表
  • 导出的包列表,即其他模块可以使用的列表
  • 开放的包列表,即其他模块可反射访问模块的列表
  • 使用的服务列表
  • 提供服务的实现列表

  JAR文件在类路径的访问规则: 所有类路径下的JAR文件及其他资源文件,都被视为自动打包在一个匿名模块(Unnamed Module)里,它可以看到和使用类路径上所有的包、JDK系统模块中所有的导出包,以及模块路径上所有模块中导出的包。

  模块在模块路径的访问规则: 模块路径下的具名模块(Named Module)只能访问到它依赖定义中列明依赖的模块和包, 具名模块看不见传统JAR包的内容。

  JAR文件在模块路径的访问规则: 如果把一个传统的、不包含模块定义的JAR文件放置到模块路径中,它就会变成一个自动模块(Automatic Module) 。尽管不包含module-info.class,但自动模块将默认依赖于整个模块路径中的所有模块,因此可以访问到所有模块导出的包,自动模块也默认导出自己所有的包。

  JDK 9中虽然仍然维持着三层类加载器和双亲委派的架构,但类加载的委派关系也发生了变动。当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载。   

   

原文地址:https://www.cnblogs.com/muxianbai/p/15240407.html