类的加载机制

类加载过程

当程序主动使用某个类时,如果该类还未被加载到内存中,则系统会通过加载、连接(包括验证、准备、解析)、初始化这几个步骤来对该类进行加载。如无意外,JVM通常会连续完成这几个步骤,所以这几个步骤统称为类加载或类初始化。

加载

加载是类加载过程的第一个阶段,在加载阶段,虚拟机需要完成以下三件事情:

  1. 通过一个类的全限定名来获取其定义的二进制字节流。
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在 Java 堆中生成一个代表这个类的 Class 对象,作为方法区中各种数据的访问入口。

相对于类加载的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。

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

  1. 从文件系统读取 .class 文件
  2. 从 jar 包获取
  3. 从网络中获取,最典型的应用是 Applet
  4. 由 jsp 文件生成

验证

验证过程保证 class 文件的正确性和安全性,确保 class 文件中的字节流包含的信息符合当前虚拟机的要求,并且不会危害虚拟机的安全。

验证过程包括以下四个方面:

  • 文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
  • 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。
  • 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
  • 符号引用验证:确保解析动作能正确执行。

准备

为类的静态变量(static修饰的变量)分配内存,这些内存在方法区中分配,并初始化为默认值。此时不会分配实例变量的内存,实例变量会在对象实例化时随着对象一块分配在Java堆中。

注意:

这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。例如:public static int value = 3;那么变量value在准备阶段过后的初始值为0,而不是3,把value赋值为3的动作在初始化阶段才会执行。

如果类字段的字段属性表中存在ConstantValue属性,即同时被final和static修饰,那么在准备阶段变量value就会被初始化为ConstValue属性所指定的值。例如: public static final int value = 3; 编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为3。

解析

虚拟机将常量池的符号引用替换为直接引用

  • 符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以使用任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标不一定已经加载到内存中
  • 直接引用(Direct References):直接引用可以是直接指向目标的指针,相对偏移量或是一个能简介定位到目标的句柄,直接引用是与虚拟机实现的内存布局相关的,统一符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同,如果有了直接引用,那引用的目标必定已经在内存中存在

初始化

初始化阶段是类加载的最后一步,主要执行 java 代码,进行相关初始化操作,例如为类的静态变量赋予正确的初始值。此时如果其父类还没有被加载,其先加载其父类。

如果多个线程同时初始化一个类,只会有一个线程能执行初始化操作。因为 JVM 保证了类的初始化在多线程环境下被正确的加锁和同步。

类加载的时机

只有当对类的主动使用的时候才会进行类的加载,类的主动使用包括以下六种:

  • 创建类的实例,也就是new的方式
  • 访问某个类或接口的静态变量,或者对该静态变量赋值
  • 调用类的静态方法
  • 反射(如Class.forName(“com.shengsiyuan.Test”))
  • 加载某个类的子类时,其父类也会被加载
  • Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类

类加载器

简介

  • 类加载器负责将.class文件(可能在磁盘上,也可能在网络上)加载到内存中,并为之生成对应的java.lang.Class对象。
  • 在java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。这意味着两个不同的类加载器(即使是同一类型的加载器,但是如果是两个不同的对象也算是不同的加载器)所加载的同名类也是不同的。

层级关系

注意:这里父类加载器并不是通过继承关系来实现的,而是采用组合实现的。

  • 站在Java虚拟机的角度来讲,只存在两种不同的类加载器:启动类加载器:它使用C++实现(这里仅限于Hotspot,也就是JDK1.5之后默认的虚拟机,有很多其他的虚拟机是用Java语言实现的),是虚拟机自身的一部分;所有其他的类加载器:这些类加载器都由Java语言实现,独立于虚拟机之外,并且全部继承自抽象类java.lang.ClassLoader,这些类加载器需要由启动类加载器加载到内存中之后才能去加载其他的类

  • 站在Java开发人员的角度来看,类加载器可以大致划分为以下三类:

  1. 启动类加载器:Bootstrap ClassLoader,负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。
  2. 扩展类加载器:Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,Bootstrp loader加载ExtClassLoader,并且将ExtClassLoader的父加载器设置为Bootstrp loader。它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。
  3. 应用程序类加载器:Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,Bootstrp loader加载完ExtClassLoader后,就会加载AppClassLoader,并且将AppClassLoader的父加载器指定为 ExtClassLoader。它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

类加载机制

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

双亲委派模型

工作流程

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

意义

保证了 API 的安全性,防止 JDK 的代码被修改。例如 java 开头的类均由启动类加载器加载,这样即使我们写了一个同名的 String 类,根据双亲委派模型,先由启动类加载器加载,而启动类加载器发现已经加载过该类,就还是使用之前加载的 String 类,也就是使用 JDK 的 String 类。

假如我们自己写了一个java.lang.String的类,我们是否可以替换调JDK本身的类?

答案是否定的。我们不能实现。为什么呢?正是因为有双亲委派模型,由于根加载器之前已经加载过String类,所以现在程序中只有一个 String 类。但是如果说双亲委托机制可以解决这个问题,其实不是非常的准确。因为双亲委托机制是可以打破的,你完全可以自己写一个classLoader来加载自己写的java.lang.String类,但是你会发现也不会加载成功,具体就是因为针对java.*开头的类,jvm的实现中已经保证了必须由bootstrp来加载。

参考

-------------    本文到此结束  感谢您的阅读    -------------
0%