虚拟机类加载过程

(一)概要:

1、虚拟机如何加载Class文件

2、Class文件加载到虚拟机以后都会发生什么变化

(二)类加载机制

1、综述

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

2、类的生命周期


虚拟机类加载过程_第1张图片

(1)加载-验证-准备-初始化必须按照严格的先后顺序开始(注意是开始)。而解析在某些情况下可能会在初始化之后进行。

(2)Java虚拟机规范没有指明一个类需要在什么时候加载,但是指明了当且仅当如下情况会触发类的初始化

a、使用new关键字创建类的实例(创建对象数组除外,创建对象数组触发的是[lorg.fenixsoft.classloading.SuperClass的初始化)、读取或设置一个类的静态变量(非final)(注意:必须是这个类定义的静态变量,如果通过子类调用父类的静态变量,会初始化父类但不会初始化子类)、调用一个类的静态方法

b、使用java.lang.reflect包的方法对类进行反射调用时

c、当初始化一个类发现父类没有被初始化,则先初始化父类

d、当虚拟机启动时,用户需制定一个要执行的主类,虚拟机会先初始化这个类

e、当使用JDK1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle示例最后的解析结果是REF_getStatic/REF_putStatic/REF_invokeStatic方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先出发其初始化

3、类加载的过程

(1)加载

a、作用对象:二进制字节流/方法区中的存储结构

b、过程:

(a)使用类全限定名获取类的二进制字节流(获取方式:如从zip包获取,从网络中获取,运行时计算生成,由其他文件生成,从数据库获取等。虚拟机自由实现)

注:此阶段是开发人员可控性最强的阶段,通过定制类加载器自定义加载二进制流的方式。

特例:数组类。数据类不由类加载器加载,由虚拟机直接创建。类加载器加载其组件。如果组件是引用类型,则数组在加载该组件类型的类加载器类名称空间被标记。如果不是引用类型,则Java虚拟机将数组与引导类加载器关联。数组类的可见性与其组件的类型可见性一致,如果不是引用类型,则默认为public。

(b)将二进制字节流代表的静态存储结构转化为方法区的运行时数据结构

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

(2)验证

a、目的:为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求

b、过程:

(a)文件格式验证:验证字节流是否符合class文件格式的规范,并且能被当前虚拟机处理(魔术验证、主次版本号验证、常量池中的常量是否有不支持的类型,指向常量池的各种索引是否指向不存在的常量......)

目的:保证二进制字节流能够被正确解析并存储到方法区中。

操作对象:二进制字节流

(b)元数据验证:对字节码描述的信息进行语义分析,保证其描述的信息符合Java语言规范(包括这个类是否有父类,这个类是否继承了被final修饰的类,如果这个类不是抽象类,是否实现了父类所有的抽象方法,类中的字段、方法是否与父类产生了矛盾比如不规范的重载等......)

操作对象:方法区的存储结构

(c)字节码验证:通过数据流和控制流分析,对类的方法体进行校验,确认程序是合法的、符合逻辑的。(保证跳转指令不会调到方法体之外、保证方法体内的类型转换是有效的...)

(d)符号引用验证:发生在虚拟机将符号引用转化为直接引用(解析阶段)的时候。可以看做是对类自身以外的信息进行匹配性校验(符号引用中通过字符串描述的全限定名是否能找到对应的类,符号引用中的类、字段、方法是否可以被当前类访问到...)

目的:保证解析动作能正常执行。

(3)准备

正式为类变量(static修饰)分配内存(在方法区中)并设置类变量初始值(零值,final修饰的变量除外,final修饰的类变量在准备阶段即被赋予程序制定的值)

(4)解析

a、工作:虚拟机将常量池中的符号引用替换为直接引用

b、符号引用和直接引用:

    符号引用是用一组符号来描述所引用的目标。各虚拟机能接受的符号引用都是一致的,因为符号引用的字面量形式明确规定在Java虚拟机规范的class文件格式中。符号引用与虚拟机内存布局无关,引用的目标不一定已经在内存中存在。

    直接引用是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用和虚拟机实现的内存布局相关,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。有了直接引用则目标必定已经在内存中存在。

    发生时机:虚拟机规范中未规定,但是在如下操作符号引用的指令之前解析其所操作的符号引用(anewarray/checkcast/getfield/getstatic/instancsof/invokedynamic/invokeinterface/invokespecial/invokestatic/invokevirtual/ldc/ldc_w/multianewarray/new/putfield/putstatic)

c、多次解析

    对同一个文件多次解析,虚拟机需要保证在同一个实体中同一个符号引用每次解析要么都成功,要么都失败。

    除了invokedynamic,其他指令操作对象的解析结果可以被缓存。

(5)初始化(真正开始执行Java程序代码。前面的过程除了加载阶段用户可以通过自定义类加载器参与外均由虚拟机全程主导)

a、初始化阶段是执行类构造器()方法的过程

b、()方法

(a)()方法是由编译器自动收集类中的所有类变量(static)的赋值动作和静态语句块中的语句合并产生(收集顺序和语句在源文件出现的顺序相同)

(b)()方法和实例构造器init方法不同,不需要显示调用父类构造器,虚拟机会保证在子类的()执行之前父类的()已经执行完毕。也就是说虚拟机中第一个执行的()是java.lang.Object.

(c)()方法对类和接口来说不是必须的,如果没有static变量或者块,虚拟机可以不用为其生成()方法。

(c)接口只有static变量不会有static块儿,接口也有()方法。但是与类不同的是接口()的执行不需要其父接口的()方法已经执行。同样接口实现类的()方法执行也不要求接口的()已经执行。

(d)虚拟机会保证多线程情况下()方法被安全执行。

4、类加载器

(1)工作:通过一个类的全限定名来获取描述此类的二进制字节流

(2)类与类加载器的关系

    每一个类加载器都有其独立的类名称空间。因此对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性。同一个Class文件只有被同一个类加载器加载,最终才是相等的类。

(3)双亲委派机制

a、类加载器的划分:

    (a)从虚拟机角度:启动类加载器(虚拟机自带),其他类加载器(Java程序实现)

    (b)从开发者角度:

        启动类加载器(负责加载\lib下面的类到内存中)。启动类加载器不能被Java程序直接引用,如果用户需要将加载请求委托给启动类加载器直接使用null代替。

        扩展类加载器(负责加载\lib\ext下面的类或者被Java.ext.dirs系统变量所指定的路径中的类到内存中),开发者可以直接使用扩展类加载器

        应用程序类加载器(负责加载应用程序类路径下的类到内存中),由sun.misc.Launcher$AppClassLoader实现,是ClassLoader的getClassLoader()方法的返回值。如果没有指定类加载器加载某个类,默认就是这个类加载器加载。

b、类加载器的双亲委派模型(Parents Delegation Model)

(a)定义:除了启动类加载器(bootstrap Classloader),所有类加载器均需要有自己的父类加载器。父子关系不是以继承(Inheritance)的方式实现,而是组合(composition)关系调用父加载器的代码。

(b)工作过程:类加载器收到加载请求不会直接加载,而是先委托给父加载器,如此进行下去,如果最后没有类加载器可以加载此类(搜索范围内找不到)则尝试自己加载。

(c)特点:Java类随着其加载器具备了一种带有优先级的层次关系。例如java.lang.Ojbect(rt.jar中),无论谁要加载它最终都会委托给启动类加载器加载。保证java程序的稳定性。

虚拟机类加载过程_第2张图片

(4)双亲委托机制被破坏的几次情况

a、源于模型本身。模型出现在jdk1.2以后,此前ClassLoader已经存在,开发者使用覆盖其loadClass方法的方式定义自己的类加载过程。为了既满足双亲委托模型,又让开发者可以自定义类加载过程。jdk1.2之后定义了findClass方法,建议开发者通过重写findCLass方法,在父加载器没有加载成功后调用自身的findClass方法进行类加载。

b、模型自身缺陷。如果基础类(启动类加载器加载)需要调用用户代码,使用线程上线文类加载器,(通过setContextClassLoader,默认为应用程序类加载器)利用线程上下文类加载器加载所需要的SPI代码。

c、用户程序动态性追求导致,希望能够热部署、热生效等。

(三)tomcat对类加载机制的扩展

1、背景

(1) 一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。

(2) 部署在同一个web容器中相同的类库相同的版本可以共享。

(3)web容器也有自己依赖的类库,不能于应用程序的类库混淆。

(4) web容器要支持jsp的修改,我们知道,jsp 文件最终也是要编译成class文件才能在虚拟机中运行,但程序运行后修改jsp已经是司空见惯的事情。

2、默认类加载器不能满足如上需求

(1)如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,默认的累加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份

(2)第二个问题,默认的类加载器是能够实现的,因为他的职责就是保证唯一性。

(3)第三个问题和第一个问题一样。

(4)第四个问题,我们想我们要怎么实现jsp文件的热修改,jsp 文件其实也就是class文件,那么如果修改了,但类名还是一样,类加载器会直接取方法区中已经存在的,修改后的jsp是不会重新加载的。那么怎么办呢?我们可以直接卸载掉这jsp文件的类加载器,所以你应该想到了,每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载器。重新创建类加载器,重新加载jsp文件。

3、解决方案---自定义类加载器

(1)commonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;

(2)catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;

(3)sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;

(4)WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见。


虚拟机类加载过程_第3张图片

你可能感兴趣的:(虚拟机类加载过程)