JVM学习笔记-运行时数据区域

之前在学习和工作的过程中,一直有听过JVM在运行时会把不同的数据存放在不同的区域,比如堆、栈、程序计数器之类的概念。但对于这些概念只是停留于听过有个印象而已。这次专门学习了JVM的一些内容,也在这里做一个笔记,记录一下这个知识点。

总览

先放一张图

可以看到,这里JVM把运行时数据区,分为上面这些。下面将对这些区域分别进行介绍。

程序计数器

一开始对程序计数器这个词是有些迷惑的。不知道程序为什么需要弄一个计数器,有什么用。但程序计数器,实际上是用来存储下一条指令所在单元的地址的。我们知道程序都是按照顺序一条条执行下去的。那当CPU执行完这条指令后,它怎么知道接下来执行什么指令呢?这就需要程序计数器来告诉它,下一条指令在哪。由于大部分的指令都是按顺序来执行的,所以程序计数器这个名字就这么来了,大多数情况下,找到下一条指令的操作都是对程序计数器简单的+1。但是,总会有例外的情况,当遇到转移指令(比如JMP)的时候,程序计数器的内容则必须从指令寄存器中的地址字段取得,这种情况下,下一条从内存中取出的指令将由转移指令来决定,而不是简单的+1。所以,程序计数器的结构应该是具有寄存器和计数两种功能的结构。

上面是对程序计数器的一个通用描述,在Java中,程序计数器可以看作是当前线程所执行的字节码的行号指示器。在JVM中,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是native方法,那么这个计数器的值则应为空。

虚拟机栈

虚拟机栈描述的是Java方法执行的线程内存模型。它是线程私有的,生命周期与线程相同。也就是说,一个线程对应着一个虚拟机栈。当每个方法被执行的时候,JVM都会同步创建一个栈帧用来存储局部变量表、操作数栈、动态连接、方法出口等信息。每个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

这里介绍一下栈帧。我个人的理解,栈帧有点像一个针对方法的压缩包。这个方法执行需要的东西,都会被打包在一起,这个打包后的产物,就被叫做栈帧。栈帧的局部变量表中,存放了编译器可知的基本数据类型、对象引用、returnAddress类型。这些类型在局部变量表中,以局部变量槽表示。64位的long和double会占用2个变量槽,其余类型则占用一个。局部变量表的内存空间在编译期间就完成了分配,当进入一个方法的时候,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,方法运行期间不会改变局部变量表的大小。

在JVM规范中,对这个内存区域规定了两类异常状况:

1.线程请求的深度大于虚拟机允许的深度,会抛出StackOverflowError异常。

2.如果JVM栈容量可以动态扩展,当扩展时无法申请到足够内存,会抛出OutOfMemoryError异常。

我们现在常用的HotSpot虚拟机的栈容量是不支持动态扩展的,也就是说,HotSpot虚拟机不会因为虚拟机栈无法扩展而导致OOM。但如果申请时就失败,则还是会抛出OOM。一个是申请时失败,一个是扩展时失败,这两个区别需要注意。

本地方法栈

本地方法栈与上面的虚拟机栈类似,区别在于虚拟机栈是为执行Java方法(也就是字节码)服务,而本地方法栈是为执行native方法服务。

堆是JVM所管理的内存中最大的一块,也是被所有线程共享的一块区域,在JVM启动时创建。堆的作用就是存放对象实例,几乎所有的对象实例都在这里分配内存。至于这里为什么要说几乎,则是因为随着Java语言的发展,由于即时编译技术的进步,以及日后可能出现的值类型的支持,栈上分配、标量替换等优化手段已经导致了一些微妙的变化,所以Java对象实例都分配在堆上这个描述,也已经变得不那么绝对了。堆也是垃圾收集器管理的内存区域,所以也被称为GC堆。

方法区

方法区与堆一样,是各个线程共享的内存区域,用来存储已经被JVM加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。在JVM规范中,方法区被描述为堆的一个逻辑部分,但是它还有一个别名叫做“非堆”(Non-Heap),目的是与堆区分开来。

这里要提一下“永久代”这个概念。这个概念是垃圾回收器分代收集理论所创造出来的。其他的还有诸如新生代、老年代、Eden空间等等。在JDK 8以前,很多人会把永久代和方法区混为一谈,但本质上这两个概念并不是等价的,只是因为当时的HotSpot虚拟机选择把垃圾收集器的分代设计扩展至方法区,或者说用永久代来实现方法区而已。但是对于非HotSpot虚拟机,比如一些不基于分代收集理论的虚拟机,就没有永久代这么个概念。所以说对于方法区的实现,不同的虚拟机会有不同的做法。现在看来,使用永久代来实现方法区,可能并不是一个好主意,它会导致更容易遇到内存溢出的问题,因为永久代有大小上限,即使不设置也会有一个默认大小。所以在JDK 6的时候,HotSpot就开始准备放弃永久代,逐步改为采用本地内存来实现方法区。到了JDK 8,终于完全废弃了永久代的概念,改为在本地内存中实现的元空间来代替。

运行时常量池

运行时常量池是方法区的一部分。在Class文件中,有一项信息是常量池表,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

JVM规范对于运行时常量池没有做任何细节的要求,不同的虚拟机可以按照自己的需要来实现这个内存区域,不过一般来说,除了保存Class文件中描述的符号引用外,还会把由符号引用翻译出来的直接引用也存储在运行时常量池中。

运行时常量池相对于Class文件常量池,具备动态性。也就是说,并非预置入Class文件中常量池的内容才能进入运行时常量池,运行期间也可以将新的常量放入池中,用的比较多的是String类的intern方法。它会检查当前字符串在常量池中是否存在,如果存在则会直接返回,如果不存在,则会在常量池创建后再返回。

当常量池无法再申请到内存时,会抛出OOM异常。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值