您的位置:首页 > 编程语言 > Java开发

【JVM系列】Java类型装载、连接与初始化

2015-12-15 12:13 253 查看
深入Java虚拟机第七章读书笔记

Java虚拟机通过装载、连接和初始化一个Java类型,使该类型可以被正在运行的Java程序所使用。装载就是把二进制形式的Java类型读入Java虚拟机中;连接就是把这种已经读入到虚拟机的二进制形式的类型数据合并到虚拟机的运行时状态中去。连接分为三步:验证、准备和解析。“验证”确保了Java类型数据格式正确并且适于Java虚拟机使用,“准备”负责为该类型分配它所需的内存,“解析”负责把常量池中的符号引用转换为直接引用,另外,虚拟机的实现可以推迟解析这一步,它可以在运行中的程序真正使用某个符号引用时再去解析它。当前面这三步完成时,该类型就已经为初始化做好了准备。

如上图所示,装载、连接和初始化这三个阶段必须按顺序进行。



装载

装载阶段由三个基本动作组成:

1、通过该类型的完全限定名,产生一个代表该类型的二进制数据流

2、解析这个二进制数据流

3、创建一个表示该类型的java.lang.Class类的实例

Java类型要么由启动类装载器装载,要么通过用户自定义的装载器装载,启动类装载器是虚拟机实现的一部分,用户自定义的类装载器是类java.lang.ClassLoader的子类实例,它以定制的方式装入类。类装载器(启动类装载器或者用户自定义类装载器)并不需要一直等到某个类型“首次主动使用”时再装载它。

验证

当类型被装载后,就准备进行连接了,连接过程的第一步就是验证,它主要确认类型是否符合Java语言的语义,并且它会不会危及虚拟机的完整性。

准备

验证完成之后,就进入准备阶段,在准备阶段,Java虚拟机为类变量分配内存,设置默认值,但是在到达初始化阶段之前,类变量都没有被初始化为真正的初始值(在准备阶段是不会执行Java代码的)。

解析

解析过程就是在类型的常量池中寻找类、接口、字段和方法的符合引用,把符合引用替换成直接引用的过程。

初始化

当一个类首次主动使用时,必须被初始化,也就是为类变量赋予正确的初始值。

在Java代码中,一个正确的初始值是通过类变量初始化语句或者静态初始化语句给出的。

一个类变量初始化语句是变量声明后面的等号和表达式:

class Example {
static int size = 10;
}


静态初始化语句是一个以static关键字开头的程序块:

class Example {
static int size;

static {
size = 10;
}

}


所有的类变量初始化语句和类型的静态初始化语句都会被Java编译器收集在一起,放在一个特殊的方法中,对于这个类来说,这个方法称作类初始化方法;对接口来说它称为接口初始化方法。在类和接口的Java class文件中,这个方法被称为
“<clinit>”
。通常Java程序方法是无法调用这个方法的,这个方法只能被java虚拟机调用,专门用来把类型的静态变量设置为它们的正确初始值。

初始化一个类包含两个步骤:

1、如果类存在直接超类的话,且直接超类还没有被初始化,就先初始化直接超类。

2、如果类存在一个类初始化方法,就执行此方法。

下面我们来举个例子,看看初始化方法执行流程。

public class Base {
public static void main(String[] args) {
System.out.println(Sub.subHeight);
}
}

class MyParent {
public static int height;
public static int width = 3;

static {
height = 5;
System.out.println("MyParent static");
}
}

class Sub extends MyParent {
public static int subHeight;
public static int subWidth = 3;

static {
subHeight = 5;
System.out.println("Sub static");
}
}


整个流程图如下:



从上面的流程我们可以很容易看出最终的执行结果:

MyParent static
Sub static
5


初始化接口并不需要初始化它的父接口,因此初始化一个接口只需一步:如果接口存在初始化方法的话,就执行此方法。

下面有几个点对初始化进行了说明:

1、Java虚拟机必须确保初始化过程被正确地同步,如果多个线程需要初始化一个类,仅仅允许一个线程来进行初始化,其他的线程需要等待。当活动的线程完成了初始化过程之后,它必须通知其他等待的线程。

2、并非所有的类都需要在它们的class文件中拥有一个
<clinit>
方法。如果类没有声明任何类变量,也没用静态初始化语句,那么它就不会有
<clinit>
方法。如果类声明了类变量,但是没有明确使用类变量初始化语句或者静态初始化语句初始化它们,那么类也不会有
<clinit>
方法。如果类仅包含静态final变量的类变量初始化语句,而且这些类变量初始化语句采用编译时常量表达式,类也不会有
<clinit>
方法。总之,只有那些的确需要执行java代码来赋予类变量正确初始值的类才会有类初始化方法。

主动使用和被动使用

Java虚拟机在首次主动使用类型时初始化它们,只有6种活动被认为是主动使用。

1、当创建某个类的新实例时。

2、当调用某个类的静态方法时。

3、当使用某个类或者接口的静态字段,或者对该字段赋值时;用final修饰的静态字段除外,它被初始化为一个编译时的常量表达式。

4、当调用Java API中的某些反射方法时,比如类Class中的方法或者java.lang.reflect包中的类的方法。

5、当初始化某个类的子类时(某个类初始化时,要求它的超类已经被初始化了)。

6、当虚拟机启动时某个被标明为启动类的类(含有main()方法的那个类)。

在某个类型首次使用的时候,必须被初始化,然而,在类型能被初始化之前,它必须已经被连接了,而它能被连接之前,它必须已经被装载了。

使用一个非常量的静态字段只有当类或者接口的确声明了这个字段时才是主动使用。比如,类中声明的字段可能被子类引用;接口中声明的字段可能会被子接口或者实现了这个接口的类引用。对于子类、子接口和实现了接口的类来说,这就是被动使用,被动使用并不会触发它们的初始化。

class Parent {
static int parent = 10;

static {
System.out.println("Parent initialized");
}
}

class Child extends Parent {
static int child = 20;

static {
System.out.println("Child initialized");
}
}

public class ExampleDemo {
static {
System.out.println("ExampleDemo was initialized");
}

public static void main(String[] args) {
int parent = Child.parent;
System.out.println(parent);
}
}


输出结果:

ExampleDemo was initialized
Parent initialized
10


在ExampleDemo里面,我们可以看到,Child使用到了父类的parent类变量,因为Child使用的这个类变量并不是Child这个类声明的,而是它的父类的,所以对于Child的使用属于被动使用,因此不会引发Child类的初始化操作。它只是引发了Parent类的初始化。

下面我们来对ExampleDemo类的man方法进行修改。

public class ExampleDemo {
static {
System.out.println("ExampleDemo was initialized");
}

public static void main(String[] args) {
int child = Child.child;
System.out.println(child);
}
}


输出结果:

ExampleDemo was initialized
Parent initialized
Child initialized
20


可以看到Child执行了初始化操作,因为Child使用了child类变量,这个类变量是这个类所声明的,它属于主动使用,会引发初始化操作。另外这里也说明了上面所讲到的:如果类存在直接超类的话,且直接超类还没有被初始化,就先初始化直接超类。它首先初始化了Parent,然后初始化自身。

如果一个字段既是静态static的又是终态final的,并且使用一个编译时常量表达式初始化,使用这样的字段,就不是对声明该字段的类的主动使用。这个很好理解,前面说过,Java编译器并没有把它当作类变量,而且当作了一个常量。同样是上面的例子,我们把类变量加上final。

class Parent {
static final int parent = 10;

static {
System.out.println("Parent initialized");
}
}

public class ExampleDemo {
static {
System.out.println("ExampleDemo was initialized");
}

public static void main(String[] args) {
int parent = Child.parent;
System.out.println(parent);
}
}


输出结果:

ExampleDemo was initialized
10


可以看到使用了parent变量,但是Parent类没有被初始化。

==============================

下面小结一下:

整个过程有三步:装载、链接、初始化

装载:就是将class文件的二进制流读入到内存,将其放在运行时数据区的方法区内,然后在堆区创建一个Class对象实例,用来封装类在方法区的数据结构。

链接分为三步:

验证:就是对这个class文件进行验证是否是一个合法符合规范的class文件,因为一般情况下,我们就直接javac将一个java文件编译成一个class文件,但是其实我们也可以根据class文件的规范来自己写一个class文件,这样,这个class文件是否是正确合法的就需要通过验证来确定,多说一个java中的Proxy动态代理的实现原理就是根据提供的接口来直接生成一个class实现类然后装载进内存。

准备:静态变量内存分配并设置默认值

解析:把字符引用替换为直接引用

初始化:静态变量的初始化过程。

从上面我们可以看到,静态变量的内存分配过程和初始化过程是两个不用的阶段。

下面举个例子:

class Singleton {
static Singleton instance = new Singleton();
static int count;
static int count1 = 0;

static Singleton getInstance() {
return instance;
}

Singleton() {
count++;
count1++;
}
}

public class Test {
public static void main(String[] args) {
Singleton instance = Singleton.getInstance();
System.out.println("count: " + instance.count);
System.out.println("count1: " + instance.count1);
}

}


首先看看输出结果:

count: 1
count1: 0


原因其实很简单,Singleton.getInstance()引发了Singleton类的装载,装载完成之后,就是验证和准备,准备过程会为静态变量分别内存并设置默认值,静态变量为instance,count,count1,默认值分别为null,0,0,为它们分别内存完成之后就是解析和初始化,初始化就会为静态变量进行初始化,首先是为instance初始化,就是new Singleton();它引发了构造函数的执行,所以count,count1分别加1,此时count = 1,count1 = 1,instance初始完成之后,接着就是count1进行初始化,因为count没有初始化值,所以不需要初始化,count1进行初始化设置为0,此时count1 = 0,所以,最终结果就是count = 1,count1 = 0。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: