虚拟机把形容类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终造成能够被虚拟机间接应用的 Java 类型,这就是虚拟机的类加载机制。Java 运行期动静加载和动静连贯的特点使得 Java 天生就具备动静扩大的语言个性。
类加载的机会
类从被加载到虚拟机内存开始,到卸载出内存为止,整个生命周期包含:
- 加载(Loading)
- 验证(Verification)
- 筹备(Preparation)
- 解析(Resolution)
- 初始化(Initialization)
- 应用(Using)
- 卸载(Unloading)
七个阶段产生的程序如下图:
其中,加载、验证、筹备、初始化和卸载这 5 个阶段的程序是确定的,类的加载过程必须依照这种程序循序渐进地开始,而解析阶段则不肯定,它在某些状况下能够在初始化阶段之后再开始,这是为了反对 Java 语言地运行时绑定。
Java 虚拟机标准严格规定了有且只有 5 种状况必须立刻对类进行“初始化”,加载、验证、筹备天然须要在此之前开始。这 5 种状况别离是:
- 遇到 new、getstatic、putstatic 或 invokestatic 这 4 条字节码指令时,如果类没有进行过初始化,则须要先触发其初始化。
- 应用 java.lang.reflect 包地办法对类进行反射调用的时候,如果类没有进行初始化,则须要先触发其初始化。
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则须要先触发其父类的初始化。
- 当虚拟机启动时,用户须要指定一个要执行的主类,虚构机会先初始化这个类。
- 当应用 JDK1.7 的动静语言反对时,如果一个 java.lang.invoke.MethodHandle 实例最初的解析后果 REF_getStatic、REF_inkokeStatic、REF_putStatic 的办法句柄,并且这个办法句柄所对应的类没有进行过初始化,则须要先触发其初始化。
下面 5 种场景中的行为称为对一个类进行被动援用。除此之外,所有援用类的形式都不会触发初始化,称为被动援用。被动援用的状况列举:
- 通过子类援用父类的动态字段,不会导致子类初始化;
- 通过数组定义来援用类,不会触发此类的初始化;
- 常量在编译阶段会存入调用类的常量池中,实质上并没有间接援用到定义常量的类,因而不会触发定义常量的类的初始化。
接口与类的区别在于前文中 5 中“有且仅有”须要开始初始化场景中的第 3 种,当一个类在初始化时,要求其父类全副都曾经初始化过了,然而一个接口在初始化时,并不要求其父接口全副都实现了初始化,只有在真正应用到父接口的时候,才会初始化。
类加载的过程
类加载的过程,即加载、验证、筹备、解析和初始化这个 5 个阶段。
加载
“加载”只是“类加载”过程的一个阶段。在加载阶段,虚拟机须要实现 3 件事件:
- 通过一个类的全限定名来获取定义此类的二进制字节流;
- 将这个字节流所代表的动态存储构造转化为办法区的运行时数据结构;
- 在内存中生成一个代表这个类的 java.lang.Class 对象,作为办法区这个类的各种数据的拜访入口。
验证
验证是连贯阶段的第一步,这一步的目标是为了确保 Class 文件中的字节流中蕴含的信息合乎以后虚拟机的要求,并且不会危害虚拟机本身的平安。从整体上看,验证阶段大抵能够分为 4 个阶段的测验动作:文件格式验证、元数据验证、字节码验证、符号援用验证。
文件格式验证
验证字节流是否合乎 Class 文件格式的标准,并且能被以后版本的虚拟机解决。文件格式验证的次要目标是保障输出的字节流能正确地解析并存储于办法区,格局上合乎形容一个 Java 类型信息的要求。可能的验证点:
- 是否以魔数 0xCAFEBABE 结尾;
- 主、次版本号是否在以后虚拟机解决的范畴之内;
- 常量池的常量中是否有不被反对的常量类型;
- 指向常量的各种索引值是否有指向不存在的常量或不合乎类型的常量;
- CONSTANT_Utf8_info 型的常量中是否有不合乎 UTF8 编码的数据;
- Class 文件中各个局部及文件自身是否有被删除的或附加的其余信息。
元数据验证
对字节码形容的信息进行语义剖析,以保障其形容的信息合乎 Java 语言标准的要求。可能的验证点:
- 这个类是否有父类(除了 java.lang.Object 之外,所有的类都应该有父类);
- 这个类的父类是否继承了不容许被继承的类(被 final 润饰的类);
- 如果这个类不是抽象类,是否实现了其父类或者接口之中要求实现的所有办法;
- 类中的字段、办法是否与父类产生矛盾(例如笼罩了父类的 final 字段,或者呈现不合乎规定的办法重载,例如办法参数都统一,但返回值类型却不同等)。
字节码验证
字节码验证是整个验证过程中最简单的一步,次要目标是通过数据流和控制流剖析,确定程序语义是非法的、合乎逻辑的。可能的验证点:
- 保障任意实可操作数栈的数据类型与指令代码序列都可能配合工作,例如不会呈现相似这样的状况:在操作数栈搁置了一个 int 类型的数据,应用时却依照 long 型来加载入本地变量表中;
- 保障跳转指令不会跳转到办法体以外的字节码指令上;
- 保障办法体中的类型转换是无效的,例如能够把一个子类对象赋值给父类数据类型,这是平安的,然而把父类对象赋值给子类型,甚至把对象赋值给与它毫无继承关系、齐全不相干的一个数据类型,则是危险和不非法的。
符号援用验证
这一步的校验产生在虚拟机将符号援用转化为间接援用的时候,这个转化动作将在连贯的第三阶段,即解析中产生。符号援用验证的次要目标是确保解析动作可能失常执行,如果无奈通过符号援用验证,那么将会抛出一个 java.lang.IncompatibleClassChangeError 异样的子类,如:java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError 等。可能的验证点:
- 符号援用中通过字符串形容的全限定名是否能找到对应的类;
- 在指定类中是否存在合乎办法的字段描述符以及简略名称所形容的办法和字段;
- 符号援用中的类、字段、办法的拜访性是否可被以后类拜访。
筹备
筹备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所应用的内存都将在办法区中进行调配。筹备阶段进行内存调配的仅包含类变量,不包含实例变量,实例变量将在对象实例化时随着对象一起调配在 Java 堆中。筹备阶段的初始值,“通常状况”下是数据类型的零值。
解析
解析阶段是虚拟机将常量池内的 符号援用 替换为 间接援用 的过程。
- 符号援用:符号援用以一组符号来形容所援用的指标,符号能够是任何模式的字面量,只有应用时能无歧义地定位到指标即可。符号援用与虚拟机实现地内存布局无关,援用的指标并不一定曾经加载到内存中。
- 间接援用:间接援用能够是间接指向指标的指针,绝对偏移量或时一个能间接定位到指标的句柄。间接援用是和虚拟机实现的内存布局相干的,同一个符号援用在不同虚拟机实例上翻译进去的间接援用个别不会雷同。如果有了间接援用,那援用的指标必然曾经在内存中存在。
解析动作次要针对类或接口、字段、类办法、接口办法、办法类型、办法句柄和调用点限定符这 7 类符号援用进行。
初始化
初始化阶段是执行类结构器 <clinit>()办法的过程。<clinit>()办法执行过程中一些可能会影响程序运行行为的特点和细节:
- 该办法是由编译器主动收集类中的所有类变量的赋值动作和动态语句块中的语句合并后产生的,编译器收集的程序是由语句在源文件中呈现的程序所决定的,动态语句块中只能拜访到定义在动态语句块之前的变量,定义在它之后的变量,在后面的动态语句块能够赋值,然而不能拜访。
- 该办法与类的构造函数不同,它不须要显式地调用父类结构器,虚构机会保障在子类的 <clinit>()办法执行之前,父类的 <clinit>()办法曾经执行结束。因而在虚拟机中第一个被执行的 <clinit>()办法的类必定是 java.lang.Object。
- 因为父类的 <clinit>()办法先执行,也就意味着父类中定义的动态语句块要优先于子类的变量赋值操作。
- 该办法对于类或接口来说并不是必须的,如果一个类中没有动态语句块,也没有对变量的赋值操作,那么编译器能够不为这个类生成 <clinit>()办法。
- 接口中不能应用动态语句块,但依然有变量初始化的赋值操作,因而接口类与类一样都会生成 <clinit>()办法。但接口与类不同的是,执行接口的 <clinit>()办法不须要先执行父接口的 <clinit>()办法。只有当接口中定义的变量应用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的 <clinit>()办法。
- 虚构机会保障一个类的 <clinit>()办法在多线程环境中被正确地桎梏、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类地 <clinit>()办法,其余线程都须要阻塞期待,直到流动线程执行 <clinit>()办法结束。如果在一个类地 <clinit>()办法中有耗时很长地操作,就可能造成多个过程阻塞。
类加载器
把类加载阶段中“通过一个类地全限定名来获取形容此类地二进制字节流”这个动作放到 Java 虚拟机内部去实现,以便让应用程序本人决定如何去获取所须要的类。实现这个动作的代码模块称为“类加载器”。
对于任意一个类,都须要由加载它的类加载器和这个类自身一起确立其在 Java 虚拟机中的唯一性,每一个类加载器,都领有一个独立的类名称空间。即:比拟两个类是否相等,只有在这个两个类是同一个类加载器加载的前提下才有意义,否则,即便这两个类来源于同一个 Class 文件,被同一个虚拟机加载,只有加载他们的类加载器不同,那这两个类就必然不相等。
双亲委派模型
从 Java 虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类加载器,这个类加载器应用 C ++ 语言实现,是虚拟机本身的一部分;另一种就是所有其余的类加载器,这些类加载器都有 Java 语言实现,独立于虚拟机内部,并且全都继承自抽象类 java.lang.ClassLoader。
从开发人员的角度来看,绝大部分 Java 程序都会应用到上面 3 种零碎提供的类加载器:
- 启动类加载器:这个类加载器负责将寄存在 <JAVA_HOME>/lib 目录中的,或者被 -Xbootclasspath 参数所指定的门路中的,并且是虚拟机依照文件名辨认的类库(例如:rt.jar,名字不合乎的类库即便放到 lib 目录中也不会被加载)加载到虚拟机内存中。启动类加载器无奈被 Java 程序间接援用,用户在编写自定义类加载器时,如果须要把加载申请委派给疏导类加载器,那间接应用 null 代替即可。
- 扩大类加载器:这个加载器由 sun.misc.Launcher$ExtClassLoader 实现,负责加载 <JAVA_HOME>/lib/ext 目录中的,或者被 java.ext.dirs 零碎变量指定的门路中的所有类库,开发者能够间接应用扩大类加载器。
- 应用程序类加载器:这个加载器由 sun.misc.Launcher$AppClassLoader 实现。因为这个类加载器是 ClassLoader 中的 getSystemClassLoader()办法的返回值,所以个别也称为零碎类加载器。负责加载用户类门路上所指定的类库,开发者能够间接应用这个类加载器,如果应用程序中没有自定义过本人的类加载器,个别状况下这个就是程序中默认的类加载器。
类加载器之前的关系个别如下图:
上图中展现的类加载器之间的档次关系,称为类加载器的双亲委派模型。双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都该当有本人的父类加载器。
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的申请,它首先不会本人去尝试加载这个类,而是把这个申请委派给父类加载器去实现,每一个档次的类加载器都是如此,因而所有的加载申请最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈本人无奈实现这个加载申请时,子加载器才会尝试本人去加载。
应用双亲委派模型来组织类加载器之间的关系,有一个不言而喻的益处就是 Java 类随着它的类加载器一起具备了一种带有优先级的档次关系。