虚拟机类加载的过程

61 阅读8分钟

虚拟机把描述类的数据从Class文件加载到内存,最终生成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制。与那些在编译时需要进行连接工作的语言不同,java类型的加载、连接和初始化都是在程序运行期间完成的。(可以实现反射,OSGi等技术)

类的生命周期:加载、连接(验证、准备、解析)、初始化、使用、卸载。

加载、连接(验证、准备、解析)、初始化是类的加载过程。

虚拟机规范中没有强制约束什么时候开始类加载,但以下几种情况必须开始初始化(而加载、验证、准备必须在初始化之前开始):

第一:生成该类对象的时候(new关键字)、读取或设置一个类的静态字段(被final修饰、已在编译期放入常量池的静态字段除外)、调用一个类的静态方法。会初始化该类及该类的所有父类;

第二:初始化一个类时,如果其父类还没有被初始化,则先初始化父类;

第三:class.forName("类名"),使用java.lang.reflect包的反射;

第四:虚拟机启动时,main方法的类;

加载

在加载阶段,虚拟机主要完成三件事:

1.通过一个类的全限定名来获取定义此类的二进制字节流。

2.将这个字节流所代表的静态存储结构转化为方法区域的运行时数据结构。

3.在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区域数据的访问入口。

数组类本身不通过类加载器创建,由java虚拟机直接创建。但数组类的元素(指数组去掉维度后的类型)最终是由类加载器创建。

加载阶段完成后,虚拟机外部的二进制字节流(如.class文件、jar、war包)就按照虚拟机所需要的格式存储在方法区之中,然后在内存中实例化一个java.lang.Class类的对象(对于HotSpot,Class对象比较特殊,它虽然是对象,但是存放在方法区里)

验证

验证的目的是为了确保Class文件中的字节流包含的信息符合当前虚拟机的要求、规范,而且不会危害虚拟机自身的安全。不同的虚拟机对类验证的实现可能会有所不同,但大致都会完成以下四个阶段的验证:文件格式的验证、元数据的验证、字节码验证和符号引用验证。

准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配

    1、这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。

    2、这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。

假设一个类变量的定义为:public static int value = 3;

那么变量value在准备阶段过后的初始值为0,而不是3,因为这时候尚未开始执行任何Java方法,所以把value赋值为3的动作将在初始化阶段才会执行。

对基本数据类型来说,对于类变量(static)和全局变量,如果不显式地对其赋值而直接使用,则系统会为其赋予默认的零值,而对于局部变量来说,在使用前必须显式地为其赋值,否则编译时不通过。

对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过;而只被final修饰的常量则既可以在声明时显式地为其赋值(只被final修饰是常量,存放在方法区),也可以在类初始化时显式地为其赋值,总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值。

对于引用数据类型reference来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null。

如果在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的零值。

    3、如果类字段的字段属性表中存在ConstantValue属性(即同时被final和static修饰,且为基本类型或String),那么在准备阶段变量value就会被初始化为ConstValue属性所指定的值。(因为static的要被赋默认值0,而final又不能变,所以提前赋值)

   假设上面的类变量value被定义为: public static final int value = 3;

编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为3。

方法区是存放虚拟机加载类的相关信息,如类、静态变量和常量

类加载的时候将所有方法的字节码放到方法区的

解析

加载、验证、准备、初始化、卸载这几个阶段的顺序是确定的,而解析阶段不一定,可以延迟执行,在某些情况下可以在初始化阶段之后再开始,这是为了支持java语言的运行时绑定。

解析阶段是虚拟机将常量池中的符号引用转化为直接引用的过程。包括类或接口的解析、字段解析、类方法解析、接口方法解析。(比如String s ="aaa",转化为 s的地址指向“aaa”的地址)

常量池中主要存放两大类常量:字面量和符号引用。字面量比较接近于Java层面的常量概念,如文本字符串、被声明为final的常量值、-128-127的Integer包装类等。而符号引用总结起来则包括了下面三类常量:

1、类和接口的全限定名(即带有包名的Class名,如:org.lxh.test.TestClass)

2、字段的名称和描述符(private、static等描述符)

3、方法的名称和描述符(private、static等描述符)

虚拟机在加载Class文件时才会进行动态连接,也就是说,Class文件中不会保存各个方法和字段的最终内存布局信息,因此,这些字段和方法的符号引用不经过转换是无法直接被虚拟机使用的。当虚拟机运行时,需要从常量池中获得对应的符号引用,再在类加载过程中的解析阶段将其替换为直接引用,并翻译到具体的内存地址中。

前面说解析阶段可能开始于初始化之前,也可能在初始化之后开始,虚拟机会根据需要来判断,到底是在类被加载器加载时就对常量池中的符号引用进行解析(初始化之前),还是等到一个符号引用将要被使用前才去解析它(初始化之后)。

    这里说明下符号引用和直接引用的区别与关联:

符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到了内存中。

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

初始化

初始化是类加载过程的最后一步,到了此阶段,才真正开始执行类中定义的Java程序代码。在准备阶段,类变量已经被赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序指定的主观计划去初始化类变量和其他资源,如赋值。

卸载

该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。

加载该类的ClassLoader已经被回收。

该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。

如果以上三个条件全部满足,jvm就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程其实就是在方法区中清空类信息,java类的整个生命周期就结束了。

由Java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。前面介绍过,Java虚拟机自带的类加载器包括根类加载器、扩展类加载器和系统类加载器。Java虚拟机本身会始终引用这些类加载器,而这些类加载器则会始终引用它们所加载的类的Class对象,因此这些Class对象始终是可触及的。

 由用户自定义的类加载器加载的类是可以被卸载的(没有引用时)。当再次有需要时,会检查类的Class对象是否存在,如果存在会直接使用,不再重新加载;如果不存在类会被重新加载