Jvm类加载机制详解--类加载的几个阶段

虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机类加载机制。在Java语言中,类的加载、连接和初始化过程都是在程序运行期间完成的,这是java作为动态语言的基础。另外值得注意的是上面提到的Class文件,并不一定值得是磁盘上的.class文件,而只需要是任何符合字节码规范的一串二进制字节流就可以了。下面来了解下类加载的具体过程和类加载中的双亲委托模型规范。

类加载过程

类从被加载到虚拟机内存开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段。其中验证、准备、解析三个阶段称之为连接。

这里写图片描述
加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持java语言的动态绑定。
在了解各个阶段的大概作用之前,还需要解决的一个问题就是如何虚拟机在何时会对类进行加载。实际上虚拟机规范没有对类加载的时机进行规定,而规定了类初始化的时机,并且规定有且只有下面5种情况需要立即对类进行初始化(在初始化之前自然需要进行加载、验证、准备几个阶段)。
1) 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类没有进行初始化,则需要先触发其初始化。生成这四条指令的场景Java代码场景是:使用new关键词实例化类的对象、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)、调用一个类的静态方法的时候。
2)使用java.lang.reflect包的方法对类进行反射调用的时候,如果类还没有进行过初始化,则需要先进行初始化。
3)当初始化一个类的时候,如果发现其父类还没有初始化,则需要先初始化其父类。
4)当虚拟机启动时,用户需要制定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类
5) 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
上面这五种方式被称为一个类的主动引用,除此之外所有引用类的方法都不会触发初始化,称为被动引用(比较常见的就是对类常量的引用,另外通过子类来引用父类的静态变量也属于被动引用,不会触发子类的初始化)。对于接口和类的初始化规则还有一些不同,主要是对类进行初始化的时候要求其父类的初始化已经全部完成了,但对接口进行初始化的时候并不要求其父接口都完成了初始化,只要在真正使用到父接口(如引用父接口中静态变量)才会初始化。下面看下加载、验证、准备、解析、初始化这5个阶段各自的工作。
1)加载
加载阶段主要完成下面三件事情:

  • 通过一个类的全限定名来获取定义此类的二进制字节流。
  • 将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构(这里可以理解为字节流的格式是虚拟机规范规定的,而每个虚拟机中对Java类的数据结构有自己的规定,这一步将外部的二进制字节流转换为虚拟机需要的格式存储在方法区之中)。
  • 在内存中共生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据结构的访问入口。

上面三个阶段中的第一个步骤”通过一个类的全限定名来获取定义该类的二进制字节流”是开发人员可控性最强的,因为虚拟机规范并没有规定一定要从Class文件中获取,所以可以通过定义自己的类加载器来完成(通过重写一个类加载器的loadClass()方法),可以实现从jar、zip、war等压缩包中读取,也可以同网络中获取,甚至可以在运行是计算生成(例如java的动态代理就是利用ProxyGenerator.generateProxyClass()来为特定接口生成代理类的二进制字节流)。

2)验证
验证是连接阶段的第一步,这一步的目的是为了确保Class文件字节流中包含的信息符合当前虚拟机的要求,并且不会威胁虚拟机自身的安全。其实如果纯粹是从Java源码编译得到的Class文件,自身是可以确保安全的,但是因为Class文件可以由任何途径产生(甚至可以由十六进制编辑器直接编写来产生Class文件),所以虚拟机很有必要对输入的字节流进行验证以维护自身的安全。
验证阶段需要完成四个阶段的校验动作:文件格式验证、元数据验证、字节码验证、符号引用验证。
文件格式验证主要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理,这个阶段是基于二进制字节流进行的,验证的目的是为了保证输入的字节流符合Class文件规范能够正确的解析并存储于方法区内,通过这个阶段的校验之后字节流才能进入到内存的方法区中进行存储,所以后面的3个验证阶段全部是基于方法区的存储结构进行的,不会再直接操作字节流。元数据验证和字节码验证主要是对字节码的语义分析和对数据流和控制流进行分析,做一些确保代码逻辑的验证工作;最后的符号引用验证发生在虚拟机将符号引用转换为直接引用的时候,这个转换动作将在链接的第三个阶段–解析阶段。符号引用验证可以看成是对类自身以外的信息进行匹配性校验的过程,简单的来说就是看下符号引用通过字符串形式描述的类是否存在并且类的定义是否规范。

3)准备
准备阶段是正式为类实例变量分配内存并且设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这里的类变量指的是被static修饰的变量,不包括实例变量,实例变量将会在对象实例化的时候随着对象一起分配在java堆中;另外这里的初始值不是代码中指的初始值而是变量数据类型对应的零值(int为0,String为null等)。

4)解析
解析阶段简单的来说就是虚拟机将常量池内的符号引用替换为直接引用的过程。具体的过程牵涉到Class文件格式中符号引用的规定,在这里就不展开了。

5)初始化
初始化是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序通过自定义类加载器参与之外,其余动作完全由虚拟机主导。到了初始化过程,才会真正开始执行类中定义的Java程序代码。这一步主要就是执行静态变量的初始化,包括静态变量的赋值和静态初始化块的执行。这里需要引入一个类构造器<clinit>()方法,下面对其进行简单的介绍:

  • <clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态初始化块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态初始化块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量静态语句块中可以赋值但是不能访问(这里有一点需要注意,静态初始化块和静态变量赋值语句执行顺序是按定义顺序来的,并不是说初始化块一定会在静态赋值语句之后执行)。
  • <clinit>()方法与类的构造函数不同,它不需要显示的调用父类的<clinit>()方法,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。因此在虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object。
  • <clinit>()方法对于类或接口来说不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。
  • 接口中不能使用静态初始化块,但是仍有static变量的赋值操作,所以也会有<clinit>()方法,但是接口执行<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量被使用到时,才会执行<clinit>()方法。
  • 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其它线程都需要阻塞等待。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值