类加载过程、类成员初始化顺序

一、 概述

       上一篇文章介绍了class文件的存储细节,class文件包括了类的各种描述信息,但是Java程序的运行需要在内存中实现,
那么虚拟机是如何加载这些class文件的?class文件中的静态结构是如何转换成实际的存储结构的?内存分配是如何完成的?这些都是本篇文章要讨论的内容。

       虚拟机将类的描述文件class文件加载到内存,并且进行安全校验、数据类型解析、内存分配以及初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程就是虚拟机的类加载机制。与解释执行语言不通,Java语言是编译型语言,类型的连接(即加载、连接、初始化过程)是在程序运行期进行的,这样就可以在程序运行期间动态的加载一些内容,这种形式虽然会增加系统的运行开销,但是可以让程序设计更加的灵活。

  • hotspot java虚拟机Class对象是放在 方法区 还是堆中 ? Class 对象确实在堆中。
  • static静态成员存放位置    static 成员保存在 Class 实例的尾部。

 二、类加载的时机

       一个类从加载到内存开始,一直到被卸载结束,它的整个生命周期包括加载、连接(验证、准备、解析)、初始化、使用、卸载阶段,其中连接阶段包括验证、准备和解析过程,这几个过程的发生顺序如下图所示:

类加载过程、类成员初始化顺序_第1张图片

        什么时候触发类的加载动作呢?Java虚拟机规范并没有强制规定类加载时机,这个情况需要具体的虚拟机进行自由实现,例如Tomcat再启动时,会启动引导类加载器、拓展类加载器、通用类加载器和应用类加载器,引导类加载器、拓展类加载器和通用类加载器首先加载和初始化一些类(jvm所需类、Tomcat所需类、及一些通用类),其余的类是收到请求时才进行类的加载操作。
        虽然虚拟机没有明确说明类加载的时机,但是对于初始化阶段,虚拟机规范给了严格规定,有且只有以下几种情况必须立即对类进行初始化:

  1. 遇到new、putstatic、getstatic及invokestatic这4条字节码指令时,如果类没有初始化,则立即进行初始化,这4个命令分别代表实例化一个类、设置&读取一个静态字段(没有被final修饰)、调用类的静态方法;
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有初始化;
  3.  当初始化一个类的时候,发现其父类没有初始化;
  4. 当虚拟机启动时,需用将执行启动的主类(有main()方法的那个类)进行初始化;
  5. 当使用动态语言时,如果一个java.lang.invoke.MethodHandle实例最终的解析结果是REF_getStatic、REF_putStatic、REF_invokeStatic句柄时,并且这个句柄对应的类没有初始化。

三、类加载的过程

接下来讨论类加载的详细过程,包括加载、验证、准备、解析和初始化阶段。

3.1 加载

    "加载"是"类加载"这个过程的一个阶段,是 “类加载”过程中最先开始进行的操作,加载阶段,虚拟机需要完成三件事:

  •     1、 根据类的全限定名获取定义此类的二进制字节流;
  •     2、 将这个字节流代表的静态存储结构转换为方法区的运行时数据结构;
  •     3、 在堆中为这个类生成一个java.lang.Class对象。

       Java的虚拟机规范并没有规定从哪里获取、怎样获取二进制字节流,这个阶段也是用户参与度最高的阶段,用户可以根据二进制文件的不同形式在自定义类加载器控制字节流的获取方式,比如成熟的二进制获取方式和类加载器有:

  •     1、 从Zip包中读取二进制文件,比如常见的jar、war、ear包;
  •     2、 运行时动态生成,比如动态代理技术,在java.lang.reflect.Proxy中,使用 ProxyGenerator.generateProxyClass为各种就接口生成形如"*$Proxy"的代理类的二进制字节流;
  •     3、 从网络中获取,这种场景比较常见的是Applet应用;
  •     4、 其他文件生产,比如jsp文件生成的二进制class文件;

    ……
        数组的加载跟普通类型加载有所不同,因为数组本身不是通过类加载器加载产生的,数组类是虚拟机自动生成的,但是数组的类型是通过类加载器完成加载的,数组类的创建过程需要遵循以下规则:

  •     1、 如果数组的类型是引用类型,则引用类型需要使用递归来进行加载,并且数组需要被加载该数组类型的类加载器的命名空间上进行标识;
  •     2、 如果数组的类型不是引用类型,是基本数据类型,Java虚拟机将会把数组标记为与引导类加载器关联;
  •     3、 数组的可见性与数组类型的可见性保持一致,如果数组类型是基本类型,则默认可见性为public。

3.2 验证

       验证是类加载的第二个阶段,这个阶段也是持续时间最长(从阶段连续性来说),这个阶段从加载开始进行,一直进行到解析阶段结束。验证是为了保证class文件中的内容是符合虚拟机规范的二进制字节流,防止通过执行一些不安全的二进制字节流而导致虚拟机奔溃。

       Java语言本身是安全的语言,它做了很多的安全校验,比如类型转换、非正常的分支语句跳转、不合法的名称定义等等。但是我们知道,Java虚拟机并不只是执行Java语言编译后的class文件,它可以执行所有的二进制字节流文件(只要符合文件规范),所以我们不能保证其他的文件是合法的,所以需要进行一些安全校验,以保证虚拟机执行的代码是不会危害虚拟机本身安全的。从整体来看,类加载过程的验证阶段可以分为四个部分:文件格式验证、元数据验证、字节码验证和符号引用验证。

3.3 准备

        准备阶段是证实为类变量分配内存并且设置初始化值的阶段,这些变量所使用的内存都在方法区分配。这个阶段进行初始化的数据只有静态字段,并且是赋值初始化值(final修饰的字段除外),不是代码中定义的值。
       Java各数据类型初始值:

类加载过程、类成员初始化顺序_第2张图片

3.4 解析

        解析阶段是虚拟机将常量池中符号引用转化为直接引用的过程,符号引用在之前已经介绍过了,在class文件中以如"CONSTANT_Class_info"、"CONSTANT_Fieldref_info"、"CONSTANT_Methodref_info"格式存在,接下来我们从更深层次的角度讨论这两个引用的区别。

       符号引用存在class文件中的常量池,包括类和接口的全限定名、字段的名称和描述符以及方法的名称和描述符。jvm加载class的时候就可以凭着这三者进行动态连接,得到具体的内存地址。当第一次运行时,要根据字符串的内容,到该类的方法表中搜索这个方法。运行一次之后,符号引用会被替换为直接引用,下次就不用搜索了。直接引用就是偏移量,通过偏移量虚拟机可以直接在该类的内存区域中找到方法字节码的起始位置。

3.5 初始化

       类初始化阶段是类加载过程的最后阶段。在这个阶段,java虚拟机才真正开始执行类定义中的java程序代码。Java虚拟机是怎么完成初始化的呢?这要从编译开始讲起。在编译的时候,编译器会自动收集类中的所有静态变量(类变量)和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是根据语句在java代码中的顺序决定的。收集完成之后,会编译成java类的 static{} 方法,java虚拟机则会保证一个类的static{} 方法在多线程或者单线程环境中正确的执行,并且只执行一次。在执行的过程中,便完成了类变量的初始化。值得说明的是,如果我们的java类中,没有显式声明static{}块,如果类中有静态变量,编译器会默认给我们生成一个static{}方法。

类成员初始化顺序

       类成员什么时候会被初始化呢?一般来说:"类的代码在初次使用时才被加载",加载过程包括了初始化。比如说new A()调用构造函数时,类中全部成员都会被初始化。

       但对于static域(包括静态成员变量、静态代码块、静态方法),当某个static域被调用时,类中的的所有staict就会被初始化,按照定义顺序(即书写顺序)初始化,且只会初始化一次(N个实例共用)所以,static域的初始化优先级要优于普通成员(即非静态域)

      下文代码例子名称解释: 静态域:静态代码块、静态成员变量 非静态域:非静态代码块、非静态成员变量。

在没有继承父类的情况下:

class HelloA {
    public HelloC helloC1 = new HelloC("普通成员C1 构造函数前");

    public HelloA() {
        System.out.println("构造函数A");
    }

    public HelloC helloC2 = new HelloC("普通成员C2 构造函数后");

    static {
        System.out.println("静态块A");
    }

    public HelloC helloC3 = new HelloC("普通成员C3 静态块后");

    {
        System.out.println("非静态块A");
    }

    public HelloC helloC4 = new HelloC("普通成员C4 非静态块后");

    public static HelloC helloC5 = new HelloC("静态成员C5");

    public static void main(String[] args) {
        new HelloA();
    }
}

class HelloC {
    public HelloC(String str) {
        System.out.println(str);
    }
}

运行结果:
静态块A
静态成员C5
普通成员C1 构造函数前
普通成员C2 构造函数后
普通成员C3 静态块后
非静态块A
普通成员C4 非静态块后
构造函数A

初始化顺序为:静态域 -> 非静态域 -> 构造函数 以上面优先级初始化。相同域按书写顺序初始化。

在有继承父类的情况下:

class HelloA extends HelloB{

    public HelloA() {
        System.out.println("子类:构造函数A");
    }

    static {
        System.out.println("子类:静态块A");
    }

    {
        System.out.println("子类:非静态块A");
    }

    public static void main(String[] args) {
        new HelloA();
    }
}

class HelloB {
    public HelloB() {
        System.out.println("父类:构造函数B");
    }

    static {
        System.out.println("父类:静态块B");
    }

    {
        System.out.println("父类:非静态块B");
    }
}

运行结果:
父类:静态块B
子类:静态块A
父类:非静态块B
父类:构造函数B
子类:非静态块A
子类:构造函数A

从结果可以看出,初始化顺序为:父类静态域->子类静态域->父类非静态域->父类构造函数->子类非静态域->子类构造函数

这里说明一点:这是初始化顺序,不等同于语句程序的执行过程。因此在上面的初始化顺序里没有成员方法(静态或者非静态都没有),这是因为成员方法都是调用了才执行,虽然静态方法已经被加载进了方法区,但初始化过程中并没有执行过。

  • 静态域优先于非静态域
  • 父类优先于子类
  • 非静态域(非静态代码块、非静态成员变量)优先于构造方法
  • 相同域按照代码书写执行

阿里一道题:

public class Alibaba {

    public static int k = 0;
    public static Alibaba t1 = new Alibaba("t1");
    public static Alibaba t2 = new Alibaba("t2");
    public static int i = print("i");
    public static int n = 99;

    private int a = 0;
    public int j = print("j");

    public Alibaba(String str) {
        System.out.println(++k + " : " + str + " i=" + i + " n=" + n);
        ++i;
        ++n;
    }

    {
        print("构造快");
    }

    static {
        print("静态块");
    }

    public static int print(String str) {
        System.out.println(++k + " : " + str + " i=" + i + " n=" + n);
        ++n;
        return ++i;
    }

    public static void main(String[] args) {
        Alibaba test = new Alibaba("init");
    }
}

运行结果:

1 : j i=0 n=0
2 : 构造快 i=1 n=1
3 : t1 i=2 n=2
4 : j i=3 n=3
5 : 构造快 i=4 n=4
6 : t2 i=5 n=5
7 : i i=6 n=6
8 : 静态块 i=7 n=99
9 : j i=8 n=100
10 : 构造快 i=9 n=101
11 : init i=10 n=102

你可能感兴趣的:(【JVM】)