文章目录
Java 类加载机制
Java 类加载过程 生命周期
类加载过程
在Java中数据类型分为基本数据类型和引用数据类型。基本数据类型由虚拟机预先定义,引用数据类型则需要进行类的加载。
过程一:类的装载 Loading
过程一:类的装载 Loading
所谓装载,简而言之就是将Java类的字节码文件加载到机器内存中,并在内存中构建出Java类的原型——类模板对象。
装载完成的操作
装载阶段,简言之,查找并加载类的二进制数据,生成Class的实例。
在加载类时,Java虚拟机必须完成以下3件事情:
通过类的全名,获取类的二进制数据流。
解析类的二进制数据流为方法区内的数据结构(Java类模型)
创建java.lang.Class类的实例,表示该类型。作为方法区这个类的各种数据的访问入口
类模板对象
所谓类模板对象,其实就是Java类在JVM内存中的一个快照,JVM将从字节码文件中解析出的常量池、类字段、类方法等信息存储到类模板中,这样JVM在运行期便能通过类模板而获取Java类中的任意信息,能够对Java类的成员变量进行遍历,也能进行Java方法的调用。
反射的机制即基于这一基础。如果JVM没有将Java类的声明信息存储起来,则JVM在运行期也无法反射。
类模型的位置
加载的类在JVM中创建相应的类结构,类结构会存储在方法区(JDK1.8之前:永久代;JDK1.8及之后:元空间)。
二进制流的获取方式
对于类的二进制数据流,虚拟机可以通过多种途径产生或获得。(只要所读取的字节码符合JVM规范即可)
虚拟机可能通过文件系统读入一个class后缀的文件(最常见)
在获取到类的二进制信息后,Java虚拟机就会处理这些数据,并最终转为一个java.lang.Class的实例。
Class实例的位置
类将.class文件加载至元空间后,会在堆中创建一个Java.lang.Class对象,用来封装类位于方法区内的数据结构,该Class对象是在加载类的过程中创建的,每个类都对应有一个Class类型的对象。
数组类的加载
创建数组类的情况稍微有些特殊,因为数组类本身并不是由类加载器负责创建,而是由JVM在运行时根据需要而直接创建的,但数组的元素类型仍然需要依靠类加载器去创建。创建数组类(下述简称A)的过程:
1. 如果数组的元素类型是引用类型,那么就遵循定义的加载过程递归加载和创建数组A的元素类型;
2. JVM使用指定的元素类型和数组维度来创建新的数组类。
3. 如果数组的元素类型是引用类型,数组类的可访问性就由元素类型的可访问性决定。否则数组类的可访问性将被缺省定义为public。
过程二:链接 Linking
链接过程之验证阶段(Verification)
它的目的是保证加载的字节码是合法、合理并符合规范的。
链接过程之准备阶段(Preparation)
简言之,为类的静态变量分配内存,并将其初始化为默认值。
注意:Java并不支持boolean类型,对于boolean类型,内部实现是int,由于int的默认值是0,故对应的,boolean的默认值就是false。
注意:
- 这里不包含基本数据类型的字段用static final修饰的情况,因为final在编译的时候就会分配了,准备阶段会显式赋值。
- 注意这里不会为实例变量分配初始化,实例变量是会随着对象一起分配到Java堆中。
- 在这个阶段并不会像初始化阶段中那样会有初始化或者代码被执行。
链接过程之解析阶段(Resolution)
将类、接口、字段和方法的符号引用转为直接引用。
所谓解析就是将符号引用转为直接引用,也就是得到类、字段、方法在内存中的指针或者偏移量。因此,可以说,如果直接引用存在,那么可以肯定系统中存在该类、方法或者字段。但只存在符号引用,不能确定系统中一定存在该结构。
不过Java虚拟机规范并没有明确要求解析阶段一定要按照顺序执行。在HotSpot VM中,加载、验证、准备和初始化会按照顺序有条不紊地执行,但链接阶段中的解析操作往往会伴随着JVM在执行完初始化之后再执行。
过程三:初始化阶段(Initialization)
为类的静态变量赋予正确的初始值。(显式初始化)
初始化阶段的重要工作是执行类的初始化方法:<clinit>()
方法。
该方法仅能由Java编译器生成并由JVM调用,程序开发者无法自定义一个同名的方法,更无法直接在Java程序中调用该方法,虽然该方法也是由字节码指令所组成。
它是由类静态成员的赋值语句以及static语句块合并产生的。
<clinit>()
: 只有在给类的中的static的变量显式赋值或在静态代码块中赋值了。才会生成此方法。
<init>()
一定会出现在Class的method表中。
1) 在加载一个类之前,虚拟机总是会试图加载该类的父类,因此父类的总是在子类之前被调用。也就是说,父类的static块优先级高于子类。
2) Java编译器并不会为所有的类都产生 < clinit > ()初始化方法。哪些类在编译为字节码后,字节码文件中将不会包含 < clinit > ()方法?
一个类中并没有声明任何的类变量,也没有静态代码块时
一个类中声明类变量,但是没有明确使用类变量的初始化语句以及静态代码块来执行初始化操作时
一个类中包含static final修饰的基本数据类型的字段,这些类字段初始化语句采用编译时常量表达式
/**
* 哪些场景下,java编译器就不会生成<clinit>()方法
*/
public class InitializationTest1 {
//场景1:对于非静态的字段,不管是否进行了显式赋值,都不会生成<clinit>()方法
public int num = 1;
//场景2:静态的字段,没有显式的赋值,不会生成<clinit>()方法
public static int num1;
//场景3:比如对于声明为static final的基本数据类型的字段,不管是否进行了显式赋值,都不会生成<clinit>()方法
public static final int num2 = 1;
}
3)static final
static final 变量在编译时(在JVM类加载之前 即.java->.class时编译时常量折叠)已经有了常量值,因此不需要生成 clinit 方法来初始化它。clinit方法通常是在类的静态变量或者静态代码块需要初始化时才会被生成,而 static final 变量在编译时会被直接优化为常量,因此不需要通过 clinit 方法进行初始化。
/ **
* 总结:
* 使用static + final 修饰的成员变量,称为:全局常量。
* 什么时候在链接阶段的准备环节:给此全局常量附的值是字面量或常量。不涉及到方法或构造器的调用。
* 除此之外,都是在初始化环节赋值的。
*
*/
public class InitializationTest2 {
// public static int a = 1; //在初始化阶段赋值
// public static final int INT_CONSTANT = 10; //在链接阶段的准备环节赋值
//
// public static Integer INTEGER_CONSTANT1 = Integer.valueOf(100); //在初始化阶段赋值
// public static final Integer INTEGER_CONSTANT2 = Integer.valueOf(1000); //在初始化阶段赋值
//
public static final String s0 = "helloworld0"; //在链接阶段的准备环节赋值
public static final String s1 = new String("helloworld1"); //在初始化阶段赋值
//
public static String s2 = "helloworld2"; //在初始化阶段赋值
//
public static final int NUM1 = new Random().nextInt(10); //在初始化阶段赋值
static int a = 9;//在初始化阶段赋值
static final int b = a; //在初始化阶段赋值
}
4)<clinit>()
方法的调用,也就是类的初始化,虚拟机会在内部确保其多线程环境中的安全性。
5) 类初始化情况:主动使用 和 被动使用
主动使用的说明:
Class只有在必须要首次使用的时候才会被装载,Java虚拟机不会无条件地装载Class类型。Java虚拟机规定,一个类或接口在初次使用前,必须要进行初始化。这里指的“使用”,是指主动使用。
被动使用的情况
除了以上的情况属于主动使用,其他的情况均属于被动使用。被动使用不会引起类的初始化。
也就是说:并不是在代码中出现的类,就一定会被加载或者初始化。如果不符合主动使用的条件,类就不会初始化。
public class Test {
static int x, y, z;
static {
int x = 5;
x--;
}
static {
x--;
}
public static void main(String[] args) {
System.out.println("x=" + x);
z--;
System.out.println(z);//-1
method();
System.out.println("result:" + (z + y + ++z));//3
}
public static void method() {
y = z++ + ++z; //-1 + 1
System.out.println(y);//0
}
}
public class T {
public static int k = 0;
public static T t1 = new T("t1");
public static T t2 = new T("t2");
public static int i = print("i");
public static int n = 99;
public int j = print("j");
{
print("构造块");
}
static {
print("静态块");
}
public T(String str) {
System.out.println((++k) + ":" + str + " i=" + i + " n=" + n);
++n;
++i;
}
public static int print(String str) {
System.out.println((++k) + ":" + str + " i=" + i + " n=" + n);
++n;
return ++i;
}
public static void main(String[] args) {
}
}
/**
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
*/
static
1 k
2 t1 -> 初始化j -> 构造块 -> t1
3 t2同理
4 i
5 静态块
由上而下执行static
过程四:类的使用(Using)
任何一个类型在使用之前都必须经历过完整的加载、链接和初始化3个类加载步骤。
开发人员可以在程序中访问和调用它的静态类成员信息(比如:静态字段、静态方法),或者使用new关键字为其创建对象实例。
过程五:类的卸载(Unloading)
一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期。
拓展:
方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型。
HotSpot虚拟机对常量池的回收策略是很明确的,只要常量池中的常量没有被任何地方引用,就可以被回收。
判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于“不再被使用的类”的条件就比较苛刻了。需要同时满足下面三个条件:
该类所有的实例都已经被回收。也就是Java堆中不存在该类及其任何派生子类的实例。
加载该类的类加载器已经被回收。这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的。
该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
Java虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是**“被允许”**,而并不是和对象一样,没有引用了就必然会回收。