类的生命周期
上述的7个阶段中,只有加载、校验、准备、初始化、卸载,这五个阶段的顺序是固定的。
类的加载过程
加载
1、将字节码文件转换成二进制字节流
2、字节流以某种数据结构存放在方法区
3、创建java.lang.Class的一个实例对象,存放在方法区。
校验
校验calss文件的字节流中包含的信息是否符合虚拟机规范,防止对虚拟机自身造成危害。
校验一共有以下4个阶段。
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
准备
把类变量存放在方法区中,并赋予初值。
public static int a=3;
此时a的初值就是0.
public static final int a=3;
此时a的初值是3.
注意区分以上两种情况。
解析
将符号引用转化成直接引用。
解析阶段一般发生在初始化完成之后。
初始化
执行类构造器<clinit>
的过程。
类构造器是编译器自动生成的,而不需要用户自己定义。
编译器收集类变量的赋值动作和静态代码块,并将它们合并在一起,组成了类构造器。
public class Student {
public static int a=3;
static{
a=4;
b=10;
}
public static int b=4;
}
Student的类构造器如下所示:
static <clinit>()V
L0
LINENUMBER 4 L0
ICONST_3
PUTSTATIC JVM/Student.a : I
L1
LINENUMBER 6 L1
ICONST_4
PUTSTATIC JVM/Student.a : I
L2
LINENUMBER 7 L2
BIPUSH 10
PUTSTATIC JVM/Student.b : I
L3
LINENUMBER 9 L3
ICONST_4
PUTSTATIC JVM/Student.b : I
RETURN
MAXSTACK = 1
MAXLOCALS = 0
}
可以发现编译器的收集顺序是自上而下的,故最终Student.a的值为4,Student.b的值为4.
虚拟机会保证类构造器在多线程环境下是同步加锁的。
public class ThreadTest {
public static void main(String[] args) {
new Thread(()->{
System.out.println(A.a);
}).start();
new Thread(()->{
System.out.println(A.a);
}).start();
}
static class A{
public static int a=9;
static {
System.out.println("a...");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
通过上述代码可以发现,有两个线程,当其中一个线程,执行类构造器方法时,另一个线程一直在等待,直到方法执行结束,才输出了静态变量的值。
类加载器
这些类加载器之间并不是继承关系,而是组合(包含)关系
类加载器其实可以分为两类:引导类加载器和自定义加载器。
自定义加载器是指继承重写了ClassLoader抽象类的类加载器。扩展类加载器和系统类加载器都是自定义加载器。
双亲委派机制
java的类加载机制是一种按需加载的机制,只有当你真正要用到它的时候才会进行加载。
步骤:
1、一个类加载器收到类加载的请求,会把请求告诉它的父级类加载器。
2、父级类加载器也告诉它的父级类加载器,就这样不断递归,直至找到最顶级的引导类加载器。
3、若是父级类加载器可以加载该类,那么成功返回,若是不能加载,那么再反向委派给它的子级类加载器。
使用双亲委派机制的优势:
1、避免类的重复加载
2、保护程序安全,避免核心API被修改
java.lang.String
package java.lang;
public class String {
public static void main(String[] args) {
String str = "hello";
System.out.println(str);
}
}
会出现以上的错误,就是因为双亲委派机制的功劳。
让类加载器加载java.lang.String这个类,引导类发现这个类以java开头,所以它可以加载,于是把java自己的String类加载进来,可是这个类是没有main方法的,于是就出现了这个错误。