ClassLoader 深入解析
在进入ClassLoader的分析之前我们先看一个JAVA程序例子。
class Singleton {
/* case 1 */
private static Singleton singleton = new Singleton();
public static int counter1;
public static int counter2 = 0;
/**
* case 2
* public static int counter1 = 0;
* public static int counter2 = 0;
* private static Singleton singleton = new Singleton();
*/
private Singleton() {
counter1++;
counter2++;
}
public static Singleton getInstance() {
return singleton;
}
}
public class MyTest {
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
System.out.println("counter1 = " + singleton.counter1);
System.out.println("counter2 = " + singleton.counter2);
}
}
/**
* result in case 1:
* counter1 = 1
* counter2 = 0
* result in case 2:
* counter1 = 1
* counter2 = 1
*/
上面的代码在case1 与case2 条件下运行结果却不一样,仅仅由于private static Singleton singleton = new Singleton();位置不同,要想了解其中的原因需要从类的使用时JVM完成的动作说起,在一个类被JVM使用时大致经历如下三步(加载 — 链接(验证--准备--解析) — 初始化)
那么一个类被JVM使用时,必须预先经历如上图所述的加载过程。那么什么样的条件才会触发上述过程的执行呢?JAVA程序使用类分为主动使用和被动使用,在JVM的实现规范中要求,所有类的“主动使用“虚拟机才执行上述过程初始化相应的类,那么问题就归结为“主动使用”的意义。
1. 创建类的实例。Object A = new ClassA();
2. 访问某个类或接口的静态变量或对静态变量赋值。如Class A{static a} 访问A.a时。需要 指出的是访问类的static final int x = 0(编译时常量)并不被认为是类的主动使用,同样 的假如有条件 Class A extends B;B{static a}如果使用A.a时只会初始化类B,这种情况被认 为是对父类的主动使用。
3. 调用类的静态方法
4.使用反射机制(Class.ForName(xxx)),而ClassLoader.load(并不会初始化类)
5. 初始化一个类的子类时,父类也被主动使用
6. 启动类(java TestMain)
下面文章将针对上述过程给出比较详细的说明。
加载过程
总的来说类的加载是JVM使用类加载器(如系统类加载器、扩展加载器、根加载器)在特定的加载路径里寻找class文件,并将class文件中的二进制数据读入到内存中,其中class的数据结构被放置在运行时数据区的方法区类,并且在堆区里创建该类的Class对象,用来封装类的数据结构信息。其中类加载类的方式有:文件系统加载、网络加载、zip jar 归档文件加载、数据库中提取、动态编译的源文件加载。
类加载的最终产品是位于堆区中的Class对象,其封装了类在方法区内数据结构,并且向Java程序员提供了访问方法区内数据结构的接口,需要指出的是,类的加载并不都是主动使用时才加载,加载器可以实现为有预加载功能,如使用一定的算法预测类的使用。在上面的叙述中我们提到过JVM使用类加载器对class文件进行加载(本文后面部分将着重描述类的加载机制)。
连接过程
类加载后,就是连接阶段了,连接就是将已经读入到内存的类的二进制数据合并到虚拟机的运行环境中去。连接的第一个阶段是类的验证,验证的内容如下:
1.类文件结构的检查,确保类的文件遵循java类文件的固定格式。
2. 语义检查:确保类本身符合java语言的语法规定,比如验证final类型没有子 类,final方法没有被从写,private没有被重写。
3. 字节码验证:确定字节码流可以被java虚拟机安全的执行。
4.二进制兼容验证:确保相互应用的类之间协调一致。
做完验证之后就是类的准备阶段,完成的工作为类的静态变量分配内存并设置为初始值。如有类
Sample{
Static int a = 1;
Static long b;
}
系统会为 a 分配4个字节,并设置初始值为 0,为b分配8个字节并设置初始值为 0.
做完类的准备工作之后就是类的解析,主要工作就是把类中的二进制中的符号引用替换为直接引用。我们举个例子 void show{ objectA.print()};objectA.print() 就是对ClassA的一个符号引用,经过解析之后该处的代码会被一个指向ClassA中方法区print方法的指针。
初始化过程
下面是类的初始化过程,初始化的主要步骤为:检查该类是否已经被加载和连接;如果该类有父类,且没有初始化,对父类进行加载 连接 初始化;假如类中存在初始化语句,依次执行初始化语句。而静态变量的声明以及静态代码块都被看作是类的初始化语句,java虚拟机会按照初始化语句在类文件中的顺序依次来执行他们。如 static int a =1,与static{ a = 3}这样的语句都会被JVM顺序执行。
前面提到,当JVM初始化一个类时要求他的父类已经被初始化,这样的机制并不适用于接口,初始化一个类时,它实现的接口并不需要被初始化,初始化一个接口时,其父接口也不需要被初始化,只有当程序首次使用接口中的静态变量时,才会导致接口的初始化。
通过上面的论述,我们大致对类的使用有了一个初步的了解,接下来我们将分析本文开始时提出的那个程序的运行结果
在case 1 中,在MyTest那种使用 Singleton singleton = Singleton.getInstance();这样的语句为类的主动使用这会触发Singleton类的加载 连接 初始化。
private static Singleton singleton = new Singleton();(A)
public static int counter1;(B)
public static int counter2 = 0;(C)
在加载完后的连接阶段的准备期,会为singleton分配内存,设定默认值为null,counter1默认值为 0,counter2 默认值为 0. 进入初始化阶段,第一步为singleton赋值 会调用Singleton的构造方法,此时执行counter1++,counter2++ counter1 = 1,counter2 = 1;第二步为counter1赋值,由于没有赋值语句counter1 仍为1;第三步为 counter2赋值,counter2 被赋值为 0,所以结果是counter1 =1
Counter2 =0
在case2 中 使用如下语句
public static int counter1;(A)
public static int counter2 = 0;(B)
private static Singleton singleton = new Singleton();(C)
连接准备阶段结束后counter1 = 0,counter2 = 0,singleton = null;初始化时 第一步A语句不用初始化 counter1 =0;第二部B语句初始化为0,counter2 =0;第三步 调用构造函数 counter1++,counter2++ counter1 = 1,counter2 = 1;所以case2的结果为 counter1 =1 counter2 = 1
在下面的部分,我将给大家深入的介绍一下JVM的类加载器。
&nb
补充:软件开发 , Java ,