类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。  

类的加载并不是首次主动使用时加载,而是JVM在“预料”到某个类将要使用时预先加载。如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器在程序首次主动使用该类时才报错(LinkageError错误),如果这个类一直没有被主动使用,那么类加载器就不会报错。

加载.class文件时,可以从本地直接加在;可以从zip包中读取;可以运行时动态代理生成;也可以从网络加载;及其他文件转换(如jsp)。

 

JVM类加载分五个阶段:加载,验证,准备,解析,初始化。

加载、验证、准备和初始化这四个阶段发生的顺序是固定的,而为了支持动态绑定,解析阶段在某些情况下可以在初始化阶段之后开始。同时这几个阶段是按顺序开始,而不是按顺序进行或完成,这些阶段通常都是互相交叉地混合进行的,在一个阶段执行的过程中调用或激活另一个阶段。

加载:这个阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的入口。

  • 获取其定义的二进制字节流
  • 将字节流所代表的静态存储结构转化为方法区的运行时数据结构
  • 在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口

验证:验证Class文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverify:none参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

  • 文件格式验证
  • 元数据验证 :保证字节码描述的信息符合Java语言规范
  • 字节码验证 :保证程序语义是合法的、符合逻辑的
  • 符号引用验证 :保证解析动作能正确执行

准备:正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间。

  此时进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中

  这里的初始值是数据类型默认的零值(如0、null、false等)而不是代码里面的初始化参数时所赋的值。

  • 对基本数据类型来说,对于类变量(static)和全局变量,如果不显式地对其赋值而直接使用,则系统会为其赋予默认的零值,而对于局部变量来说,在使用前必须显式地为其赋值,否则编译时不通过
  • 对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过
  • 只被final修饰的常量则既可以在声明时显式地赋值,也可以在类初始化时显式地为其赋值,即在使用前必须为其显式地赋值,系统不会为其赋予默认零值
  • 对于引用数据类型reference来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋值null
  • 如果在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的零值

解析:解析阶段主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用,虚拟机将常量池中的符号引用替换为直接引用。

  符号引用以一组符号来描述所引用的目标,这些信息必须足以唯一的识别一个类、字段、方法;直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。

  不同jvm的内存布局可能不同,直接引用与jvm的内存布局相关,而符号引用与内存布局不相关。

初始化:初始化阶段是执行类构造器<clint>方法的过程。<clint>方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证<clint>方法执行之前,父类的<clint>方法已经执行完毕。

  • 子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
  • 定义对象数组,不会触发类的初始化。
  • 常量在编译期间会存入调用类的常量池中,不会触发定义常量所在的类。
  • 通过类名获取Class对象,不会触发类的初始化。
  • 通过Class.forName加载指定类时,如果指定参数initialize为false时,不会触发类初始化。
  • 通过ClassLoader默认的loadClass方法,不会触发初始化。

 

类加载器

  JVM提供了三种类加载器:

  • 启动类加载器(Bootstrap ClassLoader):负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类,由C++实现,不是ClassLoader的子类。
  • 扩展类加载器(Extension ClassLoader):负责加载 JAVA_HOME\lib\ext 目录中的,或通过java.ext.dirs系统变量指定路径中的类库,java实现,ClassLoader的子类。
  • 应用程序类加载器(Application ClassLoader):负责加载用户路径(classpath)上的类库,java实现,ClassLoader的子类。

  以及自定义类加载器,应用程序根据自身需要自定义的类加载器。

 

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

采用双亲委派模型,不管是哪个加载器加载这个类,最终都是委托给顶层类加载器进行加载,防止内存中出现多份同样的字节码。

双亲委派机制仅仅是java 规范所推荐的一种实现方式,并不是强制性的要求,如OSGI就采用网络图庄的类加载模型,而非双亲委派机制。相比而言网状的类加载模型比双亲委派的层次化模型在加载类时效率更高,无需在classpath一长串列表中进行查找;同时错误提示更加友好。