JVM 学习笔记 1 : 类的加载和初始化

本文详细介绍了Java虚拟机(JVM)中类的加载和初始化过程,包括加载、验证、准备、解析和初始化五个阶段。讨论了各个阶段的任务,如加载时的二进制字节流读取,验证阶段的格式验证、元数据验证、字节码验证和符号引用验证,以及初始化阶段执行类构造器<clinit>()方法的细节。此外,还阐述了主动引用和被动引用对类初始化的影响。
摘要由CSDN通过智能技术生成

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:

  1. 加载(Loading)
  2. 验证(Verification)
  3. 准备(Preparation)
  4. 解析(Resolution)
  5. 初始化(Initialization)
  6. 使用(Using)
  7. 卸载(Unloading)


在这 7 个阶段中,验证、准备和解析 3 个部分统称为链接(Linking)。


加载

类的加载阶段就是由类加载器根据类的全限定名来读取此类的二进制字节流到 Java 虚拟机内部,并存储在运行时内存区中的方法区内,然后在Java 堆中创建一个与目标类型对应的java.lang.Class对象实例,这个 Class 对象将被作为方法区中该类的各种数据的访问入口。

如果继承ClassLoader并重写findClass()方法来实现自定义的类加载器,开发人员不仅可以从字节码文件中读取,还可以从网络中读取,甚至可以从数据库中读取一个类的二进制字节流。


验证

验证是链接中的第一个阶段。验证阶段中,Java 虚拟机所执行的一系列验证操作大致可以划分为:

  1. 格式验证

  2. 元数据验证

  3. 字节码验证

  4. 符号引用验证

当一个类的二进制信息被加载进 Java 虚拟机时,验证阶段中的格式验证将会同时进行。

格式验证的主要任务就是检查当前正在加载的字节码文件是否符合 class 文件格式的规范,例如:

  1. 字节码文件中的前 4 个字节是否是 0xCAFEBABE

  2. 编译所使用 JDK 的主次版本号是否在当前 Java 虚拟机的处理范围之内(高版本的 JDK 编译的字节码文件无法在低版本的 Java 虚拟机中运行)

格式验证成功以后,类加载器才会将类的二进制信息加载到方法区中,而后续的验证将在方法区中进行。

元数据验证的主要任务是验证字节码信息是否符合 Java 语言规范,例如:

  1. 检查一个被标记为 final 的类是否包含派生类

  2. 检查一个类中的 final 方法是否被派生类重写

  3. 检查超类和派生类之间是否存在不兼容的方法(方法签名相同,返回值不同)

字节码验证的主要任务是对类的方法体进行校验分析,以确保被校验的类的方法在运行时不会对 Java 虚拟机产生不良的影响。

符号引用验证的主要任务,是在解析阶段将常量池中的符号引用转换为直接引用时对这些需要被转换的符号引用进行验证,例如检查是否能够通过符号引用中描述的全限定名定位到指定的类。

当 Java 虚拟机执行完验证阶段后,就会执行链接阶段中的下一个阶段准备阶段


准备

准备阶段会为类的静态成员变量(而不包括实例变量)分配内存并根据变量的数据类型设置默认的初始值(而不是开发人员显式赋予的值)。

变量类型初始值
byte(byte) 0
short(short) 0
int0
long0L
float0.0f
double0.0d
char‘\0000’
booleanfalse
referencenull

注意:

  1. 对于静态成员变量和实例变量,如果不显式地为其赋值而直接使用,则 Java 虚拟机会为其赋予默认的初始值。

  2. 对于局部变量,在使用前必须显式地为其赋值,否则编译不通过。

  3. 对于同时被staticfinal修饰的常量,必须在声明时显式地为其赋值,否则编译不通过。

  4. 对于只被final修饰的实例变量,既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值,Java 虚拟机不会为其赋予默认的初始值。

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


解析

解析阶段的主要任务是将常量池中的符号引用全部转换为直接引用,包括类、接口、字段和方法的符号引用。

  • 符号引用(Symbolic Reference):符号引用以一组符号来描述所引用的目标,符号引用可以是任何形式的字面量。符号引用与 Java 虚拟机实现的内存布局无关,引用的目标并不一定已经在内存中。

  • 直接引用(Direct Reference):直接引用可以是直接指向目标的指针、相对偏移量或一个能间接定位到目标的句柄。直接引用与 Java 虚拟机实现的内存布局相关,同一个符号引用在不同的 Java 虚拟机上转换得到的直接引用一般不同,如果有了直接引用,那引用的目标必定已经在内存中存在。

注意:

Java 虚拟机规范并没有明确要求解析阶段一定要按照顺序执行,因此解析阶段可以在初始化之后执行,这样可以支持 Java 语言的运行时绑定。


初始化

初始化是类加载过程中的最后一个阶段。在初始化阶段中,所有的静态成员变量的初始化语句和静态代码块都会在 Java 源码编译时,被编译器按照其在源文件中出现的顺序收集起来存放到<clinit>()方法中。可以说,初始化阶段是执行类构造器 <clinit>() 方法的过程。

Java 虚拟机会确保一个类的<clinit>()方法执行之前,它的超类的<clinit>()方法已经被执行。同时还会确保一个类的<clinit>()方法在多线程环境中被正确地加锁和同步。如果有多个线程去同时初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其它线程都需要等待,直到活动线程执行<clinit>()方法完毕。

<clinit>()方法对于类来说并不是必需的,编译器在以下几种情况可以不为这个类产生<clinit>()方法:

  1. 类中没有静态代码块。

  2. 类中没有静态成员变量。

  3. 类中只是声明静态成员变量,而没有进行赋值操作。

  4. 类中的静态成员变量被final修饰,并通过编译时常量表达式进行赋值。

具体的初始化过程可以通过下面的示例代码来了解:

public class One {

    public One() {
        System.out.println("one");
    }
}


public class Two extends One {

    private TwoProperty1 twoProperty1 = new TwoProperty1();

    private TwoProperty2 twoProperty2 = new TwoProperty2();

    private int twoProperty3 = h();

    private static TwoStaticProperty1 twoStaticProperty1 = new TwoStaticProperty1();

    private static int twoStaticProperty2 = f();

    private static final TwoStaticProperty3 TWO_STATIC_PROPERTY_3 = new TwoStaticProperty3();

    private static final int TWO_STATIC_PROPERTY_4 = 300;

    static {
        System.out.println("two static code");
        System.out.println("twoStaticProperty2 in static code = " + twoStaticProperty2);
        System.out.println("TWO_STATIC_PROPERTY_3 in static code = " + TWO_STATIC_PROPERTY_3);
        System.out.println("TWO_STATIC_PROPERTY_4 in static code = " + TWO_STATIC_PROPERTY_4);
    }

    private static int f() {
        System.out.println("f");
        System.out.println("twoStaticProperty2 in f() = " + twoStaticProperty2);
        System.out.println("TWO_STATIC_PROPERTY_3 in f() = " + TWO_STATIC_PROPERTY_3);
        System.out.println("TWO_STATIC_PROPERTY_4 in f() = " + TWO_STATIC_PROPERTY_4);
        return 100;
    }

    private int h() {
        System.out.println("h");
        System.out.println("twoProperty3 in h() = " + twoProperty3);
        return 200;
    }

    public Two() {
        System.out.println("two");
        System.out.println("twoProperty3 in Two() = " + twoProperty3);
    }
}


public class TwoProperty1 {

    public TwoProperty1() {
        System.out.println("two property 1");
    }
}


public class TwoProperty2 {

    public TwoProperty2() {
        System.out.println("two property 2");
    }
}


public class TwoStaticProperty1 {

    public TwoStaticProperty1() {
        System.out.println("two static property 1");
    }
}


public class TwoStaticProperty3 {

    public TwoStaticProperty3() {
        System.out.println("two static property 3");
    }
}


public class Three extends Two {

    public Three() {
        System.out.println("three");
    }
}


public class Four extends Three {

    private FourProperty fourProperty = new FourProperty();

    private static FourStaticProperty fourStaticProperty = new FourStaticProperty();

    public Four() {
        System.out.println("four");
    }
}


public class FourProperty {

    public FourProperty() {
        System.out.println("four property");
    }
}


public class FourStaticProperty {

    public FourStaticProperty() {
        System.out.println("four static property");
    }
}

Java 虚拟机规范中规定,对类进行主动引用时如果类没有进行过初始化,则先触发其初始化。

对类的主动引用包括以下几种:

  1. 遇到newgetstaticputstaticinvokestatic这 4 条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这 4 条指令的最常见的 Java 代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。

  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。

    public class Eight {
    
        public static void main(String[] args) throws Exception {
            Class four = Class.forName("com.xxx.xxx.Four");
            four.newInstance();
        }
    }
    
    //输出:
    //two static property 1
    //f
    //twoStaticProperty2 in f() = 0
    //TWO_STATIC_PROPERTY_3 in f() = null
    //TWO_STATIC_PROPERTY_4 in f() = 300
    //two static property 3
    //two static code
    //twoStaticProperty2 in static code = 100
    //TWO_STATIC_PROPERTY_3 in static code = com.mj.wcs.service.impl.TwoStaticProperty3@5b37e0d2
    //TWO_STATIC_PROPERTY_4 in static code = 300
    //four static property
    //one
    //two property 1
    //two property 2
    //h
    //twoProperty3 in h() = 0
    //two
    //twoProperty3 in Two() = 200
    //three
    //four property
    //four
    
  3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。

  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。

  5. 当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStaticREF_putStaticREF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

对类进行被动引用时不会触发其初始化。

对类的被动引用包括以下几种:

  1. 通过子类引用父类的静态字段,不会导致子类初始化。

    public class Five {
    
        public static void main(String[] args) {
            System.out.println("Three.twoStaticProperty2 in Five.main() = " + Three.twoStaticProperty2);
        }
    }
    
    //输出:
    //two static property 1
    //f
    //twoStaticProperty2 in f() = 0
    //TWO_STATIC_PROPERTY_3 in f() = null
    //TWO_STATIC_PROPERTY_4 in f() = 300
    //two static property 3
    //two static code
    //twoStaticProperty2 in static code = 100
    //TWO_STATIC_PROPERTY_3 in static code = com.mj.wcs.service.impl.TwoStaticProperty3@5b37e0d2
    //TWO_STATIC_PROPERTY_4 in static code = 300
    //Three.twoStaticProperty2 in Five.main() = 100
    
  2. 通过数组定义来引用类,不会触发此类的初始化。

    public class Six {
    
        public static void main(String[] args) {
            Two[] twos = new Two[]{};
        }
    }
    
    //输出:
    //
    
  3. 常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。

    public class Seven {
    
        public static void main(String[] args) {
            System.out.println("Two.TWO_STATIC_PROPERTY_4 in Seven.main() = " + Two.TWO_STATIC_PROPERTY_4);
            System.out.println("Three.TWO_STATIC_PROPERTY_4 in Seven.main() = " + Three.TWO_STATIC_PROPERTY_4);
        }
    }
    
    //输出:
    //Two.TWO_STATIC_PROPERTY_4 in Seven.main() = 300
    //Three.TWO_STATIC_PROPERTY_4 in Seven.main() = 300
    

参考资料:

《深入理解Java虚拟机》周志明著

《Java 虚拟机精讲》高翔龙著

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值