Java中 类的加载概述和加载时机


参考自《深入理解Java虚拟机》,所以本博客还是有可信度的!!!


类的加载

当程序要使用某个类时,如果该类还未被加载到方法区内存中,则系统会通过加载,连接(验证,准备,解析),初始化三步来实现对这个类进行初始化。

1.加载

注意:“加载”是 “类加载”过程的一个阶段,希望读者没有混淆这两个看起来很相似的名词。

加载要做三件事情:

  • 通过一个类的全限定名来获取定义此类的二进制字节流。
  • 将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构。
  • 在方法区中生成一个代表这个类的 java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式由虚拟机实现自行定义,虚拟机规范未规定此区域的具体数据结构。然后在内存中实例化一个java.lang.Class类的对象(并没有明确规定是在Java堆中,对于HopSpot虚拟机而言,Class对象比较特殊,它虽然是对象,但是存放在方法区里面),这个对象将作为程序访问方法区中的这些类型数据的外部接口。

需要注意的是:加载阶段与连接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但这些夹在加载阶段之中进行的动作,仍然属于连接阶段的内容,这两个阶段的开始时间仍然保持着固定的先后顺序。

2.连接

  • 验证 这个阶段的目的是为了确保Class文件的字节流中包含的信息(方法区信息)符合当前虚拟机要求,并不会危害虚拟机自身的安全。如果有错误就会抛出异常!!!

对于虚拟机的类加载机制来说,验证阶段是一个非常重要的、但不是一定必要(因为对程序运行期没有影响) 的阶段。如果所运行的全部代码(包括自己编写的及第三方中的代码)都已经被反复使用和验证过,那么在实施阶段就可以考虑使用 -Xverify:none 参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

  • 准备 准备阶段是正式为类变量(静态成员变量)分配内存并设置类变量默认初始化值的阶段,这些变量所使用得内存都将在方法区中进行分配。这个阶段中有两个容易产生混淆的概念需要强调一下。首先,这个时候进行内存分配的仅包括类信息(被static修饰的变量),而不包括实例变量(非静态成员变量),实例变量将会在对象实例化时随着对象一起分配在Java堆中。其次,这里所说的初始值是数据类型的零值,假设一个类变量的定义为:
public static int value=123;

那变量 value 在准备阶段过后的初始化值为 0 而不是 123,因为这时候尚未开始执行任何 Java方法,表达式等,而把 value 赋值为123的putstatic指令是程序被编译后,存放于类构造器<clinit>()方法之中,所以把value赋值为123的动作将在初始化阶段才会执行。下列表中所有基本数据类型的零值:

这里写图片描述

看到没有引用类型为null哦!!!

  • 解析 解析阶段是虚拟机将常量池的符号引用替换为直接引用的过程,符号引用在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info 等类型出现,那解析阶段中所说的直接引用与符号引用又有什么关联呢??

1.符号引用(Sysmbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。

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

注意:

  • Class对象只是对.class文件的一个描述性信息,并没有存储任何的值。
  • Class对象是对操作方法区类的数据结构的接口。
  • 方法区类的数据结构已经初始化了静态成员变量和执行了static语句块后的数据结果。
  • 下次修改static成员变量后,修改结果会直接反应到方法区里面类的数据结构中。
  • 任何类被使用时系统都会建立一个Class对象,并且是唯一的一个。

3.初始化

  • 细节上:执行类变量的赋值语句和静态初始化语句,给静态成员赋值。
  • 具体:深入java虚拟机这样说:所有的类变量初始化语句和类型的静态初始化器都被Java编译器收集在一起,放到一个特殊的方法中,对于类和接口来说,这就是类的初始化方法和接口的初始化方法,在类和接口中的Java class文件中,这个方法被称为”<clinit>”。通常的Java程序是无法调用这个方法的,只能被虚拟机调用。专门把类型的静态变量设置为它的正确初始化值。即执行赋值语句。如果当前类存在直接超类的话,就先初始化超类,第一个初始化的永远是Object,然后就是被主动使用的类的继承树上的所有的类。注意,在初始化阶段会初始化静态代码块。但是不会初始化构造方法,除了创建类的实例、使用java.exe命令运行某个主类的这几个时机(java.exe运行,本质上就是调用main方法,所以必须要有main方法才行)。

类的加载时机

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载,验证,准备,解析,初始化,使用,卸载7个阶段。其中验证、准备、解析三个部分统称为连接。7个阶段的顺序如图:

这里写图片描述

加载、验证、准备、初始化和卸载这5个阶段的顺序是肯定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段后再开始,这是为了支持Java语言的运行时绑定(也被称为动态绑定或者晚期绑定)。注意,这里笔者写的是按部就班地“开始”,而不是按部就班地“进行”或“完成”,强调这点是因为这些阶段通常都是互相交叉地混合式进行的,通常会在一个阶段执行的过程中调用、激活另外一个阶段。

什么情况下需要开始类加载过程的第一个阶段:加载?Java虚拟机规范中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把握。但是对于初始化阶段,虚拟机规范则是严格规定了下面的几种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):

  • 创建类的实例
  • 访问类的静态变量(注意:当访问类的静态并且final修饰的变量时,不会触发类的初始化。),或者为静态变量赋值。
  • 调用类的静态方法(注意:调用静态且final的成员方法时,会触发类的初始化!一定要和静态且final修饰的变量区分开!!)
  • 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。如:Class.forName("bacejava.Langx");
  • 注意通过类名.class得到Class文件对象并不会触发类的加载。
  • 初始化某个类的子类
  • 直接使用java.exe命令来运行某个主类(java.exe运行,本质上就是调用main方法,所以必须要有main方法才行)。
  • 注意:通过子类调用父类的静态成员时,只会初始化父类而不会初始化子类。 因为没有调用子类的相关静态成员,这也叫做能不加载不加载原则。
  • 注意:调用静态成员时,会加载静态成员真正所在的类及其父类。 这也好理解,因为本类的父类没有加载就会先去加载父类。
  • 注意:类的加载成功后,即静态成员都被加载后,是不会再加载第二次的。只有非静态成员,如非静态成员变量、非静态代码块、非静态方法(不调用不加载)、构造方法都会被多次实例化的时候多次加载。
  • 注意:如果静态属性有 final 修饰时,则不会加载,当成常量使用。例:public static final int a =123; public static final int a=5/3*2+1;public static final double a=Math.PI;等
  • 注意:但是静态属性 有final修饰 时也有情况会被加载,public static final int a=getNum();这样也会被加载。getNum()是静态方法,并且不管这个静态方法是子类的还是父类的;
  • 在上面触发类被初始化的情况称为对类的主动引用,除此之外,那些引用类的方式没有触发初始化的叫做被动引用
另外下面这几种方式也不会进行类的初始化:
访问静态且final修饰的成员变量的时候,不会触发类的初始化!!

一:

/*
         * 通过new 数组空间,并不会对类进行初始化!!
         */

        Langx langx[]=new Langx[10];

二:

public class B {
    public static final int XXX=3; //写表达式也没有关系,public static final int a=10+9;
    public static final String STRING="FireLang";

    static{
        System.out.println("初始化静态代码块!!!!");//如果触发了类的初始化必须会执行静态代码块!!
    }

    public final static void showStatic(){
        System.out.println("调用了静态成员方法");
    }
}

测试代码:

public class TestB {
    public static void main(String[] args) {
        System.out.println(B.XXX);
        System.out.println(B.STRING);
    }
}

输出结果:

3
FireLang

上述代码并没有进行类的初始化,这是因为虽然在Java源码中引用了B类中的常量 STRING 和 XXX,但其实在编译阶段通过常量传播优化,已经将这两个常量的值 “FireLang”和 3 存储到了 TestB 类的常量池中,以后 TestB 对常量 B.XXX 和 B.STRING 的引用实际转换为 TestB 类对自身常量池的引用了。也就是说,实际上 TestB 的Class文件之中并没有 B 类的符号引用入口,这两个类在编译成 Class之后就不存在任何联系了(如果没看懂,就把这句话仔细斟酌!!)。

另外:在《深入理解Java虚拟机》中提到过public static final int XXX=3;该变量在准备阶段会直接初始化为3,而不是0。但是我个人比较赞同静态常量的传播优化这个说法!!(如果这句没看懂请参考《深入理解Java虚拟机》p233),但是不论用哪儿一种方式,两种方式都能够实现。

但是访问以下final修饰的static常量会触发类的初始化。

public class Langx {
    /*
     * 在复杂类型中只有字符串不会触发初始化过程!!
     */
    public static final String STRING="LangShen";//不会触发初始化过程!!,没有经历自动装箱,字符串是编译期直接保存在常量池中的!!!获取值时处理方法和它们不一样,它是一个常量表,一个字符序列对应一个对象来获取!!!

    /*
     * 以下都会触发初始化过程!!
     * 
     * 这些初始化指的是类的初始化!!!
     */
    public static final LangFx LANG_FX=null;//自定义类,会触发初始化过程!!,当然你就更别说new 一个对象了,new 一个也会触发初始化过程!!
    public static final Integer INTEGER=45;//会触发初始化过程!!,因为经历了自动装箱
    public static final Character CHARACTER='X';//会触发初始化过程!!因为经历了自动装箱
    public static final String STRING2=new String("456");//会触发初始化过程!!,因为你是new 出来的!!!
    static{
        System.out.println("初始化静态代码块!!");
    }
}

重点: 当你以后会学到数据库连接。在那里你会遇到为数据库连接注册驱动类。当你明白了上述内容就知道为什么需要注册驱动类了。也就是初始化阶段能够运行静态初始化语句是关键。这是为什么写上Class.forName(数据库驱动类名)能够注册驱动的原因

相关细节方面:

在类被装载、连接和初始化,这个类就随时都可能使用了。对象实例化和初始化是就是对象生命的起始阶段的活动,在这里我们主要讨论对象的初始化工作的相关特点。

Java虚拟机规范没有强制性约束在什么时候开始类加载过程的第一个阶段——加载,但是对于初始化阶段,虚拟机规范则严格规定了有以下几种情况必需立即对类进行“初始化”(而加载、验证、准备、解析阶段则必需在此之前开始),这几种情况归类如下:

  1. 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令最常见的Java代码场景是:使用new关键字实例化对象时、读取或者设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)时、以及调用一个类的静态方法的时候。
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。如:Class.forName("bacejava.Langx");
  3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要触发父类的初始化。
  4. 当虚拟机启动时,用户需要指定一个执行的主类(包含main()方法的类),虚拟机会先初始化这个类。对于这四种触发类进行初始化的场景,在java虚拟机规范中限定了“有且只有”这四种场景会触发。这四种场景的行为称为对类的主动引用,除此以外的所有引用类的方式都不会触发类的初始化,称为被动引用。《深入理解java虚拟机》 有解释主动引用和被动引用。

虽然《深入理解Java虚拟机》上面说了5种情况,但是,这上面这几种情况比较常见。

详见查看《深入理解Java虚拟机p234》

总结:

也就是说,当我们通过反射获取class文件对象成功自后Class.forName("bacejava.Langx");,class文件对象已经进行了这几个步骤 :加载(加载class文件,创建class文件对象)、连接(验证、准备、解析)、初始化方法区的.class数据结构等工作。初始化成功后,该类的静态成员就可以正常使用了。剩下的非静态成员变量需要实例化才能被使用。于是也能大概理解 静态成员变量只有一份,而非静态成员有多份,这样能够实现不同的数据分离。

在类加载的过程中是不会初始化 非静态成员变量,非静态代码块,构造方法,只有在实例化的时候才会被初始化。类加载完成只是为了做好静态成员变量被调用的准备。

有关final的基本及深入可以查看博客:有必要更了解final

关于常量池的详细讲解请参见这篇博客:Java内存图以及堆、栈、常量区、静态区、方法区的区别

  • 4
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值