虚拟机类加载机制(ClassLoader)

来源:互联网 发布:死亡岛有网络怎么联机 编辑:程序博客网 时间:2024/06/11 06:55

java中虚拟机类加载机制 (ClassLoader)

1、虚拟机类加载机制

(1)ClassNotFoundExcetpion
  Java.lang.ClassNotFoundExcetpion对于这个异常,它实质涉及到了java技术体系中的类加载。Java的类加载机制是技术体系中比较核心的部分,对其背后的机理有一定理解有助于我们排查程序中出现的类加载失败等问题。
(2)虚拟机类加载机制
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机类加载机制,在java语言里面,类的加载、连接和初始化过程都是在程序运行期间完成。
(3)类加载的时机
Java虚拟机规范中没有对加载进行强制约束,这点交由虚拟机具体实现来自由把握。虚拟机规定了有且只有5种情况必须立即对类进行“初始化”:
1)使用new关键字实例化对象的时候、读取或者设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外),以及调用一个类的静态方法的时候。
2)使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
3)当一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
4)当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机则会先初始化这个主类。
5)当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄时,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
除此之外的引用类的方法都不会触发初始化,称为被动引用:
1)通过子类引用父类的静态字段,不会导致子类的初始化;
2)通过数组定义来引用类,不会触发此类的初始化;
3)常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会出发定义常量的类的初始化。

2、类的加载过程

  一个java文件从被加载到虚拟机内存,到被卸载出内存为止这个生命过程,总共要经历5个阶段,JVM将类加载过程分为:
  加载->链接(验证+准备+解析)->初始化(使用前的准备)->使用->卸载

(1)加载

类加载过程
  首先通过一个类的全限定名来获取此类的二进制字节流;其次将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;最后在java堆中生成一个代表这个类的Class对象,作为方法区这些数据的访问入口。总的来说就是查找并加载类的二进制数据。
  类的加载的最终产品是位于堆区中的Class对象。Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。加载类的方式有以下几种:
  1)从本地系统直接加载
  2)通过网络下载.class文件
  3)从zip,jar等归档文件中加载.class文件
  4)从专有数据库中提取.class文件
  5)将Java源文件动态编译为.class文件(服务器)

(2)链接

验证:确保被加载类的正确性;确保Class文件的字节流中包含的信息符合当前虚拟机的要求。
1)文件格式验证:验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。
2)元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。
3)字节码验证:通过数据流和控制流分析,确定程序语义是合法的,符合逻辑的。
准备:为类的静态变量分配内存,并将其初始化为默认值;这些变量所使用的内存都将在方法区中分配;
 解析:把类中的符号引用转换为直接引用。
符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量。只要使用时能无歧义地定位到目标即可;
直接引用:可以是直接指向目标的指针、相对位移量或是一个能间接定位到目标的句柄。

(3)类的初始化

真正开始执行类中定义的Java程序代码(字节码)。
1. 类的初始化顺序:
  1)如果这个类还没有被加载和链接,那先进行加载和链接
  2)假如这个类存在直接父类,并且这个类还没有被初始化(注意:在一个类加载器中,类只能初始化一次),那就初始化直接的父类(不适用于接口)
  3)加入类中存在初始化语句(如static变量和static块),那就依次执行这些初始化语句。
  4)总的来说,初始化顺序依次是:(静态变量、静态初始化块)–>(变量、初始化块)–> 构造器;如果有父类,则顺序是:父类static方法 –> 子类static方法 –> 父类构造方法- -> 子类构造方法
在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段则是执行类构造器<\clinit>方法的过程。
2.<\clinit>方法
1)<\clinit>()方法是由编译器自动收集类中的所有变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定,静态语句块智能访问到定义在静态语句块之前的常量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。
public class Test{
static{
i = 0;
System.out.ptint(i); //“非法向前引用a”
}
static int i= 1;
}
2)<\clinit>()方法与类的构造函数不同,它不需要显示的调用父类构造函数,虚拟机会保证在子类的<\clinit>()方法执行之前,父类的<\clinit>()方法已经执行完毕。因此在虚拟机中第一个被执行的()方法的类肯定是java.lang.Object。
3)由于父类的<\clinit>()方法执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。
static class Parent{
public static int A = 1;
static {
A = 2;
}
}
static class Sub extends Parent{
public static int B = A;
}
static class void main(String[] args){
System.out.println(Sub.B);
}
4)<\clinit>()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的复制操作,那么编译器可以不为这个类生成<\clinit>()方法。
5)接口中不能使用静态语句块,但仍然有变量初始化的操作,因此接口和类一样都会生成<\clinit>()方法。但接口和类不同的是,执行接口<\clinit>()方法不需要先执行父接口的<\clinit>()方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的<\clinit>()方法。
6)虚拟机会保证一个类的<\clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程执行类的<\clinit>()方法,其他线程都需要阻塞等待,知道活动线程执行<\clinit>()方法完毕。

(4)加载器

从Java虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap CalssLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;另外一种就是所有其他的类加载器,这些类加载器都Java语言实现,独立于虚拟机外部,并且都继承自抽象类java.lang.ClassLoader。

(1)加载器介绍
1)Bootstrap ClassLoader(启动类加载器)
  负责加载JAVA_HOME中jre/lib/rt.jar里所有的class,加载System.getProperty(“sun.boot.class.path”)所指定的路径或jar。
2)Extension ClassLoader(标准扩展类加载器)
  负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包。载System.getProperty(“java.ext.dirs”)所指定的路径或jar。
3)Application ClassLoader(应用程序类加载器)
  负责记载classpath中指定的jar包及目录中class
4)Custom ClassLoader(自定义类加载器)
  属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现。
(2)双亲委派模型
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里的父子关系是以组合关系来复用父加载器的代码。
这里写图片描述
1)加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,保证此类只所有ClassLoader加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。
2)在加载类时,每个类加载器会将加载任务上交给其父,如果其父找不到,再由自己去加载。
3)Bootstrap Loader(启动类加载器)是最顶级的类加载器了,其父加载器为null。

原创粉丝点击