由内而外的类加载过程解析


)

前情提要

java中你new一个对象之后是怎么生效以及怎么在机器中运行的你有想过吗?

所有java代码的运行都是要依靠jvm,及Java Virtual Machine(Java虚拟机),.java文件被java编译器javac编译成.class文件之后,jvm就可以识别.class文件的内容了,这样java就相当于有了自己的语法规则,今天来聊聊jvm是怎么加载.class文件的。

jvm内存模型

在开始之前我们有必要了解一下java内存模型:
内存模型

1、什么是方法区

方法区内主要存储了一些类信息,例如字段、方法、常量池,相当于记录了一个类的结构组成,当你new一个对象的时候,jvm就会根据你这个类在方法区内的数据结构在堆中构建一个新的对象,类似于记录了一个模型,每次的新建必须根据这个模型来构造。

这里面的常量池有一点特殊,常量池里记录了不会变更的常量,比如接口名称,类名称、字段名称、方法名称等等以符号出现的形式,除此之外所有的字符串都会记录在常量池里,每次对字符串的引用如果常量池里有都会从常量池获取,初次之外,加关键字final的属性也会存储在常量池中,所以这也从侧面映射了常量池中的属性是不允许更改的,且是永久的,所以占用内存较大。

2、虚拟机栈

java虚拟机栈生命周期是随线程同生同灭的,主要存储的数据是局部变量,当你请求调用一个方法时会申请一个帧栈,用户保存当前方法的局部变量。每个线程可以申请多个帧栈,递归调用就是典型的例子,所以递归用的不好造成死循环,不断的申请帧栈,最终申请的深度大于虚拟机所允许的深度,就会抛出常见的StackOverflowError异常。

虚拟机栈

3、本地方法栈

本地方法栈从内存方面和java虚拟机栈类似,唯一不同的是本地方法栈是站在操作系统层面的,不属于jvm的范畴,至于这样设计的主要原因我想是因为要给java留有调用本地C代码的口子。

4、堆

堆是存储对象的地址空间,每个新对象的构造都需要在堆上开辟空间。由于我们需要不断的在堆上构造新的对象,而且对象的大小不统一,所以会造成堆的严重浪费和重复利用率低。但是java比C++最大的优势有GC(垃圾回收)机制,jvm会监测堆中内存占用大小,阈值,那些空间是不会再利用了,那些空间还需要继续使用,怎样压缩空间全都可以交给gc来管理了。所有有用的对象会直接或间接和一个root指针建立连接,否则会在下一次gc判断为垃圾进行空间回收。
堆

5、程序计数器

这个比较好理解,和操作系统中的程序计数器含义一样,记录着当前代码走到了那一条指令,每执行完一条指令,程序计数器就会加1,程序计数器的地址记录着下一条指令所对应的地址,每个线程都有自己的程序计数器。

类加载过程

其中类加载包括五个阶段:加载、验证、准备、解析、初始化。
加载过程

1、加载

1. 通过全限定名来加载二进制字节流,这里的全限定名包括:

包名+类名,但是有个需要记住的点,不同的类加载器加载的类不相等,也就是说在方法区中存储的是两套类结构,彼此之间不能访问。
2. 将这个字节流的静态存储结构转化为方法区运行时的数据结构
3. 根据方法区的数据结构在堆中构造一个相应的内存,提供访问入口。

2、验证

1. 文件格式的校验:验证.class文件是否能被当前jvm识别,主要包括魔数、主版本号、常量池的校验信息。
2. 元数据校验:对字节码信息进行语义校验,比如一些特性,继承、多态、final不可重写等等是否满足java规范要求。
3. 字节码校验:对数据流和控制流进行分析,保证语义是合法符合逻辑的,不会危害虚拟机的的行为。
4. 符号引用验证:虚拟机将符号转化为直接引用,主要对类自身以外的信息进行校验,确保解析动作顺利完成    

3、准备

这个阶段主要是为类变量分配空间和设置初始值。
类static变量会直接分配内存到方法区,和方法区的类结构属于同一水平,其它实例变量不会直接分配内存,分配内存是在堆上分配,但是会赋初始值,比如int型默认初始值为0,long型默认对象对0l,boolean型默认初始值为false,就算是在定义的时候给了初始值,在该阶段也不会被赋予,比如int i = 9,这个阶段的i依然被初始化为0,真正有值的情况一定是在堆上分配空间之后才会有。

4、解析

将虚拟机常量池中的符号引用转化为直接引用的过程。
什么是符号引用:就是我们定义的变量名,符合命名规范即可。
什么是直接引用:就是堆中内存分配的实际地址。解析动作就是将方法区中的符号引用转换成堆中的具体内存地址。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符。

5、初始化

这里主要是调用了类构造器的()方法的过程,具体动作主要是对类变量设置初始值。初始化步骤主要包括:
1. 检查该类有没有被加载和连接,如果没有则先加载和连接该类
2. 如果该类的父类还没有被初始化,则先初始化该父类
3. 依次执行初始化语句
初始化过程


public class A {

    static {
        System.out.println("static A");

    }
    public A(){
        System.out.println("init A");
    }
}

public class B extends A {

    static {
        System.out.println("static B");
    }
    public B(){
        System.out.println("init B");
    }

    public static void main(String[] args) {
            new B();
    }
}
static A
static B
init A
init B

初始化类的前提条件主要包括:
1. 创建实例,new方法
2. 反射
3. 调用类的静态方法
4. 调用某个类的接口或者静态变量
5. Java指定的启动类

更多精彩内容我们下期再见
公众号:计算机基础爱好者

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值