类加载过程
结合例子,解析类加载的每个环节:
1.加载:
加载阶段主要是找到并加载类的二进制数据,比如从 jar 包里或者 war 包里找到它们(字节码)。然后把类加载进方法区。
2.验证:
并不是任何.class文件都会被加载。验证阶段在虚拟机整个类加载过程中占了很大一部分,不符合规范的将抛出 java.lang.VerifyError 错误。像一些低版本的 JVM,是无法加载一些高版本的类库的,就是在这个阶段完成的。
3.准备:
准备阶段,会为类变量(即static修饰的变量)分配内存,并设置默认值,内存的分配都是在方法区中完成。
注意,静态代码块在准备阶段不会被执行,静态代码块在初始化阶段才会被执行。
public class A {
static int a ;
public static void main(String[] args) {
System.out.println(a);
}
}
public class B {
public static void main(String[] args) {
int a ;
System.out.println(a);
}
}
如上面的例子,类A中的静态变量a在准备阶段将会被分配内存,并设置默认值为0。执行结果为打印0。
而类B将无法通过编译,因为方法内的局部变量,不像静态变量存在准备阶段,能够进行赋予变量初始值。
注意点:类变量在准备阶段,只初始化默认值。并不会进行赋值,赋值操作是在初始化阶段完成。如以下代码:
public class C {
static int c=10 ;
public static void main(String[] args) {
System.out.println(c);
}
}
以上代码,在准备阶段,只会初始化默认值c为0,赋值为10的动作在初始化阶段完成。
但是也有例外,被final修饰的静态变量,会在准备阶段就初始化为指定值。如以下代码:
public class D {
static final int d=10 ;
public static void main(String[] args) {
System.out.println(d);
}
}
以上代码中,d在准备阶段将会被初始化为10.
4.解析:
解析在类加载中是非常非常重要的一环,是将符号引用替换为直接引用的过程。
解析阶段负责把整个类激活,串成一个可以找到彼此的网,过程不可谓不重要。那这个阶段都做了哪些工作呢?大体可以分为:
类或接口的解析
类方法解析
接口方法解析
字段解析
我们来看几个经常发生的异常,就与这个阶段有关。
java.lang.NoSuchFieldError 根据继承关系从下往上,找不到相关字段时的报错。
java.lang.IllegalAccessError 字段或者方法,访问权限不具备时的错误。
java.lang.NoSuchMethodError 找不到相关方法时的错误。
解析过程保证了相互引用的完整性,把继承与组合推进到运行时。
5.初始化:
5.0 类初始化的时机:
第一,new 对象,读取或者设置静态变量字段,访问静态方法的时候
第二,反射调用类的时候,类没有初始化,则需先初始化
第三,初始化类时,发现父类还没初始化,则需先初始化父类
第四,虚拟机启动时,用户需指定main方法的主类,虚拟机会先初始化该类
第五,使用jdk1.7的动态语言时.不太了解,省略不详细说明。
5.1 父类先于子类进行初始化,所以所有类第一个初始化的类必然是Object类。
5.2 基于第一点,父类的静态变量和静态代码块也先于子类的静态变量和静态代码块的初进行初始化
5.3 静态代码块,只能访问定义在静态代码块前的静态变量。而定义静态代码块之后的变量,只能赋值,不能访问。在如下例子所示
public class A {
static int a = 0 ;
static {
a = 1;
b = 1;
}
static int b = 0 ;
public static void main(String[] args) {
System.out.println(a);
System.out.println(b);
}
}
此代码将打印1和0。
此处详细解释下b的情况,首先是在准备阶段,a和b分别被初始化默认值0,而后在初始化阶段,a先被赋值为0,而后静态代码块将a,b均赋值为1,最后b会被赋值为0,所以结果为1 和0.
将以上代码修改下:
public class A {
static int a = 0 ;
static {
a = 1;
b = 1;
}
static int b ;
public static void main(String[] args) {
System.out.println(a);
System.out.println(b);
}
}
以上代码中,b静态变量,没有赋值操作,那么最后打印的结果是1和1.
详细解释下b的过程:首先准备阶段,a和b被初始化为0,其后静态代码块,将a和b赋值为1。所以最后结果为1和1.
但是如下例子将编译不通过,因为定义在代码块后的静态变量只能被赋值,不能被访问
public class B {
static {
b = b+1;
//或
System.out.println(b);
}
static int b = 0 ;
public static void main(String[] args) {
System.out.println(b);
}
}
5.4 <cinit>()和<init>()的区别
<cinit>()是初始化阶段被调用,用来初始化静态变量和静态代码块的,父类的<cinit>()执行先于子类,并且随着类的加载完成,只执行一次。
<init>()是在,类加载完完成之后,实例化对象时被调用,用来初始化普通成员变量的,父类的<init>()执行先于子类,new 对象几次,就执行几次。
如下代码所示:
public class A {
static {
System.out.println("1");
}
public A(){
System.out.println("2");
}
}
public class B extends A {
static{
System.out.println("a");
}
public B(){
System.out.println("b");
}
public static void main(String[] args){
A ab = new B();
ab = new B();
}
}
以上代码执行结果为:1 a 2 b 2 b