1.class文件从类加载过程到卸载的5个阶段:
加载 ➡ 链接(验证、准备、解析) ➡ 初始化(使用前准备) ➡ 使用 ➡ 卸载
(1):加载
- 首先根据类的全类名获取定义此类的二进制字节流。
- 并将字节流所代表的静态存储结构转换为特定的运行时数据结构。
- 最后在堆中生成一个代表这个类的Class实例对象。
加载过程会校验cafe babe魔法数,常量池,文件长度,是否有父类等
(2): 连接(Linking)
- 验证:确保被加载类的正确性
-
文件格式验证:这里验证结束后这段字节流才被允许进入Java虚拟机内存的方法区中进行存储,后面三步是在此基础上验证的
-
元数据验证:除了java.lang.Object之外,所有的类都应当有父类就是这里验证的
-
字节码验证:保证不会出现类似于“在操作 栈放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中”这样的情况。
-
符号引用验证:可访问性(private、protected、public、)是否可被当前类访问。
-
- 准备:类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初 始值的阶段
- 解析:将常量池内的符号引用替换为直接引用
(3): 初始化
(1)类什么时候才被初始化
- 创建类的实例,也就是new一个对象
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射
- 初始化一个类的子类(会首先初始化子类的父类)
- JVM启动时标明的启动类,即文件名和类名相同的那个类
(2)类的初始化顺序
- 如果这个类还没有被加载和链接,那先进行加载和链接
- 假如这个类存在直接父类,并且这个类还没有被初始化(注意:在一个类加载器中,类只能初始化一次),那就初始化直接的父类(不适用于接口)
- 加入类中存在初始化语句(如static变量和static块),那就依次执行这些初始化语句。
- 总的来说,初始化顺序依次是:(静态变量、静态初始化块)–>(变量、初始化块)–> 构造器;
- 如果有父类,则顺序是:父类static方法 –> 子类static方法 –> 父类构造方法- -> 子类构造方法
4、类的加载
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个这个类的java.lang.Class对象,用来封装类在方法区类的对象。如:
2. JVM运行时数据区:
class文件读取到加载到JVM运行时数据区图示如下:
不同版本之间划分的区别:
程序计数器
每个执行线程都有自己私有的计数器, 记录下一条需要执行的字节码指令, 多线程切换时,当切换回上一个执行的线程时, 我们需要知道上一次执行到什么位置了。如果正在执行的是Native方法,这个计数器值则为空(Undefined), 这是在Java虚拟机规范中唯一一个不会发生OutOfMemoryError情况的区域。
虚拟机栈:
和程序计数区一样属于线程私有,它的生命周期与线程相同。java虚拟机栈描述的是java方法执行的内存模型: 每一个方法在执行会在虚拟机栈中创建一个 栈帧 ,一个栈帧代表一个方法:
它是一个后进先出的栈,方法1调用方法2,方法2调用方法3,方法3调用方法4,一个栈帧中代表一个方法,存储顺序如图:
局部变量表:
基本数据类型(boolean、byte、char、short、int、float、long、double)。
对象引用(reference类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。
returnAddress类型(指向了一条字节码指令的地址)。
操作数栈:
常被称为操作栈,它是一个后入先出(LIFO)栈。同局部变量表一样,操作数栈的最大深度也在编译的时候被写入到Code属性的max_ stacks数据项之中,根据字节码执行的指令, 往栈中写入或取出数据, 即入栈/出栈。字节码指令将正在执行的指令压入操作数栈,其他指令有从操作数栈中取出此值对此值进行操作之后重新放入此栈中。可以进行的操作有:复制,交换,运算。
对以下代码解析记录它的执行时操作数栈的变化:
以下是使用 idea 的 jclasslib 插件 编译后的截图:
1> 如下图: 当执行指令地址0时,此时 程序寄存器 存储的是当前执行的 0, 而iconst_1操作指令根据 JVM字节码对照表 是 将int型1推送至栈顶。
2> 如下图:当执行到指令地址1时,此时 程序计数器 存储的是当前执行的 1,而istore_0操作指令根据 JVM字节码对照表 是 将栈顶int型数值存入第一个本地变量(局部变量表)。
3> 如下图:当执行到指令地址2时,此时 程序计数器 存储的是当前执行的 2,而iconst_3操作指令根据 JVM字节码对照表 是 将int型3推送至栈顶。
4> 如下图:当执行到指令地址3时,此时 程序计数器 存储的是当前执行的 3,而istore_1操作指令根据 JVM字节码对照表 是 将栈顶int型数值存入第二个本地变量(局部变量表)。
5> 如下图:当执行到指令地址4时,此时 程序计数器 存储的是当前执行的 4,而iload_0操作指令根据 JVM字节码对照表 是 将第一个int型本地变量(局部变量表)推送至栈顶(操作数栈)。
6> 如下图:当执行到指令地址5时,此时 程序计数器 存储的是当前执行的 5,而iload_1操作指令根据 JVM字节码对照表 是 将第二个int型本地变量(局部变量表)推送至栈顶(操作数栈)。
7> 如下图:当执行到指令地址6时,此时 程序计数器 存储的是当前执行的 6,而iload_1操作指令根据 JVM字节码对照表 是 将栈顶两int型数值相加并将结果压入栈顶(操作数栈)。
8> 如下图:当执行到指令地址7时,此时 程序计数器 存储的是当前执行的 7,而istore_2操作指令根据 JVM字节码对照表 是 将栈顶int型数值存入第三个本地变量(局部变量表)。
9> 如下图:当执行到指令地址8时,此时 程序计数器 存储的是当前执行的 8,而return操作指令根据 JVM字节码对照表 是 从当前方法返回void。
动态连接:
方法出口(正常出口,异常出口):
在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。 |
本地方法栈:
本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务
方法区:
与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”。
堆:
是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配,但是随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换[插图]优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。