Java类加载机制浅析

翻译自jvm-works-jvm-architecture,并参考JVM 的类初始化机制java

JVM(Java Virtual Machine)做为一个运行时引擎去运行Java应用。JVM 是实际调用main方法的对象。JVM是JRE(Java Runtime Enviroment)的一部分。程序员

Java应用被称为WORA(Write Once Run Anywhere).这意味着程序员能够在一个系统上编写Java程序而且预期能够不须要任何修改就运行在Java可以运行的系统中。这之因此可行就是由于JVM。bootstrap

类加载子系统

它主要有如下几个步骤:segmentfault

  • 加载
  • 连接
  • 初始化

加载

类加载器(class loader)读取.class文件生成相应的二进制数据,并存储在_方法区_中。对于每个.class文件,JVM存储了一下几个信息在方法区:数据结构

  • 加载的类及其父类的全限定名称(Fully qualified name)。(全限定名称:包含这个类所来自的包名,能够用类的getName()方法得到)
  • .class文件是否与类/接口/枚举相关
  • 修饰符,变量和方法信息等等。

在加载.class文件后,JVM在_堆内存_中建立了一个类型为Class的一个对象去表示这个文件。请注意,这个对象的类型是`java.lang`包中预约义的Class类型。这个Class对象能够被用于获取类级别的信息,如类名,父类名,方法和变量信息等等。咱们可使用Object类的_getClass()_ 去获取对象的引用。dom

总结为:jvm

  • 根据类全名=》生成二进制字节码
  • 将字节码解析成方法区对应的数据结构储存在方法区
  • 堆内存中生成Class类的实例
// A Java program to demonstrate working of a Class type 
// object created by JVM to represent .class file in 
// memory. 
import java.lang.reflect.Field; 
import java.lang.reflect.Method; 

// Java code to demonstrate use of Class object 
// created by JVM 
public class Test { 
	public static void main(String[] args) { 
		Student s1 = new Student(); 

		// Getting hold of Class object created 
		// by JVM. 
		Class c1 = s1.getClass(); 

		// Printing type of object using c1. 
		System.out.println(c1.getName()); 

		// getting all methods in an array 
		Method m[] = c1.getDeclaredMethods(); 
		for (Method method : m) 
			System.out.println(method.getName()); 

		// getting all fields in an array 
		Field f[] = c1.getDeclaredFields(); 
		for (Field field : f) 
			System.out.println(field.getName()); 
	} 
} 

// A sample class whose information is fetched above using 
// its Class object. 
class Student { 
	private String name; 
	private int roll_No; 

	public String getName() { return name; } 
	public void setName(String name) { this.name = name; } 
	public int getRoll_no() { return roll_No; } 
	public void setRoll_no(int roll_no) { 
		this.roll_No = roll_no; 
	} 
} 

复制代码

输出:函数

Student
getName
setName
getRoll_no
setRoll_no
name
roll_No
复制代码

注意:每个加载的`.class`文件都只有一个Class对象被建立(即单例)性能

一般来讲,有三种类加载器(Class loader):fetch

  • **Bootstrap class loader:**每个JVM的实现必须有一个bootstrap class loader, 它可以去加载可信的类。它加载存在于JAVA_HOME/jre/lib目录下的java核心API类。这也是bootstrap的路径。它是由原生语言(native languages)如C/C++实现的。
  • **扩展类加载器(EXtension class loader):**它是bootstrap class loader的子类。它加载存在于额外目录(JAVA_HOME/jre/lib/ext)或者其余由java.ext.dirs所指定的系统属性。它是基于java由 sun.misc.Launcher$ExtClassLoader 类实现的。
  • **系统/应用类加载器(System/Application class loader):**它是额外类加载器的子类。它负责从应用的类路径下加载类。它在内部使用映射到java.class.path的环境变量。它也是基于java由 sun.misc.Launcher$ExtClassLoader 类实现的。

JVM 中除了最顶层的Boostrap ClassLoader是用 C/C++ 实现外,其他类加载器均由 Java 实现,咱们能够用getClassLoader方法来获取当前类的类加载器:

// Java code to demonstrate Class Loader subsystem 
public class Test { 
	public static void main(String[] args) { 
		// String class is loaded by bootstrap loader, and 
		// bootstrap loader is not Java object, hence null 
		System.out.println(String.class.getClassLoader()); 

		// Test class is loaded by Application loader 
		System.out.println(Test.class.getClassLoader()); 
	} 
}	 

复制代码

Output:

null
sun.misc.Launcher$AppClassLoader@73d16e93
复制代码
java -verbose:class Test
[Opened C:\Program Files\Java\jre1.8.0_231\lib\rt.jar]
[Loaded java.lang.Object from C:\Program Files\Java\jre1.8.0_231\lib\rt.jar]
[Loaded java.io.Serializable from C:\Program Files\Java\jre1.8.0_231\lib\rt.jar]
[Loaded java.lang.Comparable from C:\Program Files\Java\jre1.8.0_231\lib\rt.jar]
[Loaded java.lang.CharSequence from C:\Program Files\Java\jre1.8.0_231\lib\rt.jar]
[Loaded java.lang.String from C:\Program Files\Java\jre1.8.0_231\lib\rt.jar]
[Loaded java.lang.reflect.AnnotatedElement from C:\Program Files\Java\jre1.8.0_231\lib\rt.jar]
[Loaded java.lang.reflect.GenericDeclaration from C:\Program Files\Java\jre1.8.0_231\lib\rt.jar]
[Loaded java.lang.reflect.Type from C:\Program Files\Java\jre1.8.0_231\lib\rt.jar]
...
[Loaded java.lang.Void from C:\Program Files\Java\jre1.8.0_231\lib\rt.jar]
null
[Loaded Test from file:/D:/chenyue/Learn/jvm/target/classes/]
sun.misc.Launcher$AppClassLoader@73d16e93
[Loaded java.lang.Shutdown from C:\Program Files\Java\jre1.8.0_231\lib\rt.jar]
[Loaded java.lang.Shutdown$Lock from C:\Program Files\Java\jre1.8.0_231\lib\rt.jar]


复制代码

注意:JVM遵循委托层级原则去加载类。系统类加载器将加载请求委托给扩展类加载器,扩展类加载器将请求委托给引导类加载器(boot-strap class loader)。若是这个类被发如今引导路径中,类将会被加载,除非请求被再次转发给扩展类加载器,而后再转发到系统加载器上。最后若是系统加载器加载类失败,那咱们将会获得一个运行时异常 java.lang.ClassNotFoundException.

连接

执行验证,准备和(可选)解决方案。

  • 验证(Verification):它保证`.class`文件的正确性即检查文件是否被有效的编译器正确格式化和生成。若是验证失败,咱们会获得一个运行时异常_java.lang.VerigyError。_主要包含但不限于:

    • bytecode 的完整性(integrity)
    • 检查final类没有被继承,final方法没有被覆盖
    • 确保没有不兼容的方法签名
  • 准备(Preparation):JVM为类变量分配内存并初始化内存赋予默认值。

    在这个阶段,JVM 也可能会为有助于提升程序性能的数据结构分配内存,常见的一个称为method table的数据结构,它包含了指向全部类方法(也包括也从父类继承的方法)的指针,这样再调用父类方法时就不用再去搜索了。

  • 解决(Resolution): 确认类、接口、属性和方法在类run-time constant pool的位置,用以将_符号引用(Symbolic References)_变为直接引用。这经过搜索方法区来定位引用的实体来实现。 *

初始化

在这个阶段,没有的静态变量和静态代码块(若是存在)都被赋予定义在代码中的数值。初始化的执行顺序在一个类中是自顶向下的,在类的层次关系中是从父类到子类。

**第一次 主动调用**某类的最后一步是Initialization,这个过程会去按照代码书写顺序进行初始化,这个阶段会去真正执行代码,注意包括:代码块(static与非static)、构造函数、变量显式赋值。若是一个类有父类,会先去执行父类的initialization阶段,而后在执行本身的。

上面这段话有两个关键词:第一次主动调用第一次是说只在第一次时才会有初始化过程,之后就不须要了,能够理解为每一个类有且仅有一次初始化的机会。那么什么是主动调用呢?
JVM 规定了如下六种状况为主动调用,其他的皆为被动调用

  1. 一个类的实例被建立(new操做、反射、cloning,反序列化)
  2. 调用类的static方法
  3. 使用或对类/接口的static属性进行赋值时(这不包括final的与在编译期肯定的常量表达式)
  4. 当调用 API 中的某些反射方法时
  5. 子类被初始化
  6. 被设定为 JVM 启动时的启动类(具备main方法的类)

本文后面会给出一个示例用于说明主动调用被动调用区别。

在这个阶段,执行代码的顺序遵循如下两个原则:

  1. 有static先初始化static,而后是非static的
  2. 显式初始化,构造块初始化,最后调用构造函数进行初始化

示例

属性在不一样时期的赋值

class Singleton {

    private static Singleton mInstance = new Singleton();// 位置1
    public static int counter1;
    public static int counter2 = 0;

// private static Singleton mInstance = new Singleton();// 位置2

    private Singleton() {
        counter1++;
        counter2++;
    }

    public static Singleton getInstantce() {
        return mInstance;
    }
}

public class InitDemo {

    public static void main(String[] args) {

        Singleton singleton = Singleton.getInstantce();
        System.out.println("counter1: " + singleton.counter1);
        System.out.println("counter2: " + singleton.counter2);
    }
}
复制代码

mInstance在位置1时,打印出

counter1: 1
counter2: 0
复制代码

mInstance在位置2时,打印出

counter1: 1
counter2: 1
复制代码

Singleton中的三个属性在Preparation阶段会根据类型赋予默认值,在Initialization阶段会根据显示赋值的表达式再次进行赋值(按顺序自上而下执行)。根据这两点,就不难理解上面的结果了。

主动调用 vs. 被动调用

class NewParent {

    static int hoursOfSleep = (int) (Math.random() * 3.0);

    static {
        System.out.println("NewParent was initialized.");
    }
}

class NewbornBaby extends NewParent {

    static int hoursOfCrying = 6 + (int) (Math.random() * 2.0);

    static {
        System.out.println("NewbornBaby was initialized.");
    }
}

public class ActiveUsageDemo {

    // Invoking main() is an active use of ActiveUsageDemo
    public static void main(String[] args) {

        // Using hoursOfSleep is an active use of NewParent,
        // but a passive use of NewbornBaby
        System.out.println(NewbornBaby.hoursOfSleep);
    }

    static {
        System.out.println("ActiveUsageDemo was initialized.");
    }
}
复制代码

上面的程序最终输出:

ActiveUsageDemo was initialized.
NewParent was initialized.
1
复制代码

之因此没有输出NewbornBaby was initialized.是由于没有主动去调用NewbornBaby,若是把打印的内容改成NewbornBaby.hoursOfCrying 那么这时就是主动调用NewbornBaby了,相应的语句也会打印出来。

首次主动调用才会初始化

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");
    {
        print("构造块");
    }
    static {
        print("静态块");
    }

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

    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 t = new Alibaba("init");
    }
}
复制代码

上面这个例子是阿里巴巴在14年的校招附加题,我当时看到这个题,就以为与阿里无缘了。囧

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
复制代码

上面是程序的输出结果,下面我来一行行分析之。

  1. 因为Alibaba是 JVM 的启动类,属于主动调用,因此会依此进行 loading、linking、initialization 三个过程。

  2. 通过 loading与 linking 阶段后,全部的属性都有了默认值,而后进入最后的 initialization 阶段。

  3. 在 initialization 阶段,先对 static 属性赋值,而后在非 static 的。k 第一个显式赋值为 0 。

  4. 接下来是t1属性,因为这时Alibaba这个类已经处于 initialization 阶段,static 变量无需再次初始化了,因此忽略 static 属性的赋值,只对非 static 的属性进行赋值,全部有了开始的:

    1:j   i=0    n=0
        2:构造块   i=1    n=1
        3:t1   i=2    n=2
    复制代码
  5. 接着对t2进行赋值,过程与t1相同

    4:j   i=3    n=3
        5:构造块   i=4    n=4
        6:t2   i=5    n=5
    复制代码
  6. 以后到了 static 的 in

    7:i   i=6    n=6
    复制代码
  7. 到如今为止,全部的static的成员变量已经赋值完成,接下来就到了 static 代码块

    8:静态块   i=7    n=99
    复制代码
  8. 至此,全部的 static 部分赋值完毕,接下来是非 static 的 j

    9:j   i=8    n=100
    复制代码
  9. 全部属性都赋值完毕,最后是构造块与构造函数

    10:构造块   i=9    n=101
        11:init   i=10    n=102
    复制代码

通过上面这9步,Alibaba这个类的初始化过程就算完成了。这里面比较容易出错的是第3步,认为会再次初始化 static 变量或代码块。而其实是不必,不然会出现屡次初始化的状况。

但愿你们能多思考思考这个例子的结果,加深这三个过程的理解。

小结

a. 加载类

  1. 为父类静态属性分配内存并赋值 / 执行父类静态代码段 (按代码顺序)
  2. 为子类静态属性分配内存并赋值 / 执行子类静态代码段 (按代码顺序)

b. 建立对象

  1. 为父类实例属性分配内存并赋值 / 执行父类非静态代码段 (按代码顺序)
  2. 执行父类构造器
  3. 为子类实例属性分配内存并赋值 / 执行子类非静态代码段 (按代码顺序)
  4. 执行子类构造器

参考

相关文章
相关标签/搜索