`
diaoaa
  • 浏览: 18065 次
  • 性别: Icon_minigender_1
  • 来自: 广州
文章分类
社区版块
存档分类
最新评论

Java类加载机制

阅读更多

Java类加载机制(一)

类加载的过程

先从一个HelloWorld说起,对于一个HelloWorld.java文件,起初我们在dos命令行下面使用javac HelloWorld.java编译源程序,生成一个HelloWorld.class的字节码文件,然后我们使用javaHelloWorld就可以执行该程序,可是从我们硬盘上的.class文件是如何变成内存中的执行指令的呢?再细分的一点说,我们知道Java的宣传语:“一次编译,出处执行”,这是由于在应用程序和操作系统中间有一层是Java虚拟机,至于Java虚拟机和操作系统之间的交互,这里不深入讲解。那么我们的问题变成了:.class文件是先怎么被加载到Java虚拟机中的呢?

其实这主要通过Java类加载器完成的,JVM自带了3种类加载器,分别是根类加载器(Bootstrap)、扩展类加载器(Extension)、系统类加载器(SystemClassLoader/也叫做应用类加载器ApplicationClassLoader),另外用户也可以自定义自己的类加载器,如何自定义类加载器后面会说,它们的层次关系如下图所示。


类加载器的工作过程简单的说就是类加载器会将.class文件中的二进制数据读取到内存中,将其放入到JVM运行时数据区的方法区内,然后在java虚拟机的堆中创建一个java.lang.Class对象用来封装类在方法区内的数据结构。Java类加载的最终结果是生成了堆中的Class对象。补充一点:对于这个Class对象其实在编译时就已经存在,无论何时编译器在编译Java源文件的时候都会在编译后的字节码中嵌入一个public、static、final类型的字段class,这个字段表示java.lang.Class的一个实例,因为它是静态的public的,所以,很多时候我们可以通过类名.class来访问它。

详细过程分析:一个.class文件被加载到内存会进行如下的步骤:加载、连接、初始化

加载:就是类加载器ClassLoader查找并加载类的二进制数据到内存的方法区,并在虚拟机的堆中创建一个Class对象的实例,加载的方式可以是本地直接加载也可以是网络下载.class文件或者直接从jar、zip等归档文件中加载等等;

连接:具体可以分为验证、准备、解析三个步骤

a验证:确保被加载类的正确性,包括类的结构检查、语义检查和字节码检查等等;
b准备:为静态变量分配内存空间,并将其初始化,这里的初始化是赋予默认的初始值,如int型则赋予0,boolean型则赋予false;

c解析:将类中的符号引用转化为直接引用

初始化:为类的静态变量赋予正确的初始值,也就是赋予用户对其设置的初始值,和上面的初始化是不同的。

类被初始化时机:JVM只有等到一个类被主动使用时才会去初始化这个类,主动使用有以下6种情况:

1.创建类的实例(比如,通过new关键字创建实例)

2.访问某个类或者接口的静态变量。或者对其静态变量赋值;

3.调用类的静态方法

4.反射

5.初始化一个类的子类,也会初始化该类的父类

6.JVM启动时被标记为启动类的那些类

除了这6种,其他对类的使用都不是主动使用,不会导致类的初始化。

根据上述知识,来看一个类加载次序的例子:

public class TestClassLoader1 {

	public static void main(String[] args) {
		Count c = Count.getCount();
		System.out.println(c.num1);
		System.out.println(c.num2);
	}
}

//定义一个内部类Count
class Count{
	private static Count count = new Count();   //位置1
	
	public static int num1;
	public static int num2 = 0;
	
	//private static Single single = new Single();   //位置2
	
	Count(){
		num1++;
		num2++;
	}
	
	public static Count getCount(){
		return count;
	}
	
}

上述程序运行时,首先会执行main方法,在main方法中调用了Count类的静态方法getCount,所以会导致Count类被加载到内存,加载的时候在连接的准备阶段,静态变量就已经被赋予默认初始值(count=null,num1=0,num2=0),由于主动使用会导致类的初始化,所以还会将用户赋予的正确值赋予它们,程序顺序执行,首先给count变量复制,new关键字调用构造方法,num1和num2都自加了一次,都等于1,接着,num1用户没有对其赋值,不用初始化,num2用户对其赋值为0,所以num2又变为0。所以,程序最后输出的是1,0。上述程序,如果把位置1的语句移到位置2,程序的输出结果就将变成1,1,分析过程和上面是一致的。

对于上述主动使用类的第五种情况:初始化一个类的之类时,也会初始化它的父类,有下面的例子验证

import java.util.Random;

public class TestClassLoader2 {

	public static void main(String[] args) {
		System.out.println(T2.y);
	}
}

class T1 {
	public static int x = 1;
	static{
		System.out.println("T1 block");
	}
}

class T2 extends T1{                                                          
	public static int y = 1;  //位置1
	static{
		System.out.println("T2 block");
	}
}

分析:首先在初始化T2之前,会先初始化它的父类T1,输出“T1 block”,然后初始化T2,输出“T2 block”,最后输出main方法的值1,最终的输出结果就是:

T1 block
T2 block
1
对于含有final修饰的变量,如果在编译的时候可以确定其确切的值,则对其的访问不算对该类的主动使用,下面的例子可以验证这一点:

例1:还是上面的例子,把位置1的语句改为:

public static final int y = 1;	//位置1

那么程序的输出结果为:1 .(分析:编译的时候y的值已经可以唯一的确定,不会改变的,实质就是一个常量,所以对其的访问不会导致T2类的初始化,也就不会导致 T1类的初始化)

例2:还是上面的例子,把位置1的语句改为:

public static final int y = new Random().nextInt(100);  //位置1
那么程序的输出结果为:

T1 block
T2 block
76

原因就是对于静态变量y的值在编译期间不能确定,所以y然是一个静态变量,对其的访问会导致类T2、T1的的初始化。

例子3:当程序访问的静态变量和静态方法确实在当前类或接口中定义时,才可认为是对当前类或接口的主动使用.

public class TestClassLoader3 {

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

class Parent{
	public static int x;
	static{
		System.out.println("Parent Block");
	}
}

class Child extends Parent{
	static{
		System.out.println("Child Block");
	}
}
程序的输出结果为:(原因是对Child的静态变量x的访问不是真正访问Child类的静态变量,而是其父类的静态变量,所以只会初始化其父类)

Parent Block
0


当JVM初始化一个类的时候,要求它的所有父类已经被初始化,但这条规则并不适用于接口。初始化一个类时,并不会初始化它所实现的接口;初始化一个接口时,也不会初始化它已实现的父接口,只有当程序首次使用特定接口的静态变量时,才会导致该接口的初始化。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics