目录
目的
感觉最近很回答了几个关于类加载的问题,所以萌生了写该文章的念头。以前也查阅、搜索过相关的资料,这里按照自己的想法总结一下,以便加深记忆和以后查阅。同时也希望能给读者带来一些启发。
类的生命周期
(该图片来至网络,如有侵权请联系我删除)
加载
需要加载的类并把类的信息加载到jvm的方法区中,然后在堆区中实例化一个java.lang.Class对象,作为方法区中这个类的信息的入口。
对于这个步骤笔者理解并不是很深,所以这里就不多说了,等以后搞明白了再抽空补充。
连接
连接阶段比较复杂,一般会跟加载阶段和初始化阶段交叉进行,这个阶段的主要任务就是做一些加载后的验证工作以及一些初始化前的准备工作,可以细分为三个步骤:验证、准备和解析。
- 验证:这一步很好理解,就是要验证一下这个类是否合法,比如这个类是不是符合字节码的格式、变量与方法是不是有重复、数据类型是不是有效、继承与实现是否符合标准等。总之,其目的就是保证加载的类是能够被jvm所运行。
- 准备:准备阶段的工作就是为类的静态变量分配内存并设为jvm默认的初值,对于非静态的变量,则不会为它们分配内存。有一点需要注意,这时候,静态变量的初值为jvm默认的初值,而不是我们在程序中设定的初值。jvm默认的初值是这样的:
- 基本类型:
- 引用类型的默认值为null。
- 常量的默认值为我们程序中设定的值。
- 基本类型:
解析:这一阶段的任务就是把常量池中的转换为直接引用。
- 符号引用:个人的理解是它就个字符串,丢在class文件中的常量池中,包括类和接口的全限定名、字段的名称和描述符以及方法的名称和描述符。比如Object类,那么它的符号引用就是: java/lang/Object
- 直接引用:个人理解就是内存地址。如:68d767dc
你要JVM去找 java/lang/Object,它肯定不知道那是个什么鬼。所以要转成直接引用,它就知道原来在内存中的68d767dc这个位置。
初始化
如果一个类被直接引用,就会触发类的初始化。在java中,直接引用的情况有以下几种:
- 创建类的实例,即使用new关键字实例化对象
- 引用类的静态变量(除常量)
- 引用类的静态方法
- 通过反射方式执行以上三种行为
- 当初始化一个类时,发现其父类还未初始化,则先初始化父类
- 虚拟机启动时,作为程序直接入口(定义了main()方法)的那个类先初始化
除了上述的几种情况,其他使用类的方式叫做被动引用,而被动引用不会触发类的初始化。
被动引用:
- 引用父类的静态字段,只会引起父类的初始化,而不会引起子类的初始化。
- 定义类数组,不会引起类的初始化。
- 引用类的static final,不会引起类的初始化。
关于初始化的顺序后面说
使用
这个就不多说了,累死累活初始化了一个类不用放那干啥。直接引用和间接引用都算使用。
卸载
这个我的理解就是GC觉得它没用了(找不到对于该类的引用),干掉之。
类的初始化顺序
一个类里面有static成员变量/代码块、非static成员变量、构造代码块、构造方法、构造方法里面的代码。那么它们的执行顺序又是怎么样呢?
构造方法
构造方法和构造代码块
我们先来看一下我们先来看看
构造方法和构造代码块
谁先谁后,看下面代码:public class Test1 extends Test2 { // 构造方法 public Test1() { } // 构造代码块 { System.out.println(1); } public static void main(String[] args) { new Test1(); } } class Test2 { public Test2() { System.out.println(2); } }
输出:2 1
证明:
构造方法 先于 构造代码块 执行
有人可能会问,这段代码如何能证明?首先我们都知道,一个类的构造方法里面第一行必须是super()或者this(),如果没有显示声明,那么就会隐式的加上super()。然后我们看输出,先输出了2,这说明先执行了super()。因为super()必须在构造方法第一行,所以这里就证明了构造方法 先于 构造代码块 执行
构造代码块和构造方法内的代码
既然构造方法先于构造代码块执行,那么构造代码块和构造方法内的代码谁先谁后呢?看下面代码:
public class Test1 { // 构造方法 public Test1() { System.out.println(1); } // 构造代码块 { System.out.println(2); } public static void main(String[] args) { new Test1(); } }
输出:2 1
证明:构造代码块 先于 构造方法内的代码 执行
构造方法和非static成员变量
接下来看一下我们来看看
构造方法和非static成员变量
谁先谁后,看下面代码:public class Test1 extends Test2 { private Test3 t = new Test3(); // 构造方法 public Test1() { } public static void main(String[] args) { new Test1(); } } class Test2 { public Test2() { System.out.println(2); } } class Test3 { public Test3() { System.out.println(3); } }
输出:2 3
证明:构造方法 先于 非static成员变量 执行
证明原理参考1,也是根据super()来的。
非static成员变量和构造代码块
接下来看一下我们来看看
非static成员变量和构造代码块
谁先谁后,看下面代码:public class Test1 { private Test2 t = new Test2(); // 构造代码块 { System.out.println(1); } // 构造方法 public Test1() { } public static void main(String[] args) { new Test1(); } } class Test2 { public Test2() { System.out.println(2); } }
输出:2 1
证明:非static成员变量 先于 构造代码块 执行
总结
由上述证明可以得出结论:
构造方法 > super() > 非static成员变量 > 构造代码块 > 构造方法里的代码
其实准确的说,构造方法的执行分为两步:
- 执行隐式三步
- super()
- 非static成员变量初始化
- 执行构造代码块
- 执行构造方法里面的代码
所以上面的结论修改为:
构造方法( super() > 非static成员变量 > 构造代码块 > 构造方法里的代码 )
static成员变量/代码块
现在还剩下static成员变量/代码块 和 构造方法谁先谁后,看代码:
public class Test1 {
public Test1() {
System.out.println(2);
}
static {
System.out.println(1);
}
public static void main(String[] args) {
new Test1();
}
}
输出:1 2。
证明:
static成员变量/代码块最先执行
上面只有一个static成员变量/代码块,如果有多个呢?看下面代码
public class Test1 {
static {
System.out.println(3);
}
private static Test2 t = new Test2();
static {
System.out.println(1);
}
public Test1() {
System.out.println(2);
}
public static void main(String[] args) {
new Test1();
}
}
class Test2 {
public Test2() {
System.out.println(4);
}
}
输出:3 4 1 2。
证明:
static成员变量/代码块最先执行,如果有多个static则按照从上到下的顺序执行
初始化顺序总结
由上述证明可以得出结论:
static成员变量/代码块 > 构造方法(super() > 非static成员变量 > 构造代码块 > 构造方法里的代码)