类的生命周期:分为装载,链接,初始化
如图:
1)装载:查找并装载类型的二进制数据
2)连接:执行验证,准备,和解析(可选)
a) 验证:确保导入类型正确
b) 准备:为类变量分配内存,并将其初始化为默认值
c) 解析:把类型中的符号引用转换成直接引用
3)初始化:把类变量初始化为默认初值
随着Java虚拟机装载了一个类,并执行了一些它选择进行的验证之后,类就可以进入准备阶
段了。在准备阶段,Java虚拟机为类变量分配内存,设置默认初始值:但在到达初始化阶段之前,
类变量都没有被初始化为真正的初始值。(在准备阶段是不会执行Java代码的。)在准备阶段,虚
拟机把给类变量新分配的内存根据类型设置为默认值。
为了准备让一个类或者接口被"首次主动"使用,最后一个步骤就是初始化,也就是为类变量
赋予正确的初始值。这里的”正确”初始值指的是程序员希望这个类变量所具备的起始值。正
确的初始值是和在准备阶段赋予的默认初始值对比而言的。前面说过,根据类型的不同,类变
量已经被赋予了默认初始值。而正确的初始值是根据程序员制定的主观计划面生成的。
在Java代码中,一个正确的初始值是通过类变量初始化语句或者静态初始化语句给出的。
1)一个类变量初始化语句是变量声明后面的等号和表达式:
2)静态初始化语句是一个以static开头的程序块
example :
public class Example1 {
// 类变量初始化语句
static int value = (int) (Math.random()*6.0);
// 静态初始化语句
static{
System.out.println("this is example");
}
}
所有的类变量初始化语句和类型的静态初始化器都被Java编译器收集在—起,放到——个特殊
的方法中。对于类来说,这个方法被称作类初始化方法;对于接口来说,它被称为接口初始化
方法。在类和接口的Javaclass文件中,这个方法被称为”<clinit>”。通常的Java程序方法是无法
调用这个<clinit>方法的。这种方法只能被Java虚拟机调用
clinit>()方法
前面说过,Java编译器把类变量初始化语句和静态初始化浯句的代码都放到class文件的
<clinit>()方法中,顺序就按照它们在类或者接门声明中出现的顺序。
example:
public class Example1 {
static int width;
static int height = (int) (Math.random()*6.0);
static{
width = (int) (Math.random()*3.0);
}
}
java 编译器生成下面<clinit>方法:
0 invokestatic java.lang.Math.random
3 ldc2_w 6.0 (double)
6 dmul
7 d2i
8 putstatic Example1.height
11 invokestatic java.lang.Math.random
14 ldc2_w 3.0 (double) 17 dmul
18 d2i
19 putstatic Example1.width
22 return
clinit 方法首先执行唯一的类变量初始化语句初始化heght,然后在静态初始化语句中
初始化width(虽然它声明在height之前,但那仅仅是声明了类变量而不是类变量初始化语句).
除接口以外,初始化一个类之前必须保证其直接超类已被初始化,并且该初始化过程是由 Jvm 保证线程安全的。
另外,并非所有的类都会拥有一个 <clinit>() 方法。
1)如果类没有声明任何类变量,也没有静态初始化语句,那么它不会有<clinit>()方法。
2)如果声明了类变量但是没有使用类变量初始化语句或者静态初始化语句初始它们,那么类不会有<clinit>()方法。
example:
public class example{
static int val;
}
3)如果类仅包含静态 final 变量的类变量初始化语句,并且类变量初始化语句是编译时常量表达式,类不会有<clinit>()方法。
example:
public class Example {
static final String str ="abc";
static final int value = 100;
}
这种情况java编译器把 str 和 value 被看做是常量,jvm会直接使用该类的常量池或者在字节码中直接存放常量值。该类不会被加载。
如果接口不包含在编译时解析成常量的字段初始化语句,接口中就包含一个<clinit>()方法。
example:
interface Example{
int i =5;
int hoursOfSleep = (int) (Math.random()*3.0);
}
字段hoursOfSleep会被放在<clinit>()方法中(比较诡异???它被看作类变量了),而字段i被看作是编译时常量特殊处理(JAVA语法规定,接口中的变量默认自动隐含是public static final)。
java 编译器生成下面<clinit>方法:
0 invokestatic java.lang.Math.random
3 ldc2_w 3.0 (double)
6 dmul
7 d2i
8 putstatic Example.hoursOfSleep
11 return
主动使用和被动使用
在前面讲过,Java虚拟机在首次主动使用类型时初始化它们。只有6种活动被认为是主动使
用:
1)创建类的新实例,
2)调用类中声明的静态方法,
3)操作类或者接口中声明的非常量静态字段,
4)调用JavaAPI中特定的反射方法
5)初始化一个类的子类;
6)以及指定一个类作为Java虚拟机启动时的初始化类。
使用一个非常量的静态字段只有当类或者接口的确声明了这个字段时才是主动使用、比如,
类中声明的字段可能会被子类引用;接口中声明的字段可能会被子接口或者实现了这个接口的
类引用。对于子类、子接口和实现接口的类来说.这就是被动使用(使用它们并不会触发
它们的初始化)。下面的例子说明了这个原理:
class NewParement{
static int hoursOfSleep = (int) (Math.random()*3.0);
static{
System.out.println("new parement is initialized.");
}
}
class NewbornBaby extends NewParement{
static int hoursOfCry = (int) (Math.random()*2.0);
static{
System.out.println("new bornBaby is initialized.");
}
}
public class Example1 {
public static void main(String[] args){
int hours = NewbornBaby.hoursOfSleep;
System.out.println(hours);
}
static{
System.out.println("example1 is initialized.");
}
}
运行结果:
example1 is initialized.
new parement is initialized.
0
NewbornBaby 没有被初始化,也没有被加载。
对象的生命周期
当java虚拟机创建一个新的类实例时不管明确的还是隐含的,首先要在堆中为保存对象的实例变量分配内存,包含所有在对象类中和它超类中
声明的变量(包括隐藏的实例变量)都要分配内存。其次赋默认初值,最后赋予正确的初始值。
java编译器为每个类都至少生成一个实例初始化方法 "<init>()"与构造方法相对应。
如果构造方法调用同一个类中的另一个构造方法(构造方法重载),它对应的init<>():
1)一个同类init<>()调用。
2)对应构造方法体代码的调用。
如果构造方法不是通过this()调用开始,且对象不是Object 它对应的init<>():
1)一个超类init<>()调用。
2)任意实例变量初始化代码调用。
3)对应构造方法体代码的调用。
如果上述对象是Object,则去掉第一条。如果构造方法明确使用super()首先调用对应超类init<>()其余不变。
下面的例子详细说明了实例变量初始化(摘自Java Language Specification)
class Point{
int x,y;
Point(){x=1;y=1;}
}
class ColoredPoint extends Point{
int color = OxFF00FF;
}
class Test{
public static void main(String[] args){
ColoredPoint cp = new ColoredPoint();
System.out.println(cp.color);
}
}
首先,为新的ColoredPoint实例分配内存空间,以存储实例变量x,y和color;然后将这些变量初始化成默认值
在这个例子中都是0。
接下来调用无参数的ColoredPoint(),由于ColorPoint没有声明构造方法,java编译器会自动提供如下的构造方
法:ColoredPoint(){super();}。
该构造方法然后调用无参数的Point(),而Point()没有显示的超类,编译器会提供一个对其无参数的构造方法的
隐式调用:Point(){super();x=1;y=1}。
因此将会调用到Object();Object类没有超类,至此递归调用会终止。接下来会调用Object任何实例初始化语句
及任何实例变量初始化语句。
接着执行Object()由于Object类中未声明这样的构造方法。因此编译器会提供默认的构造方法object(){}。
但是执行该构造方法不会产生任何影响,然后返回。
接下来执行Point类实例变量初始化语句。当这个过程发生时,x,y的声明没有提供任何初始化表达式,因此这个
步骤未采取任何动作(x,y 仍为0);
接下来执行Point构造方法体,将x,y赋值为1。
接下来会执行类ColoredPoint的实例变量初始化语句。把color赋值0xFF00FF,最后执行ColoredPoint构造方法体
余下的部分(super()调用之后的部分),碰巧没有任何语句,因此不需要进一步的动作,初始化完成。
与C++不同的是,在创建新的类实例期间,java编程语言不会为方法分派来指定变更的规则。如果调用的方法在被
初始化对象的子类中重写,那么就是用重写的方法。甚至新对象被完全初始化前也是如此。编译和运行下面的例子
class Super{
Super(){printThree();}
void printThree{System.out.println("Three");}
}
class Test extends Super{
int three = (int)Math.PI; // That is 3
public static void main(String args[]){
Test t = new Test();
t.printThree();
}
void printThree(){System.out.println(three);}
}
输出:
0
3
这表明Super类中的printThree()没有被执行。而是调用的Test中的printThree()。