类加载机制概述:
虚拟机把描述类的数据从Classe文件加载到内存,并对数据进行检验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
类加载的时机:
- 类加载的生命周期:加载、连接(验证、准备、解析)、初始化、使用和卸载
- 加载时机:Java虚拟机规范中并没有进行强制约束,由虚拟机自由把握。
- 连接时机:加载之后就开始连接,加载结束连接才结束。
- 初始化时机:Java虚拟机规范中严格规定有且只有5种情况立即对类进行“初始化”:
1).遇到new、getstatic、puttstatic或invoketstatic这4条字节码指令时,如果类没有进行初始化,则需要先触发其初始化。生成这4条指令的常用场景:使用new关键字实例化对象时、读取或设置类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)时、调用类静态方法的时候。
2).使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化。
3).初始化一个类的时候,如果发现其父类没有进行初始化,则需要先触发父类的初始化。
4).虚拟机启动时,需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个类。
加载:
加载阶段主要完成3件事:
- 通过一个类的完全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类各种数据的访问入口。
验证:
这一阶段主要是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并不会危害虚拟机自身安全。该阶段大致会完成4个阶段的检验动作:文件格式验证、元数据验证、字节码验证、符号引用验证。
准备:
该阶段正式为类变量分配内存并设置类变量(static修饰的变量)的初始值(默认值,比如int类型为0,Boolean为false… …),但如果被final修饰的类变量,初始值即为常量值,比如:
public static final int a =123; // 准备阶段会将a设置为123
该阶段不为实例变量分配内存,实例变量会在对象实例化时随对象一起分配到Java堆中。
解析:
该阶段是虚拟机将常量池的符号引用替换为直接引用的过程。
初始化:
该阶段是执行类构造器初始化方法< clinit>()的过程。
1).< clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{})中的语句合并产生的;编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,在前面的静态语句块可以赋值,但不能访问。
public class Test {
static {
i = 0; //给变量赋值可以正常编译通过
System.out.print(i); //编译器会提示“非法向前引用”
}
static int i = 1;
}
//静态语句块可以给块后定义的静态变量赋值 i=0;但不能引用,即第4行会提示错误
2).< clinit>()方法与类的构造函数(或者说实例构造器< init>方法)不同,虚拟机会保证在子类的初始化< clinit>()方法执行之前,父类的初始化方法已经执行完毕。
3).由于父类的< clinit>()方法先执行,意味着父类中定义的静态语句块要优先于子类的变量赋值操作
static class Parent {
public static int A = 1;
static {
A = 2;
}
}
static class Sub extends Parent {
public static int B = A;
}
public static void main(String[] args) {
System.out.println(Sub.B);
}
//父类中定义的静态语句块先执行,所以结果为2.
4).< clinit>()方法对于类或接口来说并不是必须的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成< clinit>()方法。
5).接口中不能使用静态语句块,但仍然有变量初始化的操作。
6).虚拟机会保证一个类的< clinit>()方法在多线程环境中被正确的加锁、同步;如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的< clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行类初始化方法完毕。
举个栗子:
父类:
package Test_ClassLoard;
public class Person {
public static int a = 10;
public String b = "aaa";
public Person() {System.out.println("--父类构造方法--");}
static {
System.out.println("--父类静态属性a:"+a);
System.out.println("--父类静态代码块--");
}
{
System.out.println("--父类成员变量b:"+b);
System.out.println("--父类非静态代码块--");
}
private static void staticMethod(){System.out.println("--父类静态方法--");}
private void noStaticMethod(){ System.out.println("--父类非静态方法--");}
}
子类:
package Test_ClassLoard;
public class Children extends Person{
public static final int c = 100;
private String d = "bbb";
public Children() { System.out.println("--子类构造方法--");}
static {
System.out.println("--子类静态属性c:"+c);
System.out.println("--子类静态代码块--");
}
{
System.out.println("--子类成员变量d:"+d);
System.out.println("--子类非静态代码块--");
}
public static void staticMethod(){System.out.println("--子类静态方法--");}
private void noStaticMethod(){ System.out.println("--子类非静态方法--");}
}
入口类:
package Test_ClassLoard;
public class App {
public static void main(String[] args) {
Children children = new Children();
System.out.println("----------");
new Children();
// System.out.println(Children.c);//被final修饰,类加载时不会初始化
// System.out.println(Children.a);//子类调用父类类变量,子类加载时不会初始化
}
}
运行结果:
过程:
1). 编译好 App.java 后得到 App.class 后,执行 App.class,系统会启动一个 JVM 进程,从 classpath 路径中找到一个名为 App.class 的二进制文件,将 App 的类信息加载到运行时数据区的方法区内(类加载)。
2). JVM 找到 App 的主程序入口,执行main方法。
3). 自上而下执行,遇到new指令,发现方法区的运行时常量池没有Children类的符号引用(参考对象的创建),则先加载Children类(可见类加载是懒加载);由于继承父类,所以会先加载父类Person(加载、验证、准备、解析),然后对Person进行初始化(执行类构造器初始化方法< clinit>() –自上而下执行类变量和静态语句块)
4).Person类加载完后同理加载子类Children,输出:
–子类静态属性c:100
–子类静态代码块–
5).加载类后,虚拟机为父类Person分配内存,然后调用< init >()构造函数初始化 Person实例。同理,再给子类Children分配内存和< init >()初始化。所以输出:
–父类成员变量b:aaa
–父类非静态代码块–
–父类构造方法–
–子类成员变量d:bbb
–子类非静态代码块–
–子类构造方法–
6).至此, Children children = new Children();新建对象完毕;当再次 new Children()时,之前父类和子类都已经加载过了,所以只输出实例化信息。
7). 小结: 父类静态-子类静态;
父类非静态-父类构造-子类非静态-子类构造;
均自上而下执行;
参考:
《深入理解Java虚拟机:JVM高级特性与最佳实践(第二版)》
大部分内容均来自这本书,下图为思维导图。
互相交流,互相学习,有误指正。