Java虚拟机 第七章之虚拟机类加载机制

#7.1 概述

问题:
		1.虚拟机如何加载class文件? 
		2.class文件中的信息进入到虚拟机后会发生什么变化?

什么是虚拟机类加载机制? 书中写到:虚拟机把描述类的数据从class文件中加载到内存,并对数据进行验证、转化解析和初始化,最终形成被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。

在Java语言中,类的加载、连接和初始化过程都是在程序运行期间完成的,这种策略虽然会令类加载时稍微增加一些性能开销,但是会为Java应用程序提供高度的灵活性,Java里天生可以动态扩展的语言特性就是依赖运行期间动态加载和动态连接这个特性实现的。

#7.2 类加载时机
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:
加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)7个阶段。其中验证、准备、解析3个部分统称为连接(Linking)
其中加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始(这里说的是按部就班的"开始",并不是按部就班的"执行"或“完成”,因为这些阶段通常都是相互交叉地混合式进行的,通常会在一个阶段执行的过程中调用、激活另一个阶段),而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。

对于什么情况下需要开始类加载过程的第一个阶段:加载? Java虚拟机中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把握。但是对于初始化这个阶段,虚拟机规范则是严格规定了有且只有5种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始);

  1. 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。对应的场景为:
    – A.使用new关键字实例化对象的时候
    – B.读取一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)
    – C.设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)
    – D.调用一个类的静态方法的时候
  2. 使用java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化则需要先触发其初始化。
  3. 当初始化一个类的时候,如果发现其父类还没有进行初始化的时候,则需要先触发其父类的初始化。
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  5. 当使用JDK1.7 的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例租后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的的类没有进行初始化,则需要先触发其初始化。

这些触发类初始化的场景,虚拟机中使用了一个很很强烈的限定语:“有且只有”,这些场景中个的行为被称为:对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称之为被动引用。

被动引用举例:
1) 通过子类引用父类的静态字段,父类会进行初始化,但不会导致子类初始化。
2) 通过数组定义来引用类,不会触发此类的初始化。
3) 常量的引用,不会导致该类初始化(常量在编译阶段会存放到常量池中,本质上并没有直接引用到定义常量的类上)

class SuperClass {
	static {
		System.out.println(" inti SuperClass now !");
	}
	public static int value = 123;
	public static final int finalvalue = 11111;
}
class SubClass extends SuperClass {
	static {
		System.out.println(" inti SubClass now !");
	}
}
public class NewStaticDemo {
	// 主动引用 静态变量 所以进行初始化了 输出为: inti SuperClass now !
	@Test
	public void testValueBySuperClass() {
		System.out.println(SuperClass.value);
	}
	// 主动引用 父类的静态变量 所以父类进行初始化了 输出为: inti SuperClass now !
	@Test
	public void testValueBySubClass() {
		//
		System.out.println(SubClass.value);
	}
	// 主动引用 new SubClass时 触发初始化 而其父类未初始化 所以先父类初始化然后才到 SubClass
	// 所以输出为: inti SuperClass now ! inti SubClass now !
	@Test
	public void testValueByNew() {
		System.out.println(new SubClass());
	}
	// 主动引用 new SubClass时 触发初始化 而其父类未初始化 所以先父类初始化然后才到 SubClass
	// 所以输出为: inti SuperClass now ! inti SubClass now !
	@Test
	public void testValueByInstance() {
		SuperClass superClass = new SubClass();
	}
	// 被动引用:通过数组定义引用类,不会触发初始化!!!!!!
	@Test
	public void testByArray() {
		SuperClass[] superClasses = new SuperClass[10];
	}
	// 被动引用:常量在编译阶段放入常量池,本质上没有直接引用到定义常量的类,因此不用触发定义常量类的初始化!!!!!!
	@Test
	public void testFinal() {
		System.err.println(SuperClass.finalvalue);
	}

}

注:接口与类的加载过程大多是相似的,但第3种情况(当初始化一个类的时候,如果发现其父类还没有进行初始化的时候,则需要先触发其父类的初始化),接口并不要求父接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化

#7.3 类的加载过程 之 加载

加载:Java虚拟机中类加载过程的第一个开始的阶段,其中虚拟机需要完成3件事情
1) 通过一个类的全限定名来获取定义此类的二进制字节流。
2)将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构。
3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

  在此阶段加载一个非数组类与加载一个数组类的操作时不同的,当加载一个非数组类的时候既可以使用系统提供的引导类加载器来完成,也可以由用户自定义的类加载器去完成(开发人员可以通过定义自己的类加载器去控制字节流的获取方式,即重写一个类加载器的loadClass()方法)。
  而对于数组类,数组类本身不通过类加载器创建,它是由Java虚拟机直接创建的。但数组类与类加载器任然很有密切关系,因为数组类的元素类型最终还是要靠类加载器去创建,一个数组类(简称C)创建过程遵循以下规则:
 1)如果一个数组类的组件类型(Component Type,指的是数组去掉一个维度的类型)是引用类型,那就递归采用非数组类的加载过程去加载这个组件类型,数组C将在加载该组件类型的类的类加载器的类名称空间上被标识(一个类必须与类加载器一起确定唯一性)。
 2)如果数组的组件类型不是引用类型(如int[]数组),Java虚拟机将会把数组C标记为与引导类加载器关联。
 3)数组类的可见性与它的组件类型的可见性一致,如果组件类型不是引用类型,那数组类的可见性将默认为public。

 在加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式有虚拟机实现自行定义,虚拟机规范未规定此区域的具体数据结构。然后在内存中实例化一个java.lang.Class类的对象,作为方法区这个类的各种数据的访问入口。

注:加载阶段与连接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段还未完成,连接阶段可能已经开始了,但这些夹在夹在阶段之中进行的动作,仍然属于连接阶段的内容,这两个阶段的开始时间仍然保留着固定的先后顺序。

#7.4 类的加载过程之 验证

 验证是连接阶段的第一步,这一阶段的目的是为了保证class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。从整体上这个阶段大致上会有4个阶段的验证动作:文件格式验证、元数据验证、字节码验证、符号引用验证
  1.文件格式验证: 验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。
  2.元数据验证:对字节码描述的信息镜像语义分析,以保证其描述的信息符合Java语言规范的要求。
  3.字节码验证:(最复杂的一个阶段)通过数据流和控制流分析,确定程序语义是合法的。符合逻辑的。
  4.符号引用验证:发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三个阶段-----解析阶段发生。符号引用验证可以看做对类自身以外(常量池中各种符号引用)的信息进行匹配性校验,确保解析动作能正常执行。

#7.5 类的加载过程之 准备

  准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的的内存都将在方法区中进行分配。
强调:
  1.类变量指的是被static修饰的变量
  2.上述的类变量不包含实例变量,实例变量将会在对象实例化的时随着对象一起分配在Java堆中,令这里的初始值对于基本类型一般是0或false,引用类型是null。

#7.6 类的加载过程之 解析
  解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用
  符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用是能无歧义地定位到目标即可。符号引用于虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。
直接引用
  可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用于虚拟机实现的内存布局有关,同一个符号引用在不同虚拟机实例翻译出来的直接引用一般不会相同。如果有了直接引用,那么引用的目标必定已经在内存中存在。
  解析动作主要针对:
  1) 类或接口解析
  2) 字段解析
  3) 类方法解析
  4) 接口方法解析
  5) 方法类型
  6) 方法句柄
  7) 调用点限定符

#7.7 类的加载过程之 初始化
  类加载过程的最后一步,真正开始执行类中定义的Java程序代码(或者说字节码)。在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者而已从另一个角度来表达:初始化阶段是执行类构造器< clinit >()方法的过程
   1)< clinit >()方法是有编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{})中的语句合并产生的,编辑器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。
   2)< clinit >()方法与类的构造函数不同,它不需要显示地调用父类构造函数,虚拟机会保证在子类的< clinit >()方法执行之前,父类的< clinit >()方法已经执行完毕。因此在虚拟机中第一个被执行的< clinit >()方法的类肯定是java.lang.Object。
   3) 由于父类的< clinit >()方法先执行,也就意味着弗雷中定义的静态语句块要优先于子类的变量赋值操作。
   4) < clinit >()方法对于类或接口来说并不是必须的,如果一个来中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成< clinit >()方法。
   5) 接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口也有< clinit >()方法,但接口与类不同的是:执行接口< clinit >()方法时不需要父接口的< clinit >()方法。只有当父接口中定义的变量使用时,父接口才会初始化。
   6) 虚拟机会保证一个类的< clinit >()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的< clinit >()方法,其他线程都是阻塞等待,直到活动线程执行< clinit >()方法完毕(其他线程进入后不会再次执行< clinit >()方法,同一个类加载器下,一个类型只会初始化一次)。

#7.8 类的加载过程之 类加载器
   对于任意一个类,都需要有加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。
  从Java虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器是使用C++语言实现,是虚拟机自身一部分;另一种就是所有其他类的类加载器,这些都是有Java语言实现,独立于虚拟机外部,并且全部继承抽象类java.lang.ClassLoader。

  从Java开发人员角度来看,类加载器还可以划分得更细致些:
  1) 启动类加载器(Bootstrap ClassLoader)
  2) 扩展类加载器(Extension ClassLoader)
  3) 应用程序类加载器(Application ClassLoader)

我们的程序都是有这3种类加载器相互配合进行加载的,如有必要,还可以加入自己定义的类加载器。其中类加载器的关系如下图:
类加载器双亲委派模型
类加载器双亲委派模型:
  要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承的关系来实现,而都是使用组合关系来复用父加载器的代码。
  双亲委派模型并不是一个强制的约束模型,而是Java设计者推荐给开发者的类加载器实现,在Java中大部分类加载器都遵循这个模型,但也有例外
  1)双亲委派模型是在JDK1.2之后才被引入,为了向前兼容。在java.lang.ClassLoader添加了一个新的protected方法findClass(),而这个方法的唯一逻辑就是调用自己的loadClass()
  2)由于模型的自身缺陷导致,双亲委派模型很好的解决了各个类加载器的基础类的统一问题(越基础的类越有上层的加载器进行加载),基础类之所以被称为“基类”,是因为它们总是作为被用户代码调用的API,但当基础类调用用户的代码,则会出现失败的问题,所以引入线程上下文类加载器(Thread Context ClassLoader),这个类可以通过java.lang.Thread类的setContextClassLoaser()方法进行设置。此类情况多用在JNDI、JDBC、JCE等
  3)由于用户对程序动态性的追求(如代码热替换,模块热部署等)。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值