1. JVM和类

当一个Java程序运行后,无论该程序代码量有多少,无论它包含多少个线程,他们都处于同一个JVM内存里面,并且同一个JVM的所有线程、变量都处于同一个进程中,共用同一块内存等资源。同一个Java程序运行两次会被放入两个JVM内存中,二者毫无关系。java程序结束时,JVM进程也终止了。

2. 类的加载机制

当程序要使用的某个类还没有放入内存的时候,JVM会通过加载、验证、解析、准备、初始化等步骤,使其加载到内存中,并且还会为他创建一个java.lang.Class对象。

这里需要指出,万物皆对象,包括class,类本身也是个对象,他是java.lang.Class的对象。当我们定义了一个类的时候,我们定义了某个对象的抽象,或者说某个对象的描述。但同时,该类也是java.lang.Class的实例化对象。就好比我们通过某种概念去描述某个对象,此概念是抽象的,但是这个概念也对象,他本身也是一种事物。

2.1 加载阶段

类的加载就是通过指定类的全限定类名,获取此类的二进制字节流(由二进制字节码转化而来),然后将此二进制字节流放入内存中,然后会为这个类在JVM的方法区创建一个对应的Class对象,这个 Class 对象就是这个类各种数据的访问入口。

其实加载阶段用一句话来说就是:把代码数据加载到内存中。完成类加载的是JVM提供的系统类加载器,你也可以通过继承ClassLoader基类来创建自己的类加载器。加载的来源主要包括两个方面:

  • 从本地加载class文件
  • 从Jar包加载class文件

2.2 验证阶段

当JVM加载完class文件的字节码并在方法区创建了一个Class对象之后,JVM并会对该字节码流进行校验。校验主要分为下面两个方面:

  • JVM规范的校验:主要是对字节流进行文件格式的校验,判断其是否符合JVM规范。
  • 代码逻辑的校验:主要是检查你代码的逻辑写的有没有问题,比如某个方法要求传int型数据,但是传了个String。比如说代码中引用了一个名为 Apple 的类,但是你实际上却没有定义 Apple 类。

2.3 准备阶段

该阶段JVM会为变量分配内存,并初始化其值为默认值。需注意两个概念:内存分配的对象、初始化的类型

  • 内存分配对象:Java中的变量分为两种:类变量(被 static 修饰的变量)、类成员变量(其余变量)。在准备阶段,JVM 只会为「类变量」分配内存,而不会为「类成员变量」分配内存。「类成员变量」的内存分配需要等到初始化阶段才开始。

例如下面的代码在准备阶段,只会为 factor 属性分配内存,而不会为 website 属性分配内存。

public static int factor = 3;
public String website = "www.eastnotes.com";
  • 初始化值:在准备阶段,变量的值是某种类型的默认值,而不是你指定的值。比如int类型的默认值是0。

如果一个变量不是常量,例如下面的代码在准备阶段之后,sector 的值将是 0,而不是 3。

public static int sector = 3;

如果一个变量是常量(被 static final 修饰)的话,那么在准备阶段,属性便会被赋予用户希望的值。例如下面的代码在准备阶段之后,number 的值将是 3,而不是 0。

public static final int number = 3;

2.4 解析阶段

当通过准备阶段之后,JVM 针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类引用进行解析。这个阶段的主要任务是将其在常量池中的符号引用替换成直接其在内存中的直接引用。

2.5 初始化阶段

到了初始化阶段,用户定义的 Java 程序代码才真正开始执行。在这个阶段,JVM 会根据语句执行顺序对类对象进行初始化,也就是真正为变量进行赋值。

下面是几种数据类型的默认值:

下面几种情况会触发类的初始化:

  • 创建类实例的时候,包括使用new创建实例、通过反射创建实例、通过反序列化创建实例。
  • 调用某个类的类方法(被static修饰的方法)、类变量(被static修饰的变量)
  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  • 通过反射来强制创建某个类或者接口对应的java.lang.Class对象,如:Class.forName("Person")
  • 当虚拟机启动时,会先初始化包含main()方法的那个类。
  • 当使用 JDK1.7 动态语言支持时,如果一个 java.lang.invoke.MethodHandle实例最后的解析结果 REF_getstatic,REF_putstatic,REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。

3. 类、对象初始化方法

在Java代码编译成字节码之后,是没有构造方法的概念的,只有类初始化方法和对象初始化方法这两个方法。

  • 类初始化方法。编译器会按照其出现顺序,收集类变量(被static修饰的变量)的赋值语句、静态代码块,最终组成类初始化方法。类初始化方法一般在类初始化的时候执行,也就是没有被new的时候。

类初始化方法的代码示例:

static
{
    System.out.println("书的静态代码块");
}
static int amount = 112;
  • 对象初始化方法。编译器会按照其出现顺序,收集成员变量(没有被static修饰的变量)的赋值语句、普通代码块,最后收集构造函数的代码,最终组成对象初始化方法。对象初始化方法一般在实例化类对象的时候执行。

对象初始化方法的代码示例:

{
    System.out.println("书的普通代码块");
}
int price = 110;
System.out.println("书的构造方法");

4. 类的执行顺序

  • 确定类变量的初始值:没有被final修饰则赋给零值,被final修饰的直接赋予=后面的值。
  • 初始化入口方法:初始化 main 方法所在的整个类。
  • 初始化类构造器:JVM 会按顺序收集类变量的赋值语句、静态代码块,最终组成类构造器由 JVM 执行。
  • 初始化对象构造器:JVM 会按照收集成员变量的赋值语句、普通代码块,最后收集构造方法,将它们组成对象构造器,最终由 JVM 执行。

如果再初始化main方法的时候遇到了其他类的初始化,则先初始化其他方法,如果遇到了没有被初始化的父类方法,则先初始化父类方法。如果在类构造器进行初始化的时候遇到了对象构造器,则先执行对象初始化方法,如:

5. 举个例子

public class Book {
    public static void main(String[] args)
    {
        staticFunction();
    }

    static Book book = new Book();

    static
    {
        System.out.println("书的静态代码块");
    }

    {
        System.out.println("书的普通代码块");
    }

    Book()
    {
        System.out.println("书的构造方法");
        System.out.println("price=" + price +",amount=" + amount);
    }

    public static void staticFunction(){
        System.out.println("书的静态方法");
    }

    int price = 110;
    static int amount = 112;
}

程序的运行结果如下:

书的普通代码块
书的构造方法
price=110,amount=0
书的静态代码块
书的静态方法
  • 该类含有main方法,因此首先初始化Book这个类
  • 首先执行这个类的类初始化方法,也就是被static修饰的语句
static Book book = new Book();
static
{
    System.out.println("书的静态代码块");
}
static int amount = 112;
  • 执行:static Book book = new Book();时遇到了对象实例化,因此JVM执行对象构造器,搜集的普通代码块、构造方法代码如下:
{
    System.out.println("书的普通代码块");
}

int price = 110;

Book()
{
    System.out.println("书的构造方法");
    System.out.println("price=" + price +", amount=" + amount);
}
  • 上面的对象构造器执行完之后,继续执行类构造器:System.out.println("书的静态代码块");
  • 等所有的构造器执行完之后,执行main方法的内容。

6. 再举个例子

public class Book {
    public static void main(String[] args)
    {
        System.out.println("Hello ShuYi.");
    }

    Book()
    {
        System.out.println("书的构造方法");
        System.out.println("price=" + price +",amount=" + amount);
    }

    {
        System.out.println("书的普通代码块");
    }

    int price = 110;

    static
    {
        System.out.println("书的静态代码块");
    }

    static int amount = 112;
}

运行结果是:

书的静态代码块
Hello ShuYi.

但是如果在Main方法中实例化一下该对象,则结果又变了:

public class Book {
    public static void main(String[] args)
    {
        new Book();
        System.out.println("Hello ShuYi.");
    }

    Book()
    {
        System.out.println("书的构造方法");
        System.out.println("price=" + price +",amount=" + amount);
    }

    {
        System.out.println("书的普通代码块");
    }

    int price = 110;

    static
    {
        System.out.println("书的静态代码块");
    }

    static int amount = 112;
}

输出结果为:

书的静态代码块
书的普通代码块
书的构造方法
price=110,amount=112
Hello ShuYi.

参考资料

https://www.cnblogs.com/chanshuyi/p/the_java_class_load_mechamism.html