java加载机制_Java类加载机制浅析

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

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

e5bbf383a7113f6fa9bc6ae6e0241a54.png

类加载子系统

它主要有以下几个步骤:

加载(Loading)

链接(Linking)

初始化(Initialising)

4e1d1408879aefeac03482d648542e6e.png

加载

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

加载的类及其父类的全限定名称(Fully qualified name)。(全限定名称:包含这个类所来自的包名,可以用类的getName()方法获得)

.class文件是否与类/接口/枚举相关

修饰符,变量和方法信息等等。

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

总结:

根据类全名 -> 生成二进制字节码

将字节码解析成方法区对应的数据结构储存在方法区

在堆内存中生成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):

引导类加载器(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 类实现的。

18238cce6847704174d1b03ae4b65f0f.png

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遵循委托层级原则去加载类。系统类加载器将加载请求委托给扩展类加载器,扩展类加载器将请求委托给引导类加载器(Bootstrap class loader)。如果这个类被发现在引导路径中,类将会被加载,除非请求被再次转发给扩展类加载器,然后再转发到系统加载器上。最后如果系统加载器加载类失败,那我们将会得到一个运行时异常 java.lang.ClassNotFoundException.

860dcf739341083b85abe5960239d5fa.png

链接

执行验证,准备和(可选)优化。

验证(Verification)

它保证.class文件的正确性即检查文件是否被有效的编译器正确格式化和生成。如果验证失败,我们会得到一个运行时异常java.lang.VerigyError。主要包含但不限于:

检查字节码的完整性(integrity)。

检查final类没有被继承,final方法没有被覆盖。

确保没有不兼容的方法签名。

准备(Preparation)

JVM为类变量分配内存并初始化内存赋予默认值。

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

解析(Resolution)

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

327324f8080cf7e79f63fc4318fe32f5.png

初始化

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

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

上面这段话有两个关键词:第一次与主动调用。

第一次:是说只在第一次时才会有初始化过程,以后就不需要了,可以理解为每个类有且仅有一次初始化的机会。

主动调用:JVM 规定了以下六种情况为主动调用,其余的皆为被动调用:

一个类的实例被创建(new操作、反射、cloning、反序列化)

调用类的static方法

使用或对类/接口的static属性进行赋值时(这不包括final的与在编译期确定的常量表达式)

当调用 API 中的某些反射方法时

子类被初始化

被设定为 JVM 启动时的启动类(具有main方法的类)

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

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

有static先初始化static,然后是非static的

显式初始化,构造块初始化,最后调用构造函数进行初始化

JVM内存

方法区(Method area)

每个JVM唯一,共享资源区。

所有层级的类信息如类名,直接父类的类名称,方法名和变量信息(包括静态变量)等等都存储在方法区中。

堆区(Heap area):

每个JVM唯一,共享资源区。

所有类的实例对象的信息存储在堆中。

栈区(Stack area):

每一个线程持有一个栈,不是共享资源。

对于每一个线程,JVM创造了一个运行时栈。栈的每个区块被称为activation record/stack frame(活动记录/栈帧)。

栈中存储数据类型:

方法调用(methods calls)。

方法中的所有本地变量。

在一个线程终止过后,运行时栈将会被JVM销毁。

程序计数器(PC Registers):存储当前线程执行指令的地址(即记录程序运行到哪了)。显然每个线程都有其独立的程序计数器。

原生方法栈(Native method stacks):对于每一个线程,原生方法栈会被分别创建。它存储了原生方法的信息。

a95042ffdf3a300ab0e2a901895b8647.png

执行引擎

执行引擎执行.class文件(字节码)。它按行读取字节码,使用数据和信息放置在不同内存区域中并且执行指令。它可以被分为三部分:

解释器(Interpreter):它按行解释字节码并且执行。它的劣势就在于当一个方法被调用多次,每次都要花费时间去解释其字节码。

即时编译器(Just-In-Time Compiler JIT):它是用于提高解释器效率的。它编译了整个字节码并将其变成了原生的代码,使得无论什么时候,解释器看到重复的方法调用,JIT能够提供直接的原生代码使得无需重复解释,因此效率得到了提升。

垃圾回收器:销毁了未被引用的对象。

Java原生接口(Java Native Interface JNI):

它是一个用于与原生方法库进行交互的接口,提供原生库(C,C++)的执行。它使得JVM能够调用C/C++ 库,并且可以被特定硬件的C/C++库所调用。

原生方法库:

它是由被执行引擎所需要的原生库(C,C++)的集合。

示例

属性在不同时期的赋值

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

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

由于Alibaba是 JVM 的启动类,属于主动调用,所以会依此进行 loading、linking、initialization 三个过程。

经过 loading与 linking 阶段后,所有的属性都有了默认值,然后进入最后的 initialization 阶段。

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

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

1:j i=0 n=0

2:构造块 i=1 n=1

3:t1 i=2 n=2

接着对t2进行赋值,过程与t1相同

4:j i=3 n=3

5:构造块 i=4 n=4

6:t2 i=5 n=5

之后到了 static 的 i 与 n:

7:i i=6 n=6

到现在为止,所有的static的成员变量已经赋值完成,接下来就到了 static 代码块

8:静态块 i=7 n=99

至此,所有的 static 部分赋值完毕,接下来是非 static 的 j

9:j i=8 n=100

所有属性都赋值完毕,最后是构造块与构造函数

10:构造块 i=9 n=101

11:init i=10 n=102

经过上面这9步,Alibaba这个类的初始化过程就算完成了。这里面比较容易出错的是第3步,认为会再次初始化 static 变量或代码块。而实际上是没必要,否则会出现多次初始化的情况。

希望大家能多思考思考这个例子的结果,加深这三个过程的理解。

小结

a. 加载类

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

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

b. 创建对象

为父类实例属性分配内存并赋值 / 执行父类非静态代码段 (按代码顺序)

执行父类构造器

为子类实例属性分配内存并赋值 / 执行子类非静态代码段 (按代码顺序)

执行子类构造器

参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值