第七章 虚拟机类加载机制 《深入理解java虚拟机》

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

类型的加载、连接和初始化过程都是在程序运行期间完成的。

类加载的时机

类从被加载到最后被卸载,整个生命周期包括:加载、验证、准备、解析、初始化、使用、卸载7个阶段。

其中,加载、验证、准备、初始化和卸载这5个顺序是确定的。

虚拟机规范规定以下五种情况必须对类进行初始化:

1.遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发初始化。这4条指令的常见场景是:使用new关键字实例化对象时、读取或设置一个类的静态字段的时候、以及调用一个类的静态方法的时候。

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

3.当初始化一个类,发现其父类还没有进行过初始化,则要初始化其父类

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

5.使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,并且这个方法句柄多对应的类没有被初始化。

被动引用不会触发初始化:1.通过子类引用父类的静态字段,不会导致子类初始化;2.通过数组定义来引用类,不会触发类的初始化;3.常量(final)在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化

类的加载过程

加载

加载是类加载过程的一个阶段,加载阶段,虚拟机完成3件事情:1.通过一个类的全限定名来获取定义此类的二进制字节流;2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口。非数组类的加载最终都是通过引导类加载器或者用户自定义的类加载器去完成。

对于数组类的加载,情况有点特殊:数组类本身不通过类加载器创建,由java虚拟机直接创建,但是数组类的元素类型最终是靠类加载器创建。

验证

验证是连接阶段的第一步,这一阶段的主要目的确保Class文件的字节流包含的信息符合当前虚拟机的要求。

验证阶段分为4个阶段的检验动作:文件格式验证、元数据验证、字节码验证、符号引用验证。

文件格式验证

验证字节流是否符合Class文件格式的规范,并且能被当前虚拟机处理。

该阶段的主要目的是保证输入的字节流能正确的解析并存储与方法区之内,是基于二进制流进行验证,只有通过了这个阶段的验证,字节流才会进入内存的方法区进行存储,所以文件格式验证是基于二进制流进行验证的,元数据验证、字节码验证、符号引用验证都是基于方法区进行验证的。

元数据验证

对字节码描述的信息进行语义分析,以保证其描述的信息符合java语言规范的要求。

字节码验证

通过数据流和控制流分析,确定程序的语义是合法的、符合逻辑的。在第二阶段对元数据信息中的数据类型昨晚校验后,这个阶段对类的方法体进行校验分析,保证被校验类的方法在运行时不会危害虚拟机。

符号引用验证

符号引用验证发生在虚拟机将符号引用转化为直接引用的时候,在连接的第三个阶段-解析阶段发生。符号引用验证可以看做是对类自身以外的信息进行匹配性校验。

准备

准备阶段是正式为类变量(static修饰的变量;如果带有final,这个变量在准备阶段会直接被初始化成指定的值,而非零值)赋初始值的阶段,这些变量的所使用的内存都在方法区进行分配。此时的赋值是赋初始值,也就是赋零值,并且不包括实例变量。

解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,符号引用在Class文件中以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现(参见第六章)

符号引用:理解成能够定位到目标的一串字符串。

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

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行,分别对应常量池的CONSTANT_Class_info、CONSTANT_Fieldref、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、CONSTANT_MethodHandle_info、CONSTANT_InvokeDynamic_info 7中类型常量。具体解析过程参见书中对应小节,解析过程实际上就是一层层验证解析,如果失败则抛出对应的异常信息。

初始化

初始化阶段是执行类构造器<clinit>()方法的过程:

1.<clinit>()方法是由编译器自动收集类中所有类变量的赋值动作(类变量的初始化已经在准备阶段进行了)和静态语句块(static{}块)中的语句合并产生的,编译器收集顺序是由语句在源文件中出现的顺序所决定的,静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问(现在版本编译不通过):

2.<clinit>()方法与类的构造函数(或者说实例构造器<init>()方法)不同,它不需要显示的调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()已经执行完毕。因此在虚拟机中第一个执行<clinit>()方法的类一定是java.lang.Object。

3.由于父类的<clinit>()方法先执行,意味着父类的静态语句块要优先于子类的变量赋值操作。

4.<clinit>()方法对于类或接口来说并不是必须的,如果一个类中没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。

5.接口中不能使用静态语句块,但是仍然有变量赋值操作,因此接口也会生成<clinit>()方法。但是接口和类不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量使用时,父接口才会初始化,接口的实现类在初始化的时候也一样不会执行父接口的<clinit>()方法。

6.虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个进程阻塞。(不易排查)

上图执行就会发现线程2在执行<clinit>()方法时,进入死循环,导致阻塞所有初始化该类的线程。

类加载器

虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”动作放在java虚拟机外部实现,实现这个动作的模块被称为类加载器。

对于任意一个类,都需要有加载他的类加载器和这个类本身一同确立在其java虚拟机中的唯一性,也就是比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,但是加载它们的类加载器不同,那么这两个类必定不等。

双亲委派模型

对java虚拟机而言,类加载器有两种:启动类加载器和其他类加载器。启动类加载器是C++实现,是虚拟机自身的一部分;其他类加载器由java语言实现,独立于虚拟机外部,继承自抽象类java.lang.ClassLoader。

对java程序员而言,类加载器分为三种:

1.启动类加载器:加载<JAVA_HOME>/lib目录中的类库,java中获取该加载器时获取的是null

2.扩展类加载器:加载<JAVA_HOME>/lib/ext目录中的类库

3.应用程序类加载器:被称为系统类加载器,负责加载用户类路径上的类库,是系统中的默认类加载器

双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器,父子类加载器通过组合的方式实现。

双亲委派模型的工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己尝试加载这个类,而是把这个请求委托给父类加载器去完成,每一层均如此,只有当父类加载器反馈无法完成这个加载请求时,子加载器才会尝试自己去加载。

双亲委派模型的好处是让java类随着它的加载器一起具备了一种带有优先级的层次关系。

双亲委派模型的实现逻辑:实现的代码集中在java.lang.ClassLoader的loadClass()方法中。先检查是否已经被加载过,若没有加载则调用父加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父加载器加载失败,抛出ClassNotFoundException后,再调用自己的findClass()方法进行加载

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值