加载类的生命周期如下,加载的开始执行顺序(注意是开始执行顺序,而不是执行完再执行下一步,是交叉进行的)必须按照以下顺序执行(解析和初始化某些情况会倒过来)。
1.加载
加载分为以下三步:
a. 通过一个类的全限定名(比如: com.demo.test.java)获取此类的二进制字节流。(不一定直接加载一个class文件,比如通过反射动态加载,通过jsp文件生成class类,从压缩包中读取等)
b. 将此字节流所代表的静态存储结构转化成方法区(java8 以上放在元空间)的运行时数据结构。
c. 在内存中生成一个代表这个类的java.lang.class对象,作为这个类在方法区(java8 以上在元空间)各种数据(方法,变量等)的访问入口。
2.验证
验证过程是防止执行了有害代码导致系统崩溃(如果是通过编译java类编译的class文件那么在编译时就会失败,但是字节流不止是来源于直接编译java类生成了,还有其他的途径生成,因此需要验证字节流的安全性)
a. 文件格式验证。比如验证是否以magic number(magic number 用于标记一个文件格式)0xCAFEBABY(java 的magic number格式)开头,验证主次版本是否在当前虚拟机处理范围(允许执行低版本,不可执行超过当前版本的,比如jdk7虚拟机加载jdk8生成的字节流)等等。
b. 元数据验证。主要是对元数据进行语义分析,比如验证是否有继承父类(除了Objec类,其他类都应当有父类),验证是否继承了final继承的类,重载方法是否符合规则,字段方法名是否冲突等等。
c. 字节码验证。通过数据流和控制流分析整个程序的语义是否合法。比如是否直接将一个父类对象赋值给一个子类数据类型,甚至是毫无相关的数据类型。比如保证跳转指令不会跳转到方法体以外的字节码指令上。
d. 符号引用验证,这个验证发生在解析阶段将符号引用转化为直接引用。(符号引用: 比如一个类a引用了类b,由于编译期并不知道类b实际内存地址,因此通过类似全限定名的常量来标记引用了类b,这个常量即为符号引用,方法和字段的符号引用也同理)。此验证内容包括验证通过该全限定名是否可以找到对应的类,在引用类中是否存在当前引用的方法和字段,符号引用的类,字段,方法的访问性(public,private,protected,default)是否可以被当前类访问。
3. 准备
准备阶段是正式为类变量(被static修饰的变量)分配内存和设置变量初始值的阶段。
如下定义的变量,在该阶段value会被初始化被0,注意不是111而是0,因为通常情况下当前阶段还没有进行赋值动作。
public static int value = 111;
但是如果是被final定义的属性,那在该阶段就会进行赋值为111。
public static final int value = 111;
4.解析
解析阶段是将符号引用替换成直接引用的过程。
a. 类或接口的解析,将代表符号引用的全限定名传递给类加载器去加载。
b. 字段解析,进行字段解析会先解析字段所属的类或接口的符号引用,过程会先查类或接口本身有没有包含此字段,如果找不到会从下而上找继承的类或者实现的接口有没有。
c. 类方法解析。类似字段的解析方式。
d. 接口方法解析。类似字段解析的方式。
5. 初始化
<clinit>() 方法(执行类变量即静态变量的赋值操作和静态语句块的语句 static{}),java类会先执行父类的clinit方法再执行子类,因此object类的clinit方法是最先执行的,接口不会去执行父类的clinit方法,多线程环境下同时初始化一个类,只有一个执行,其他线程阻塞等待。
- java类初始化三个特殊实例
a.通过子类引用父类的静态字段不会触发子类的类初始化
public class Test {
public static void main(String[] args) {
System.out.println(Test2.value);
}
}
class Test1{
public static int value =123;
static {
System.out.println("test1初始化了");
}
}
class Test2 extends Test1{
static {
System.out.println("test2初始化了");
}
}
b.通过数组定义引用类,不会触发此类的初始化
public class Test {
public static void main(String[] args) {
Test1[] test1 = new Test1[10];
}
}
class Test1{
public static int value =123;
static {
System.out.println("test1初始化了");
}
}
结果如下,没有任何输出。
c.调用另外一个类的定义的静态常量,不会触发被调用类的初始化
不会加载的原因是编译阶段通过常量传播优化,调用Test1的常量会被优化成Test直接引用常量池中的该常量,因此实际上Test已经不存在Test1的符号引用入口了。
public class Test {
public static void main(String[] args) {
System.out.print(Test1.HELLO);
}
}
class Test1{
static final String HELLO = "hello";
static {
System.out.println("test1初始化了");
}
}
结果如下。
类加载器
1. bootstrap classLoader 启动类加载器,用于加载jdk根目录下的lib目录中的jar文件。
2. extension classLoader 扩展类加载器,用于加载jdk根目录下的lib/ext/目录中的jar文件。
3. application classLoader 应用程序类加载器,用于加载classpath即用户类路径上的类,一般没有自定义类加载器,那程序默认就是用这个加载器加载。
双亲委派模式
如果一个类加载器收到类加载的请求,不会先自己尝试加载,而是先把这个请求交给父加载器加载,父加载器无法加载后,子加载器再去加载,加载关系图如下。通过这个模式,可以避免系统出现同包同名的类,从而导致应用程序出问题。