目录

目录

一、类加载机制

1、类加载?

1.1 什么是类加载机制?

首先,在代码被编译器编译后生成的二进制字节流(.class)文件;

然后,JVM把Class文件加载到内存,并进行验证、准备、解析、初始化;

最后,能够形成被JVM直接使用的Java类型的过程。

--这就是类加载机制

类加载器并不需要等到某个类被“首次主动使用”时才加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载。

如果预先加载的过程中遇到了.class文件缺失或者存在错误,类加载器不会马上报告错误;类加载器必须在程序【首次主动使用】该类时才报告错误(LinkageError错误)。

1.2 加载.class 文件的方式

从本地系统中直接加载

通过网络下载.class文件

从zip、jar等文件中加载

从专有数据库中提取.class文件

将Java远文件动态编译为.class文件

2、类加载流程图

类加载机制

二、类加载机制阶段详解

1、类的加载

类的加载是类加载机制过程的第一个阶段,该阶段主要完成三件任务:

①. 通过类的全限定名来获取类的二进制字节流。

②. 将字节流中所有代表的静态存储结构转化为【方法区】的运行时数据结构。

③. 在内存Java堆中生成一个代表这个类的Java.lang.Class对象,作为方法区中这个类的各种数据的访问入口。

2、连接

在经历类的加载过程后,生成了类的java.lang.Class对象,接着会进入连接阶段。连接阶段负责将类的二进制数据合并如JRE(Java运行时环境)中。类的连接大致分为三个阶段。

2.1 验证阶段

验证:确保被加载的类符合JVM规范和安全。

验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致会完成4个阶段的检验动作:

文件格式验证:

验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。

元数据验证:

对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。

字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。

符号引用验证:确保解析动作能正确执行。

验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

2.2 准备(重点!!)

准备阶段:为类的静态变量(static filed)在【方法区】分配内存,并附上默认初始值(0或者null值)。

静态变量在方法去分配内存

静态变量在分配内存后,附上初始值。

静态常量(static final filed)会在准备阶段直接将程序设定的值附上。

例如:

static final int a = 10;
// 该静态常量a 会在【准备阶段】直接将10赋值。
static int b = 11;
// 该静态变量b 在【准备阶段】只会赋值初始值0,等到了【初始化】阶段会将真正的11赋值给静态变量b。

2.3 解析

解析:把类中的符号引用转换为直接引用。

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

符号引用,就是一组符号来描述目标,可以是任何字面量。

直接引用,就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

3、初始化(重点!!)

初始化,为类的静态变量赋予正确的初始值。

初始化阶段是执行类构造器()方法。

3.1 在Java中堆类变量惊喜初始值设定有两种方式:

①声明类静态变量是制定初始值。

②使用静态代码块为类静态变量制定初始值。

3.2 JVM初始化步骤

① 如果这个类还没有被加载和连接,则程序先加载并连接该类。(其实就是执行上面的类加载、连接两步骤)

② 如果的直接父类还没有被初始化,则先初始化其直接父类。

③ 如果这个类中有初始化语句,则系统会一次执行这些初始化语句。

3.3 类初始化时机

类初始化时机,有且只有主动引用时才会触发类的初始化。

被动引用则不会触发类初始化。

类初始化时间后面单独详细说明。

4、使用

类的正常使用。

5、卸载

类的卸载需要根据【该类对象不再被引用+GC回收 】来判断何时被卸载。

①由Java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。因为一直被引用着。

②由用户自定义的类加载器加载的类是可以被卸载。

三、类的加载时机与初始化时机

(一)类的加载时机

当应用程序启动的时候,所有的类都会被一次性加载吗?

答案是否定的。不能,因为如果一次性加载,内存资源有限,可能会影响应用程序的正常运行。

类是什么时候i被加载的呢?

当一个类真正被加载的时机是在创建对象的时候,才会执行类加载。

例如:A a= new A();该类的加载,只有在创建对象的时候才加载类。

其中,最先加载拥有main方法的主线程所在的类。

(二)类的初始化时机(重要!!)

引用方式主要分为两种:主动引用和被动引用。

有且只有主动引用才会触发类初始化的过程。被动引用不会触发类初始化过程。

主动引用

有且只有主动引用才会触发类初始化的过程。触发主动引用的方式有以下五种:

创建类的实例。即通过new的方式,new一个对象。

例如:

A a = new A();

调用类的静态变量(非final修饰的常量) 和静态方法。

代码示例:

Test3.class代码如下

public class Test3 {
public static final int A = 10;// 静态常量, 不会触发初始化,该代码在连接准备阶段已赋值。
public static int B = 11;// 静态变量, 在被外界调用时,会主动触发类的初始化。
static{//静态代码块,如果类初始化,则一定会执行该代码块
System.out.println("test3..print~~~");
}
public void fun() {//实例方法,只有实例化后代码才能调用此方法
System.out.println("test3..fun~~~");
}
public static void fun2() {//静态方法,在被外界调用时,会主动触发类的初始化。
System.out.println("test3..static fun2...");
}
}
mian()方法测试代码:
public class TestStatic {
public static void main(String[] ags) {
// 申明类时,不会主动触发初始化流程.
Test3 test3;
// 类的静态常量,不会触发初始化,该代码在连接准备阶段已赋值。
System.out.println("test3 A "+Test3.A);
// 类的静态变量,在被外界调用时,会主动触发类的初始化。【注意,此时会初始化Test3】
System.out.println("test3 B " + Test3.B);
// 直接调用test3的静态方法fun(),也会主动触发类的初始化工作。
test3.fun();
}
}
输出内容:
test3 A 10
test3..print~~~
test3 B 11

错误:这里会发现,static静态代码块先执行,然后再执行static变量初始化。(@油炸小居崽 的指正)

正确:static静态代码块不会比static变量初始化优先执行,可以采用静态代码块中输出静态变量验证。

通过反射对类进行调用。

例如:Class.forName("com.jx.Test2");

Test3.class示例代码:

public class Test3 {
public static final int A = 10;// 静态常量, 不会触发初始化,该代码在连接准备阶段已赋值。
public static int B = 11;// 静态变量, 在被外界调用时,会主动触发类的初始化。
static{//静态代码块,如果类初始化,则一定会执行该代码块
System.out.println("test3..print~~~");
}
public void fun() {//实例方法,只有实例化后代码才能调用此方法
System.out.println("test3..fun~~~");
}
public static void fun2() {//静态方法,在被外界调用时,会主动触发类的初始化。
System.out.println("test3..static fun2...");
}
}

mian()方法类,测试代码:

public class TestStatic {
public static void main(String[] ags) {
try {
Class> aClass = Class.forName("com.jx.Test3");//调用此代码,则类会初始化。
System.out.println("aClass " + aClass);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
输出内容:
test1..print~~~
aClass class com.jx.Test1

初始化某个类的子类,则父类也会被初始化。

Test4.class示例代码:

public class Test4 extends Test3 { // 继承了Test3
public void fun4() {
System.out.println("test4...fun4...");
}
}

测试代码:

public class TestStatic {
public static void main(String[] ags) {
Test4 test4 = new Test4();
test4.fun4();
}
}
输出内容:
test3..print~~~
test4...fun4...

Java虚拟机启动时,指定的main方法所在的类,需要被提前初始化。

测试代码:

public class TestStatic {
static {
System.out.println("test static ...");
}
public static void main(String[] ags) {
Test4 test4 = new Test4();
test4.fun4();
}
}
输出内容:
test static ...
test3..print~~~
test4...fun4...

被动引用

被动引用,不会发生类的初始化过程。

被动引用又分为三种方式:

当访问一个类的静态变量时(该静态变量是父类所持有),只有真正声明这个变量的类才会初始化。

子类调用父类的静态变量,只有父类初始化,而子类不会进行初始化。

代码示例:

public class SuperClass { // 父类
public static int A = 7; // 父类静态变量
static { // 静态代码块在初始化时执行
System.out.println("super class static ...");
}
}
public class SubClass extends SuperClass { // 子类继承父类
static { // 静态代码块在初始化时执行
System.out.println("sub class static ...");
}
}
public class TestStatic2 {
public static void main(String[] args) {
System.out.println("A =" + SubClass.A); // 调用子类继承的父类静态变量
}
}
输出内容:
super class static ...
A =7

通过数据定义引用类,不会触发类的初始化。

因为是数据进行new,而对应的应用类没有被new,所以该类没有触发任何主动引用。

代码示例

public class TestStatic3 {
public static void main(String[] args) {
SuperClass[] superClasses = new SuperClass[3];
System.out.println(superClasses);
}
}
输出内容:
[Lcom.jx.SuperClass;@193b845

final 常量不会触发类的初始化,因为编译阶段就存储在常量池中。

//常量类
public class ConstClass {
static{
System.out.println("常量类初始化!");
}
public static final String HELLOWORLD = "hello world!";
}
//主类、测试类
public class NotInit {
public static void main(String[] args){
System.out.println(ConstClass.HELLOWORLD);
}
}

四、类生命周期与JVM生命周期

(一) 类的生命周期

当一个类被加载、连接、初始化后,它的生命周期就开始了。

当这个类的class对象不再被引用,即类不可触及时,Class对象就会结束生命周期。这个类在方法区的数据也会被卸载,从而结束这个类的生命周期。

所以,一个类结束生命周期,取决于代表它的Class对象何时结束生命周期。

(二)JVM生命周期

Java虚拟机结束生命周期的情况:

1.执行了System.exit()方法.

程序正常执行结束。

程序在执行过程中遇到了异常或错误而并未处理,导致异常终止。

由于依赖的操作系统出现错误,而导致Java虚拟机进程终止。

参考