Java在刚刚诞生时提出过一个非常著名的宣传口号:”一次编写,到处运行”,java可以运行在不同的平台上,而Java语言之外的语言,如Clojure、Groovy、JRuby、Jython、Scala等也可以在Java虚拟机上运行。这种实现语言无关性的基础得益于虚拟机和字节码存储格式。
在Java代码被编译成Class文件(.class),在Class文件中描述的各种信息,最终都需要加载到虚拟机中之后才能运行和使用。
什么是类加载机制
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。
在Java语言里,类型的加载、连接和初始化过程都是在程序运行期间完成的。Java里天生可以动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特点实现的。例如编写一个面向接口的应用程序,可以等到运行时再指定其实际的实现类;
类加载的时机
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用、卸载,其中验证、准备、解析3个部分统称为连接。
其中加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班的开始。而解析阶段则不一定:在某些情况下可以在初始化阶段之后再开始。
什么情况下需要开始类加载过程的第一阶段:加载,Java虚拟机规范中并没有强制约束。但是对于初始化阶段有严格规定,有且只有5种情况必须立即对类进行“初始化”;
1、遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。例如:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
2、使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化
3、当初始化一个类的时候,其父类还没有进行初始化,则需要先触发其父类的初始化
4、当虚拟机启动时,用户需要指定一个要执行的主流(包含main()方法的那个类),虚拟机会先初始化这个主类
5、当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
类加载的过程
加载
在加载阶段,虚拟机需要完成3件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生产一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
验证
验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。大致分为4个阶段的验证动作:文件格式验证(例如是否以魔数0xCAFEBABE开头。主、次版本号是否在当前虚拟机处理范围之内)、元数据验证(例如这类是否有父类。这个类是否继承了不被允许继承的类)、字节码验证(例如保证跳转指令不会跳转到方法体以外的字节码指令上)、符号引用验证(例如符号引用中通过字符串描述的全限定名是否能找到对应的类)
准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这时候进行的内存分配仅包含类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。(注意这里的初始值并非代码中的值,譬如int类型则初始值为0)
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。
直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,则引用的目标必定已经在内存中存在。
初始化
类初始化是类加载过程的最后一步,除了在加载阶段用户应用程序可以通过自定义加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码。
本文来自《深入理解java虚拟机》