JVM运行时数据区

前言

  我们都知道Java语言有一个很重要的特性——跨平台,即同一个程序可以在不同的平台运行,其跨平台的核心就是JVM。JVM就像一个平台与Java程序之间的适配器,官方通过为我们提供不同平台适配器(JDK或JRE),达到Java程序的跨平台,为此我们学习Java,就需要了解JVM,今天主要聊聊JVM运行时数据区。

JVM运行时数据区

​  JVM运行时数据区分为方法区、虚拟机栈、本地方法栈、堆、程序计数器。其中虚拟机栈、本地方法栈、程序计数器为线程私有,而方法区、堆为所有线程共享,如图所示(图片来自于《深入理解Java虚拟机》):
在这里插入图片描述

​  不同的数据区域有不同的功能,有些区域随着JVM进程的启动而存在,有些则随着线程的生命周期创建与销毁,要想知道为什么划分为线程私有或者线程共有,则需要了解各个区域的功能。

1. 程序计数器

​  我们知道线程是CPU调度的一个基本单位,一个CPU在任意一个时刻只会执行一条线程中的指令(指令并非代码,一条代码可以包括多条指令),而由于CPU会在多个线程之间来回切换,那么就需要保证在线程切换后可以恢复到正确的执行位置,这个就是程序计数器的功能,我们可以把程序计数器看做是当前线程所执行的字节码行号指示器, CPU在切换后可以通过程序计数器恢复到正确的执行位置。我们编码过程中常用的逻辑,如分支、循环、跳转、异常处理等功能都是依赖程序计数器完成,而由于程序计数器记录的是当前线程的字节码行号指示器,因此就需要各个线程独立存储,互不影响,因此程序计数器必须是线程私有的。

2. 虚拟机栈

​  虚拟机栈也就是我们常说的栈空间,也是线程私有的,生命周期与线程相同。举个例子,我们都知道方法内部的局部变量是互不干扰的,这就需要在方法层面进行数据隔离,由此引入了栈帧的概念,即每个方法在执行的同时会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,如图所示:

2.1 局部变量表

​  局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,不是对象本身,可以是指向对象其实地址的引用指针)。局部变量表所需的内存空间在编译期内完成分配,方法运行期间不会改变局部变量表的大小。假设有一个方法定义:

	public void test(int i, float j, long x, double y, Object o) {
        int z = 3;
        return;
    }

​ 其局部变量表的情况如下表所示:

槽位(slot)类型字段
1inti
2floatj
3longx
5doubley
7referenceo
8Intz

​  由于long和double类型的数据会占用2个字节,所以其槽位值加2。上述的表格可以通过命令javap -v Main.class进行查看,如果查看不到,则用javac -g Main.java命令重新进行编译再进行查看,结果如图所示:

可以看到slot为0的存放的是this指针,其他局部变量的定义实际是从1开始。

2.2 操作数栈

​  仅仅有局部变量表是不够的,程序需要进行数值上的运算,因此就引入操作数栈,与局部变量表不同的是操作数栈是通过入栈、出栈的方式访问,而局部变量表是通过槽位进行访问。两者结合达到计算的目的,举个简单的例子:

public class Main {

    public static void main(String[] args) {
        Main main = new Main();
        main.test();
    }

    public void test() {
        int x = 10+3;
        int y = x + 3;
        int z = x +5;
        int h = x + y + z;

    }

}

局部变量表为:

test函数指令如下(可以通过命令javap -c Main.class查看):

每条指令的功能如下表所示:

行数指令作用
0bipush 13将13压入栈顶,从此处也可以看出10+3在编译期间已进行优化
2istore_1从栈顶弹出,并压入局部变量表slot为1的位置,即赋值给x(13)
3iload_1从局部变量表槽位为1的数据压栈,即将x的值(13)压栈
4iconst_3将数值3进行压栈(0~5使用的指令是iconst)
5iadd将栈顶的两个数(即13和3)弹出并进行加法运算,计算后的结果(16)会再次入栈
6istore_2栈顶弹出,并压入局部变量表slot为2的位置,即赋值给y(16)
7Iload_1从局部变量表槽位为1的数据压栈,即将x的值(13)压栈
8iconst_5将数值5进行压栈(0~5使用的指令是iconst)
9iadd将栈顶的两个数(即13和5)弹出并进行加法运算,计算后的结果(16)会再次入栈
10Istore_3栈顶弹出,并压入局部变量表slot为3的位置,即赋值给z(18)
11iload_1从局部变量表槽位为1的数据压栈,即将x的值(13)压栈
12iload_2从局部变量表槽位为2数据压栈,即将y的值(16)压栈
13iadd将栈顶的两个数(即13和16)弹出并进行加法运算,计算后的结果(29)会再次入栈
14iload_3从局部变量表槽位为3数据压栈,即将z的值(18)压栈
15iadd将栈顶的两个数(即29和18)弹出并进行加法运算,计算后的结果(47)会再次入栈
16istore 4栈顶弹出,并压入局部变量表slot为4的位置,即赋值给h(47)
18return函数返回
2.3 动态链接

​  每个栈帧都会包含一个指向该帧所属方法的引用,使用这个引用是为了支持动态链接。我们都知道,Java语言的多态性,子类可以重写父类的方法,因此如果是使用父类引用指向子类对象,则需要正确的定位到访问的子类方法中,这就是动态链接的作用。有动态链接就得说明下静态链接,静态链接指的是在编译时已经确定调用的方法,如所有的private方法,静态方法,构造器等,而上述的动态链接由于不能在编译器确定,因此需要在运行时进行绑定。

2.4 方法出口

​  在Java中,一个方法的退出只能有两种方式:return或者抛出异常。return可以是有返回值也可以是无返回值的,但是如果是异常结束,则调用方是获取不到返回值的。

​  返回值并不是方法出口的关键点,无论哪种方法退出,需要考虑的都是调用方如何恢复,其实正如上述所说,一个栈帧对应的是一个方法,当A方法调用B方法时,实际上就是重新创建一个栈帧进行入栈,在B方法退出后,则直接将最顶的栈帧弹出,此时当前的方法即为A方法,还需要恢复其局部变量表和操作数栈等,并将B方法的返回值压入操作数栈。

2.5 栈溢出

​  按照上面所述,一个栈帧对应一个方法,而每个栈帧会保存各自的信息,但是系统资源是有限的,对于每个线程的最大栈帧也是有限制的,超过这个限制就会抛出StackOverflowError异常,简单的例子就是错误的递归调用,如

    public void test() {
        test();
    }
3. 本地方法栈

​  本地方法栈与虚拟机栈类似,区别在于本地方法栈执行的是Native方法。

4. 堆

​  堆区是我们最常见的一块,是所有线程共享的区域。Java程序中大部分的对象都是通过堆区进行分配的,比如我们常规使用一个对象都是用new关键字,所以堆区也别称为”GC堆“。

​  堆区是垃圾回收的重点,可以将其分为新生代和老年代,新生代又可以分为Eden区、Survivor区(包括2个,From Survivor区域和To Survivor区域),本文不细讲垃圾回收机制。当堆区没有足够的内存分配时,则会抛出OutOfMemoryError异常。

4.1 对象栈上分配

​  并不是所有的对象都是在堆区进行分配,对于一些小对象,且JVM通过逃逸分析确定该对象不会被外部访问,这样就会将该对象在栈上进行分配,之后也就随着栈帧的出栈而消亡(逃逸分析可以通过-XX:-DoEscapeAnalysis进行关闭),而且如果该对象不会逃逸,且对象较小,则JVM不会进行对象的创建,而是通过标量替换的方式将该对象分解为多个成员变量进行代替(标量指的是int、long、double等基本数据类型)

5. 方法区

​  方法区与堆区一样,也是所有线程共享的,用于存储类信息、常量、静态变量等。方法区也需要进行垃圾回收,当没有足够的空间进行内存分配时,也会抛出OutOfMemoryError异常

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值