Class文件的各种信息,最终都需要加载到虚拟机中之后才能被运行和使用,而虚拟机如何加载这些Class文件?Class文件中的信息进入到虚拟机之后会发生什么变化?这些都应该是我们思考的问题。
虚拟机把描述类的数据加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
具体见下图。
类从被加载到虚拟机内存开始、到卸载出内存为止,它的整个生命周期包括了:加载、验证、准备、解析、初始化、使用和卸载七个阶段。其中验证、准备、和解析三个部分统称为连接。
在这几个阶段之中,只有解析阶段不一定,它在某些情况下可以在初始化阶段之后再开始,这是为了支持java语言的动态绑定。
java虚拟机对于初始化阶段有了明确的规定,在如下情况下,必须立即堆垒进行初始化。
遇到new 、getstatic、putstatic或invokestatic这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。使用java.lang.reflect包的方法堆类进行发射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。当初始化一个类的时候,如果发现其父类还没有进行过初始化,那么需要先触发其父类的初始化。当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化这个主类。我们需要注意的时,加载时类加载的一个阶段,一定不能混淆这两个的概念。在加载的过程中,虚拟机会完成以下三件事情
通过一个类的全限定名来获取定义此类的二进制字节流。将这个字节流锁带便的静态存储结构转化为方法区的运行时参数。Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。这一步的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。具体验证的东西如下:
文件格式验证。这里验证的时字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。元数据的验证。就是堆字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。字节码校验。这是这个过程中最为复杂的一个过程,主要工作是进行数据流和控制流的分析。符号引用验证。最后一个阶段校验发生在虚拟机将符号引用转换为直接引用的时候,这个转化动作将在连接的第三个阶段-解析阶段中发生。准备阶段是正式为类变量分配内存并设置初始值的阶段,这些内存都将在方法区中进行分配,我们这里必须强调的是,这时候进行内存分配的仅包括类变量,也就是被static修饰的变量,而不包含实例变量,实例变量将在对象实例化时随着对象一起分配在Java堆中。
解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程。
类初始化阶段是类加载过程的最后一步,前面的加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制,到了初始化阶段,才真正开始执行类中定义的Java程序代码,或者说是字节码。
在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则是根据长许愿通过程序制定的主观计划去初始化类变量和其他资源。
通过一个类的全限定类名来获取描述此类的二进制字节流,实现这个动作的代码模块称为类加载器。
类加载器虽然只用于实现类的加载动作,但是它在java程序中起到的作用却圆润不限于类加载阶段,对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在java虚拟机中的唯一性。这句话可以表达的通俗一些,就是如果我们在比较两个类是否相同的时候,那么这两个类必须是由同一个类加载器加载的前提之下才有意义。否则,即使这两个类源自同一个Class文件只要加载它们的类加载器不同,那这两个类就必定不想等。
在java虚拟机中,只存在两种不同的类加载器,一种是启动类加载器,这个类加载器是使用C++语言实现,独立与虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader。
程序都会使用以下三种类加载器:
启动类加载器扩展类加载器应用程序类加载器工作过程:
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的家在请求最懂都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。
使用双亲委派机制来组织类加载器之间的关系,有一个显而易见的好处就是java类随着它的类加载器一起具备了带有优先级的层次关系。双亲委派机制对于保证java程序的稳定运作很重要。
双亲委派机制不是一个强制性的约束模型,所以历史上发生了三次大规模的破环事件。
具体内容见本书的195页。