虚拟机类加载机制——类加载时机

由于道行不够深,所以此篇类加载机制的讲解主要来自于《深入理解Java虚拟机——JVM高级特性与最佳实践》的第7章 虚拟机类加载机制。

在前面《初识Java反射》中我们在开头提到要了解Java反射,就得要了解虚拟机的类加载机制。在这里,我们来试着窥探一下何为类加载。

“虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,类型的加载、连接和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。”这句话确实读着好懂,但到底类加载做了什么呢?我们都知道Java编译后形成.class字节码文件,虚拟机是不认识.java文件的,所以虚拟机要加载Class文件将它做一些处理才能到“还原”成我们所写的java程序,按照我们的逻辑步骤来执行。Java之所以称为动态语言正是因为类型的加载、连接和初始化都是在程序运行期间完成的。这虽然会带来一些开销,但同时它也为Java语言带来了很大的灵活性。

那么在此期间虚拟机做了什么呢?我们可以通过下面的图来了解类的整个生命周期:加载、验证、准备、解析、初始化、使用、卸载。

这7个阶段中的:加载、验证、准备、初始化、卸载的顺序是固定的。但它们并不一定是严格同步串行执行,它们之间可能会有交叉,但总是以“开始”的顺序总是按部就班的。至于解析则有可能在初始化之后才开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)

对于加载阶段(注意加载和类加载概念,加载是类加载过程的第一个阶段)JVM并没有对此约束,而是交由虚拟机的具体实现。但对于初始化,虚拟机规范则做了严格的规定,初始化可能也是对我们实际编程运用当中非常值得注意的问题。

虚拟机对于类的初始化阶段严格规定了有且仅有只有5种情况如果对类没有进行过初始化,则必须对类进行“初始化”!

  1. 遇到new、读取一个类的静态字段(getstatic)、设置一个类的静态字段(putstatic)、调用一个类的静态方法(invokestatic)。
  2. 使用java.lang.reflect包的方法对类进行反射调用时。
  3. 当类初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。(如果是接口,则不必触发其父类初始化)
  4. 当虚拟机执行一个main方法时,会首先初始化main所在的这个主类。
  5. 当只用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。(暂未研究此种场景)

上面5种场景是有且仅有,称之为“主动引用”,只有满足上述5种场景之一,就会触发对类进行初始化。其余都不会触发类初始化,称之为“被动引用”。

下面列举3个例子来说明何为“被动引用”:

被动引用——例1


 1 package day_13_passiveReference;
 2
 3 /**
 4  * @author turbo
 5  *
 6  * 2016年9月19日
 7  */
 8 public class SuperClass {
 9     static{
10         System.out.println("SuperClass init!");
11     }
12     public static int value = 123;
13 }
 1 package day_13_passiveReference;
 2
 3 /**
 4  * @author turbo
 5  *
 6  * 2016年9月19日
 7  */
 8 public class SubClass extends SuperClass {
 9     static{
10         System.out.println("SubClass init!");
11     }
12 }
 1 package day_13_passiveReference;
 2
 3 /**
 4  * @author turbo
 5  *
 6  * 2016年9月19日
 7  */
 8 public class Main {
 9
10     /**
11      * @param args
12      */
13     public static void main(String[] args) {
14         System.out.println(SubClass.value);
15     }
16
17 }

输出结果:

我们看到虽然我们是通过子类来调用的父类静态字段,但从结果可以看到并没有初始化子类,而是初始化了父类,这即是“被动引用”。对于静态字段,只有直接定义这个字段的类才会被初始化,通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。

被动引用——例2


 1 package day_13_passiveReference;
 2
 3 /**
 4  * @author turbo
 5  *
 6  * 2016年9月19日
 7  */
 8 public class Main {
 9
10     /**
11      * @param args
12      */
13     public static void main(String[] args) {
14         SuperClass[] sca = new SuperClass[10];
15     }
16
17 }

我们还是利用例1的SuperClass来创建一个数组,这个应该都知道,在创建数组时并不会初始化,在编程不注意的时候可能常常因为没有初始化数组导致空指针的情况。它仅仅做了创建一个大小为10的数组。

被动引用——例3


 1 package day_13_passiveReference;
 2
 3 /**
 4  * @author turbo
 5  *
 6  * 2016年9月19日
 7  */
 8 public class ConstClass {
 9     static {
10         System.out.println("ConstClass init!");
11     }
12
13     public static final String HELLO = "hello";
14 }
 1 package day_13_passiveReference;
 2
 3 /**
 4  * @author turbo
 5  *
 6  * 2016年9月19日
 7  */
 8 public class Main {
 9
10     /**
11      * @param args
12      */
13     public static void main(String[] args) {
14         System.out.println(ConstClass.HELLO);
15     }
16
17 }

这个例子的输出会初始化ConstClass类吗?

答案是并不会。这是因为常量在编译阶段会存入调用类的常量池中,本质上并没有直接饮用到定义常量的类。进一步解释,虽然在main方法中引用了ConstClass类中的常量HELLO,但其实在编译阶段通过常量传播优化,已经将此常量的值“hello”存储到了Main类的常量池中,之后对ConstClass.HELLO的引用实际上都被转化为Main类对自身常量池的引用。也就是说,两个类在编译过后实际上不存在任何联系了。

类加载时机就讲到这里了,类加载的初始化步骤非常重要的一个步骤,理解清楚“初始化”对我们写出高质量不易出错的代码非常重要。

时间: 2024-10-10 05:41:21

虚拟机类加载机制——类加载时机的相关文章

Jvm(51),虚拟机类加载机制----类加载的时机

在了解下面的举的例子之前我们先来了解一下类的加载顺序? 1 public class test1 { 2 public static void main(String[] args) { 3 C c = new C(); 4 } 5 } 6 7 class A{ 8 int a = 0; 9 Method m = new Method(a); 10 static int a1 = 10; 11 static{ 12 System.out.println("A:执行静态代码块A"+a1)

虚拟机类加载机制--类加载器

准备阶段的"通过一个类的全限定名来获取描述此类的二进制字节流"这个动作放到了Java虚拟机外部去实现,以便让应用程序自己决定如何如获取所需要的类.实现这个动作的代码模块称为"类加载器" 1.类与类加载器 每一个类加载器都有一个独立的类名称空间,由类加载器和类一起合作才能确定一个类在虚拟机中的唯一性.也就是说:比较两个类是否"相等",即使他们来自同一个Class文件,在同一个虚拟机上被加载,如果加载它们的类加载器不同,那么这两个类就不相等. 这里的

虚拟机类加载机制------类加载的过程

1.加载 虚拟机需要干三件事: ①.通过一个类的的全限定名来获取定义此类的二进制字节流(没有规定二进制字节流从那里获取,怎样获取,许多java技术也都建立在这基础上) ②将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构(将常量池转变成运行时常量池) ③在内存中生成一个代表这个类的java.lang.Class对象,作为方法区着各类的各种数据的访问入口. 相比较于类加载过程的其他阶段,非数组类获取类的二进制字节流的动作是开发人员可控性最强的,因为加载阶段既可以使用系统提供的引导类加载器

Jvm(56),虚拟机类加载机制----类加载的过程----初始化

类初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制.到了初始化阶段,才真正开始执行类中定义的Java程序代码(或者说是字节码). 在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者可以从另外一个角度来表达:初始化阶段是 执行类构造器<clinit>()方法的过程.我们在下文会讲解<clinit>()方法是怎么生成的

Jvm(55),虚拟机类加载机制----类加载的过程----解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,符号引用在前一章讲解 Class文件格式的时候已经出现过多次,在Class文件中它以CONSTANT_Class_info. CONSTANT_Fieldref_info.CONSTANT_Methodref_info等类型的常量出现,那解析阶段中所说的直接引用与符号引用又有什么关联呢? 符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可

JVM类加载机制---类加载的过程

一.类加载的时机 类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载.验证.准备.解析.初始化.使用.卸载 7个阶段,其中验证.准备.解析 3个部分统称为 连接. 二.具体步骤解析 1.加载 加载阶段,虚拟机要完成以下3件事情: 1)通过一个类的全限定名来获取定义此类的二进制字节流: 2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构: 3)在内存中生成一个代表这个类的java.lang.class对象,作为方法区这个类的各种数据的访问入口. 相对于类加载过

Java 虚拟机程序执行:02 虚拟机的类加载机制

虚拟机的类加载机制 虚拟机的类加载机制 类加载的时机 类的显式加载和隐式加载 类加载的过程 类的生命周期 加载 加载的 3 个阶段 分类 验证 准备 解析 初始化 类加载器 如何判断两个类 “相等” 类加载器的分类 双亲委派模型 类加载的时机 JVM 会在程序第一次主动引用类的时候,加载该类,被动引用时并不会引发类加载的操作.也就是说,JVM 并不是在一开始就把一个程序就所有的类都加载到内存中,而是到不得不用的时候才把它加载进来,而且只加载一次.那么什么是主动引用,什么是被动引用呢? 主动引用

Java虚拟机 - 类加载机制

[深入Java虚拟机]之四:类加载机制 类加载过程     类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载.验证.准备.解析.初始化.使用和卸载七个阶段.它们开始的顺序如下图所示: 其中类加载的过程包括了加载.验证.准备.解析.初始化五个阶段.在这五个阶段中,加载.验证.准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定).另外注意这里的几个阶段是按顺序开

一文读懂Java类加载机制

Java 类加载机制 Java 类加载机制详解. @pdai Java 类加载机制 类的生命周期 类的加载:查找并加载类的二进制数据 连接 验证:确保被加载的类的正确性 准备:为类的静态变量分配内存,并将其初始化为默认值 解析:把类中的符号引用转换为直接引用 初始化 使用 卸载 类加载器, JVM类加载机制 类加载器的层次 寻找类加载器 类的加载 JVM类加载机制 自定义类加载器 参考文章 类的生命周期 其中类加载的过程包括了加载.验证.准备.解析.初始化五个阶段.在这五个阶段中,加载.验证.准