java虚拟机类加载机制详解

加载类的生命周期如下,加载的开始执行顺序(注意是开始执行顺序,而不是执行完再执行下一步,是交叉进行的)必须按照以下顺序执行(解析和初始化某些情况会倒过来)。

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即用户类路径上的类,一般没有自定义类加载器,那程序默认就是用这个加载器加载。

双亲委派模式

如果一个类加载器收到类加载的请求,不会先自己尝试加载,而是先把这个请求交给父加载器加载,父加载器无法加载后,子加载器再去加载,加载关系图如下。通过这个模式,可以避免系统出现同包同名的类,从而导致应用程序出问题。

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值