jvm内存布局(jdk8)
前置知识
类加载流程
- 加载:加载二进制数据生成class实例
- 链接(重要)
- 验证: 验证有效性包括格式、元数据、符号引用有效性等
- 准备: 静态变量初始化值,jvm赋值
- 解析(重要): 将符号引用转化为直接引用,比如局部变量表的reference可以确定具体指向的堆内存地址,还有确定具体方法调用的版本。 如果无法解析时确定的方法分派,则后续在首次使用时通过栈帧进行动态链接。
- 初始化:** **主要执行构造方法和程序员自己设置的初始化赋值
方法调用涉及的概念
在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制有关
方法的静态链接与动态链接
- 静态链接:当一个字节码文件被装在进JVM内存时,如果被调用的目标方法在编译期可知,且运行期保持不变时,这种情况下将调用方法的符号引用转换为直接而引用的过程称之为静态链接。 例如,super()方法。静态链接可以发生在类加载的解析阶段。
- 动态链接:如果被调用的方法无法在编译期确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程中具备动态性,因此也被称之为动态链接。对应着接口回调,多态动态绑定等
与之对应的则是方法的绑定机制。早期绑定(Early Binding)和晚期绑定(late Binding)。绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,这里仅仅发生一次。
- 早期绑定:早期绑定就是被调用的目标函数如果在编译期可知,且运行期间保持不变,即可将这个方法与所属的类型进行绑定。
- 晚期绑定:如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型确定相关的方法,被称之为晚期绑定。其实也就是动态绑定
方法的静态分派与动态分派。分派主要指确定具体调用的方法版本。在java中一般我们把首次类加载初始化之前的流程也认为是编译期间。
- 静态分派:编译期间根据变量的静态类型可以确定方法,例如方法重载
- 动态分派:根据变量的动态类型确定具体调用的方法,例如重载
tips: 分派 vs 链接
分派是编程语言中的术语,其他编程语言也可以用,概念上比链接更加粗一些。而链接主要是java中使用的术语。本质上是差不多的,目的都是为了确定方法调用的版本。链接由于是java中的概念,所以含义会更加具体,链接包含将符号引用转化为直接应用的过程。
虚方法和非虚方法
- 应着进行早期绑定和静态链接的定义,即在编译期就确定了具体的调用版本,在运行时不可变,称之为非虚方法
- 静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法
- 其他方法称之为虚方法
- 子类对象的多态性使用的前提为:类的继承关系,方法的重写
- 可以简单的理解为自己写的方法就是虚方法。
- 在面向对象的编程中,会很频繁的使用到动态分派,如果在每次动态分派的过程中都要重新在类的方法元数据中搜索合适的目标的话可能影响到执行效率,因此,为了提高性能,JVM采用在类的方法区建立一个虚方法表(virtual method table)(非虚方法不会出现在表中)来实现,使用索引表来替代查找
- 每个类中都有一个虚方法表,表中存放着各个方法的实际入口
- 虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成以后,JVM会把该类的方法表也初始化完毕
方法调用的字节码指令
- 普通调用指令
- invokestatic:调用静态方法,解析阶段确定唯一方法版本
- invokesopecial:调用
方法、私有即父类犯法,解析阶段确定唯一方法版本 - invokevirtual:调用所有虚方法
- invokeinterface:调用接口方法
- 动态调用指令:
- invokedynamic:动态解析出所有需要的方法,然后执行,(lamble表达式),和python一样,变量不需要自己执行,运行时才知道
常量池基本概念
类常量池/静态常量池/Constant Pool
这些信息可以直接保存在字节码中,占用空间也不大。因为具体的内存地址得等到类加载之后才确定,这边的引用都是符号引用,在类加载的解析阶段会转成内存地址的直接引用。
字符串常量池
在类加载完成经过验证准备阶段之后,在堆空间生成的一块内存区域,保存的是字符串常量的引用。会配合运行时常量池工作。
运行时常量池
类加载的解析阶段涉及将符号引用转化为直接引用。类方法字段的直接引用信息全部存放在运行时常量池,参考下图。每一个栈帧都包含一个 指向运行时常量池的引用 ,持有这个引用是为了支持方法调用过程中的**动态连接。**一些多态的方法我们只有在运行时再将符号引用关联成运行时常量池中的直接引用。运行时常量池既包含符号引用也包含直接引用。符号引用转化成直接引用后就不再转回去了。
tips: 运行时常量池中涉及字符串引用的,会优先从字符串常量池中获取引用地址
内存布局图
- 本地方法栈:native方法调用的栈
- 程序计数器:区别CPU中的程序计数器寄存器硬件,这个内存区域记录字节码执行的行号,指示下一条要执行的字节码指令
- 虚拟机栈:生命周期和线程相同,当方法/函数被执行时会创建栈帧入栈,按照FILO的方式执行函数。
- 局部变量表: 定义成数字数组,保存局部变量值和对象引用,大小编译区确定,方法调用结束后销毁
- 运行时常量池引用(constant pool reference): 这个栈帧中的对象有些文章称之为动态连接其实不太合理。本质这个对象是个指向运行时常量池的引用,它支持方法进行动态连接,但是不能称这个引用为动态连接。之所以有运行时常量池引用是因为像多态的方法,在非运行时无法确定。运行时常量池引用就是一个指向运行时常量池的引用,叫他constant pool reference更合理。
- 操作数栈: 生成字节码指令后,需要对局部变量值进行计算时会利用操作数栈完成计算
- 返回地址
- 堆:分代管理
- MetaSpace: 主要是类方法字段元信息(这些就是类常量池)和运行时常量池
- CodeCache: JVM代码缓存是JVM将其字节码存储为本机代码的区域.实时(JIT)编译器是代码缓存区域的最大消费者
- Direct Memory: jvm可以申请堆外内存
参考资料
- [微信公众号] 17张图带你了解,JVM 运行时数据区
- [微信公众号] JVM源码分析之Metaspace解密
- class常量池、字符串常量池和运行时常量池的区别
- 解析与分派
- 栈帧的内部结构–动态链接 (Dynamic Linking)
- [微信公众号] Java虚拟机的多态性实现机制和原理:静态分派与动态分派
- [微信公众号] 面试官:说一下类加载的过程(10张图解)
- JVM里的符号引用如何存储?(R大回复)