Java中任何一个类型对象在使用之前都必须经历加载、连接、初始化三个类加载步骤。一旦成功执行这三个步骤,它就可以被使用了,我们可以通过访问类的静态类成员表变量,或者使用new关键字创建对象实例。
类加载器
类加载是JVM执行类加载的前提。简单来说,类加载的主要任务就是根据一个类的全限定名来读取此类的二进制字节流(.class)文件到JVM内部,然后再转为一个实例对象。在程序中,类加载器只有三种:
- BootstrapClassLoader
- ExtClassLoader
- AppClassLoader
BootstrapClassLoader主要负责加载“java_home/lib”下面的类型,ExtClassLoader负责加载“/lib/ext”中的类型,AppClassLoader负责加载ClassPath下面的类。
如果当前的类加载不能满足我们的需要是,我们可以编写自定义的类加载器,只要继承抽象类ClassLoader并重写findClass()方法即可,之后只要在程序中调用loadClass()方法来实现类加载。ClassLoader中常用的方法如下:
//使用类名name加载类
loadClass(String name) throws ClassNotFoundException;
//查找名为name的类
findClass(String name) throws ClassNotFoundException;
//将字节数组b内容转化为一个类
defineClass(String name,byte[] b,int off,int len);
//返回父类加载器
getParent();
如果要自定义个一个类加载器,除了要继承ClassLoader以外,还要重写findClass()方法和defineClass()方法。
为了确保一个类的全局唯一性,也就是当程序中出现多个相同的全限定名相同的类时候,只会加载一个类,不会多个类都执行加载,java设计者们使用了双亲委派模型:除了启动类加载以外,程序中每一个加载器都有自己父类加载器;当一个类加载器接收一个类加载任务时,不会马上加载,而是把加载任务委派给父类加载器,层层类推,直至委派给顶层类加载器;如果父类加载器不能加载委派给的加载任务时,就会退回个下一级类加载器。
注意:如果程序中没有显示的指定类加载器时,AppClassLoader就是默认的任务委派发起者,如果被加载的类没有包含在ClassPath目录中时,就会报ClassNotFoundException。
类的加载过程
类加载器锁执行的加载任务,仅仅是属于JVM中类加载过程中的一个阶段,一个完整的类加载过程有加载、连接和初始化,加载结束以后就可以使用了。
- 加载阶段:由类加载器根据一个类的全限定名来读取这个类的.class文件到JVM内部,并存储在内存中的方法区,接着转化成一个目标类的对象实例,这个对象会以后会成为方法区中该类的数据访问入口。
- 验证阶段:该阶段主要的任务是就是验证数据类型是否符合JVM规范,字节码文件是否有效。
- 准备阶段:主要任务是为存放在方法区中类的所有类变量分配内存空间,并设置一个初始值(对象没有产生,所以不操作实例变量)。
- 解析阶段:将常量池中的所有符号引用全部转化为直接引用,解析阶段不一定按照顺序执行,因此可以等到初始化阶段结束以后再执行。
- 初始化阶段:这是类加载过程中的最后一个阶段,在这个阶段中,JVM会把类中所有static变量、代码块执行一次,如果是static变量,就会覆盖准备阶段赋的初始值,如果没有对static变量显示的赋值,则值还是准备阶段赋的值。所有的类变量初始化和静态代码块在编译时,都会存放到一个特殊的方法“clinit()”中。
一个类或者接口应该在何时被初始化呢?
- 使用new关键字、反射或序列化
- 调用类型的静态方法
- 调用类型的静态字段,或者对其赋值操作;final修饰的静态字段除外,它被初始为一个常量
- 调用Java API中反射方法时
- 初始化一个类的子类时,父类会提前初始化
- JVM启动带有main()方法的启动类
对于接口初始化有点不同:当接口中的非常量字段被使用时,改接口才会被初始化,而不会在实现该接口时被初始化。换句话说,接口初始化时,父类接口不需要初始化。
public class LoadingTest {
public static LoadingTest obj = new LoadingTest();//1
static{
System.out.println("执行:static int value1");
}
public static int value1;//6
static{
System.out.println("执行:int value2 = 0");
}
public static int value2 = 0;//7
public LoadingTest() {
System.out.println(value1);//2
System.out.println(value2);//3
value1 = 10;//4
value2 = value1;//5
System.out.println("前面value1:"+value1);
System.out.println("前面value2:"+value2);
}
public static void main(String[] args) {
System.out.println("后面value1:"+value1);
System.out.println("后面value1:"+value2);
}
}
/*
output:
0
0
前面value1:10
前面value2:10
执行:static int value1
执行:int value2 = 0
后面value1:10
后面value2:0
*/
main()调用了静态变量value1、value2,JVM对LoadingTest执行加载。初始化时,首先执行代码1,需要new一个LoadingTest对象,执行了构造方法,此时静态变量value1、value2还没有被设置初始值,还是准备阶段JVM赋的初始值,所以代码2,3输出为0和0。当执行代码4,5时,为静态变量value1、value2赋值了;结束构造函数以后,开始执行代码6,此时并有为value1赋值,所以value1还是为10;执行代码7时,对value2赋值为0。所以最后结果value1=10,value2=0。
注意:JVM会确保类的初始化过程是线程安全的。