运行时数据区简单介绍(上)

本文深入探讨JVM1.8之后的内存布局,包括程序计数器、虚拟机栈、方法区等关键区域的功能与作用。特别分析了栈帧结构、局部变量表、操作数栈等细节,以及方法区的实现变迁。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

JVM1.8之后的内存布局:

 

 

JDK8 之前的内存区域图如下:

 

本文先介绍JVM内存五大部分的程序计数器虚拟机栈和方法区,其它三部分下篇详细介绍。

我们把程序代码抽象一下,可以理解为由三个部分组成,分别是数据、指令、控制流,所谓数据,可以理解为定义的成员变量,静态变量,常量;指令理解为在方法中执行的语句,控制流理解为分支、循环、跳转、异常处理、线程恢复等。

程序计数器

程序计数器是一块较小的内存空间,指的是当前线所执行的字节码的行号指示器。这是比较官方的解释,通俗一点来说,首先程序计数器是与线程绑定的,也就是说每个线程在运行期都有独立的程序计数器,在上图中也可以看出,程序计数器是属于线程隔离的数据区。我们在学习Java多线程的时候讲过,多线程是通过线程间切换抢夺 CPU分配的时间片来竞争执行的(多核CPU来说指的是一个内核),一个 CPU 处理器只会在同一时间执行一条线程指令。举个例子,有两个线程A和B,当A线程获取到运行时间并执行到一半时,CPU分配的时间片用完了,此时 A 线程就要被挂起,然后两个线程再次竞争下一次的 CPU 时间片,因此 A 线程就需要一个计数器来记录上次执行的位置,好让下次再获取到CPU时间片时可以恢复到正确位置继续执行下去。各个线程之间的计数器是独立的,互不影响,独立存储,如果线程在执行一个方法时,此线程记录的是正在执行的字节码指令的地址,如果执行的是本地(Native)方法,则计数器的值为空(undefined),由于程序计数器的内存空间非常小,所以 JVM 规范中没有规定此区域的内存溢出的情况。

Java虚拟机栈

Java虚拟机栈也是线程独立的,多个线程有独立的Java虚拟机栈,栈的生命周期与线程相同,在线程启动时被创建,线程结束时被销毁,栈是用来存储Java方法运行时数据的,那栈中存储的数据是什么方式来组织的呢?其实在栈中存储的数据结构是一个数据单位来体现的,这个数据单位称为栈帧(Stack Frame),当程序执行一个方法时,会创建一个栈帧,我们称为入栈,当方法执行结束后,栈帧就会被销毁,我们称为出栈。在一个栈帧里,用于存储局部变量表、操作数栈、动态链接、方法出口和一些额外的附加信息。也许你会跟我一样,栈帧里的这些东东是什么鬼?不要捉急,下面我们将详细介绍运行时栈帧的结构。前面我们说过,栈帧是虚拟机在方法调用执行时存储在虚拟机栈的数据结构,也可以称为栈元素,一个方法对应一个栈帧,一个栈帧概念结构如下图所示。

 

 

栈数据结构是先进后出,当前正在运行的线程所在的位置称为栈顶,多个线程拥有各自独立的虚拟机栈,在一个栈帧里面,接下来详细讲解一下栈帧中的局部变量表、操作数栈、动态连接、返回地址等部分的作用和数据结构。

局部变量表

可以理解为是一组变量值的存储空间,目的是为了存放方法参数和方法内部定义的局部变量。当程序被编译成Class文件时,该方法会有一个Code属性的max_locals数据项来确定该方法所需要分配的局部变量表的最大容量,所以,栈帧中需要多大的局部变量表,在编译后就已经确定了,并且在程序运行期变量表的容量不用改变

操作数栈

是一个先入后出,同局部变量表一样,操作数栈的最大深度也在编译的时候写到方法的Code属性的max_stacks数据项中,操作数栈可以理解为正在操作中需要处理的数据和结果,看个例子哈,很简单的两数相加操作,加法的字节码指令是iadd,在运行的时候操作数栈中最接近栈顶的两个元素例如已经存入了两个int型的数值,执行iadd指令时,会将这两个int值出栈并相加,然后将相加的结果入栈。

动态连接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,有这个引用是为了支持方法调用过程中的动态连接,因为Class文件的常量池有很多符号引用,这些符号有一部分将在每一次运行期转化为直接引用,称为动态连接,还有一部分是在类加载或第一次使用时转化为直接引用,称为静态解析。说白了,动态连接,就是通过符号的方式来引用常量池。

方法返回地址

记录着该方法要返回到被调用的位置(通过地址来记录),我们知道方法结束有两种方式,一种是方法内部执行时遇到任意一个返回的字节码指令,这时候可能有返回值要传递给上层方法的调用者,就是调用当前方法的方法;另一种结束方式是在执行的过程中遇到了异常,并且没有在方法体内进行处理,也就是没有使用try...catch语句,此时在本方法中维护的异常表没有搜索到匹配的异常处理器,就会导致方法退出,而这种退出为异常完成出口,就不会给上层调用者返回任何值。所以方法返回地址就是用于记录调用者在哪里,以便于可以正常回到调用者的位置上。

方法区

方法区(Method Area)与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。

Java 虚拟机规范对方法区的限制非常宽松,除了和 Java 堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。垃圾收集行为在这个区域是比较少出现的,其内存回收目标主要是针对常量池的回收和对类型的卸载。当方法区无法满足内存分配需求时,将抛出 OutOfMemoryError 异常。

JDK8 之前,Hotspot 中方法区的实现是永久代(Perm),JDK8 开始使用元空间(Metaspace),以前永久代所有内容的字符串常量移至堆内存,其他内容移至元空间,元空间直接在本地内存分配。

为什么要使用元空间取代永久代的实现?

1.字符串存在永久代中,容易出现性能问题和内存溢出。

2.类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。

永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。

运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

一般来说,除了保存 Class 文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。

运行时常量池相对于 Class 文件常量池的另外一个重要特征是具备动态性,Java 语言并不要求常量一定只有编译期才能产生,也就是并非预置入 Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是 String 类的 intern() 方法。

既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。

下面来写点小程序玩玩一下

 

public class TestIPulsPlus {

    public static void main(String[] args) {

        int i = 8;

        i = i++;

        System.out.println(i);

    }

}

输出打印结果:

为什么结果是8,解释一下原因:

先看下编译后的字节码

 

可以直接点击指令,到官网查看指令的意思

1  Bipush 8 是表示把8压栈

2  istore_1 表示把压栈的8弹出,赋值给局部变量表为1的变量,也就是i,这里的第0个局部变量是args,所有第一个局部变量是i.

3 iload_1 把局部变量为1的变量i 压栈。也就是把8压栈。

4 iinc 1 by 1  ,把局部变量表中为1 的i变量加1,也就是把8加1 = 9

5 istore_1 表示把之前压栈局部变量i的值8弹出,赋值给i

6 执行打印输出 i的值,所以最后结果是8.

最后在总结一下:

Bipush 8  表示把8压栈

Istore_1  表示把8弹出,赋值给i

这两条指令等于 int i = 8;

iload_1 表示把i的值8压栈

iinc 1 by 1 这里是在局部变量表中完成的相加,表示把i的值8加1 ,注意:这里是在栈外面执行的操作

istore_1 表示把i的值8弹出,赋值给局部变量表i

这三条指令等于 i = i++;

 

再看一个例子

public class TestIPulsPlus {

    public static void main(String[] args) {

        int i = 8;

        i = ++i;

        System.out.println(i);

    }

}

执行结果:9

字节码:

1 bipush8  把8 压栈

2 istore_1  把8出栈赋值给i

i=i+8

3 iinc 1 by 1 局部变量表中把i的值8+1 = 9

4 iload_1 把i的值9压栈

5 istore_1 表示把i的值9弹出,赋值给局部变量表i

i= ++i;

所以最后打印的是9;

 

注意,如果是非static方法,局部变量表第一个变量是this

 

public class Hello_03 {

    public static void main(String[] args) {

        Hello_03 h = new Hello_03();

        int i = h.m1();

    }

 

    public int m1() {

        return 100;

    }

}

 

字节码:

有关指令这块,后续总结再详细讲解

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值