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"); } }
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
例子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