JVM笔记整理(第7章 虚拟机类加载机制)

时间:2022-12-21 10:01:09

资料来源:《深入理解java虚拟机》

 

这一章主要讲了2部分内容,其一是:类加载的整个过程。其二,几种类加载器及其工作原理。其实这一章的内容还是比较少而且简单的。

 

 

1、综述

 

1.1、首先,要对类加载机制有个整体上的认识。概括的说就是:虚拟机将class文件中描述类的数据加载到内存当中,并对数据进行校验、转换解析、初始化,最终形成可以被虚拟机直接使用的java类型。

 

1.2、整个的类生命周期包括7个阶段:加载、验证、准备、解析、初始化、使用、卸载。其中有5个阶段的顺序是确定的,这5个阶段开始顺序必须依次为:加载、验证、准备、初始化、卸载。所谓开始时间顺序是确定的,但不是说各自的结束时间也是顺次确定的。事实上,在执行过程中,这几个阶段的执行通常都是有交叉的进行的。

 

1.3、什么时候开始进行类加载?虚拟机规范没有明确规定。

 

1.4、什么时候必须进行类初始化?有且仅有5个情况

1.4.1、遇到new、getstatic、putstatic、invokestatic这4个指令时,如果对应的类没有进行初始化,则需要先触发这个类的初始化。

1.4.2、使用java.lang.reflect包进行类的反射调用时。如果该类没有进行初始化,则必须先触发这个类的初始化。

1.4.3、初始化一个类时,如果其父类还没有初始化,则需要先触发其父类的初始化。

1.4.4、虚拟机启动时,用户会指定一个要执行的主类。虚拟机会先初始化这个主类。

1.4.5、JDK1.7后,如果一个实例最终的解析结果为一个句柄(引用),并且这个句柄对应的类没有进行初始化,则需要先触发这个类的初始化。

 

1.5、主动引用:上述1.4中的5中情况,被称为对类的主动引用。

 被动引用:除此之外,所有引用类的引用都称为“被动引用”,自然也不会触发对类对初始化。

1.5.1被动引用的3个小例子:

A、通过子类引用父类的静态字段,不会引起子类的初始化。

B、数组的类型为一个类,声明数组时,不会触发此类的初始化。

C、被调用类中的常量在编译阶段会被存入调用类的常量池,本质上并没有直接引用到定义该常量的类。因此也不会触发定义该常量的类的初始化。

 

1.6、什么时候进行接口的初始化?和类的5种情况中只有第3种不一样。即为一个接口进行初始化时,不要求其父接口都已经完成了初始化。只有用到其父接口时,才对其父接口进行初始化。

 

 

2、类加载过程

共计5个阶段:加载、验证、准备、解析、初始化。

 

2.1、加载

 

2.1.1本阶段完成的工作(3个):

A、根据常量池中的CONSTANT_Class_info常量,来获取该类的全限定名。根据全限定名,获取定义此类的二进制字节流。

B、将这个静态的二进制字节流转化为方法区的运行时数据结构。

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

 

注1:读取到的类的二进制字节流不一定是从Class文件中读取的,还有其他几种可以获取二进制字节流的方式。比如,从zip包读取;从网络中获取;运行时计算生成等等。

注2:生成的java.lang.Class对象是存储在方法区的,虽然它是对象,但并不存在堆中。

 

2.1.2、类加载是否通过类加载器?分2种情况讨论

2.1.2.1、对于非数组类:可以由系统提供的引导类加载器加载,也可以由用户自定义的类加载器加载。

2.1.2.2、对于数组类:数组本身是有虚拟机直接创建的,不由类加载器创建。但要注意的是:数组类的元素类型是必须由类加载器进行创建的。

 

注1:数组创建过程遵循的原则有3个:

A、如果数组的组件类型(指去掉一个维度的类型)为引用类型,则递归调用2.1.1中的工作。

B、如果不是引用类型,则虚拟机将会把数组标记为与引导类加载器关联。

C、数组类可见性和他的组件类型一致。如果组件类型不是引用类型,则数组类型默认为public。

 

 

2.2、验证

 

2.2.1、本阶段功能:确保Class文件的字节流中包含的信息符合当前虚拟机的要求,且不会危害虚拟机自身的安全。

 

2.2.2、地位:非常重要,但非必须。这一阶段直接决定了java虚拟机是否能承受恶意代码的攻击。验证阶段的工作量在虚拟机的类加载子系统中占相当大一部分。

 

2.2.3、验证内容有4个方面:

 

2.2.3.1、文件格式验证:这一阶段是后面3个验证阶段的基础

A、是否符合Class文件格式;

B、是否能被当前版本的java虚拟机加载。

2.2.3.2、元数据验证

A、目的:进行语义检验,保证符合java语言规范。

2.2.3.3、字节码验证

A、地位:整个验证过程最复杂的阶段

B、目的:通过验证数据流和控制流,来确保程序语义是合法、符合逻辑的。

 

注1:由于数据流验证的高复杂性,此处是有一个优化要注意的。其实这在第6张的属性表里面已经提到过,Code属性的属性表里面有个属性StackMapTable。这项属性描述了方法体的所有基本块(按控制流拆分的代码块)开始时本地变量表和操作数栈应用的状态。类型检查替代了类型推导,时间自然就短了。

2.2.3.4、符号引用验证

A、功能:确保解析操作能正常执行。解析操作要完成功能是:将符号引用转为直接引用。

B、要检查内容:

★根据类的全限定名能否找到指定的类

★在指定类中是否存在符合方法的字段描述符以及简单名称所对应的字段和方法。

★符号引用中的类、字段、方法的访问性,是否可以被当前类访问。

 

 

2.3准备

 

2.3.1功能:

A、正式为类分配内存。

B、为类变量(static)赋初值

注1:这些变量分配内存位置:方法区

     这里的初值“通常”是零值

    如果变量被final修饰,则直接赋给定的值,没有赋零值的过程。

注2:实例变量分配内存位置:堆

    实例变量分配内存时间:类实例化时

 

2.4、解析

 

2.4.1、功能:将常量池内的符号引用转化为直接引用。

注1:符号引用:任何形式字面量都可以。只要能定位到目标。

注2:直接引用:直接指向目标的指针 or 相对偏移量 or 能间接定位到目标的句柄。

2.4.2:缓存功能:对同一个符号引用进行解析是常见的。所以对于第一次解析结果会进行缓存,从而避免重复操作。但除了invokedynamic指令。

 

2.5初始化

 

2.5.1、地位:类加载过程的最后一步。真正开始执行类定义的java代码。

2.5.2、功能:根据程序员的代码,为类变量和其他资源进行初始化。虽然准备阶段已经为类变量进行过初始化了。

2.5.3、实质:执行类构造器<clinit>方法的过程。

注1:<clinit>方法:

A、内容:类中所有的类变量、静态语句块(static{})中的语句组成。

B、语句的组合收集工作由编译器自动完成。

C、收集顺序:源文件中出现的顺序。

D、静态语句块中:只能访问在静态语句块之前的变量,定义在它后面的变量,静态语句块只能为其赋值,但不能访问。

 

 

 

3、类加载器

 

3.1、定义:实现了“根据类的全限定名来获取此类的二进制字节流”功能的代码模块。

3.2、功能:

A、实现类的加载动作。

B、和类本身,共同确立了java虚拟机中唯一的一个类。也就是说,如果像知道两个类是否相同,首先他们的类加载器必须是一致的,才能继续比下去。

 

3.3、分类:

A、java虚拟机角度:启动类加载器、所有的其他类加载器。前者是虚拟机自身的一部分,后者是独立于虚拟机外部。

B、java程序员角度:大部分程序都会用到系统提供的3种类加载器。

★启动类加载器

  负责:将存放在<java_home>\lib中的,或者被-Xbootclasspath参数所指定的路径中的,且可以被虚拟机识别的类库加载到虚拟机中。

★扩展类加载器

   负责:加载<java_home>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库。开发者可以直接使用扩展类加载器。

★应用程序类加载器

    负责:加载用户类路径上所指定的类库。开发者可以直接使用这个类加载器。

 

3.4、工作原理

A、类加载器的双亲委派模型定义:类加载器之间的层次关系

B、双亲委派模型工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每个层次的类加载器都是如此。因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个请求时,子加载器才会尝试自己去加载。

C、设计意义:

★java类随着它的类加载器一起具备了一种带有优先级的层次关系。

★双亲委派模型对于保证java程序的稳定运作很重要。

 

3.5、双亲委派模型的3次破坏

注1:破坏,并不带有贬义色彩,这只是根据实际情况的需要,适当的改变了双亲委派模型的设计而已。

A、第一次破坏:JDK1.2之前。也在双亲委派模型出现之前。

B、第二次破坏:因设计本身缺陷导致。Java中所有设计SPI的加载动作基本上都采用这种方式,如JNDI、JDBC等。

C、第三次破坏:是由于用户对程序动态性的追求而导致的。