玉林社区   玉林天天茶座   生活常识JVM 运行时数据区域,书中没有说清
返回列表
查看: 424|回复: 0

生活常识JVM 运行时数据区域,书中没有说清

[复制链接]

1590

主题

1590

帖子

5790

积分

论坛元老

Rank: 8Rank: 8

积分
5790
发表于 2022-2-27 12:55:32 | 显示全部楼层 |阅读模式

马上注册玉林红豆网会员,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有帐号?立即注册

x

数据库系列吭哧吭哧写得差不多了,准备寒假看完JVM,然后开学来看看框架背背八股就准备秋招了。话不多说,JVM第一个知识点必定要奉献给J程序运行时的数据区域划分。[url=http:///www.wangsu.com/]IPv6[/url]的最新消息可以到我们平台网站了解一下,也可以咨询客服人员进行详细的解答![align=center]

                               
登录/注册后可看大图
[/align]


本文转载自微信「飞天小牛肉」,作者小牛肉。转载本文请联系飞天小牛肉。


数据库系列吭哧吭哧写得差不多了,准备寒假看完JVM,然后开学来看看框架背背八股就准备秋招了。话不多说,JVM第一个知识点必定要奉献给J程序运行时的数据区域划分。


老规矩,背诵版在文末。点击阅读原文可以直达我收录整理的各大厂面试真题


JVM运行时数据区域总览
JVM在执行J程序的过程中(简称运行时)会把它所管理的内存划分为若干个不同的数据区域。这些区域有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而一直存在,有些区域则是依赖用户线程的启动和结束而建立和销毁。


根据《J虚拟机规范》的规定,J虚拟机所管理的内存将会包括以下几个运行时数据区域,如下图所示:





从图中可以看到,线程共享的区域是方法区和堆,线程隔离(线程私有)的区域是虚拟机栈、本地方法栈和程序计数器。


简单解释下线程共享和线程私有是啥意思:



所谓线程私有,通俗来说就是每个线程都会创建一个属于自己的空间,每个线程之间的这块私有空间互不影响,独立存储。比如程序计数器就是线程私有的,每个线程都会拥有一个属于自己的程序计数器,互不干涉。
线程共享就没啥好说的,简单理解为公共场所,谁都能去,存储的数据所有线程都能访问。

下面我们来分别解释下这几个数据区域??


线程私有:程序计数器PCR
程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。


由于J虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。


因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各个线程之间的计数器互不影响。


那么程序计数器里存的到底是什么东西呢?



如果线程正在执行的是一个J方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址
如果正在执行的是本地(N)方法,这个计数器值则应为空(U)。至于什么是N方法,在本地方法栈那一小节会详细解释

注意!此内存区域是唯一一个在《J虚拟机规范》中没有规定任何OOME(内存溢出)情况的区域。这个问题也算是一个比较常见的面试题了


线程私有:虚拟机栈JVMS
虚拟机栈其是由一个一个的栈帧(SF)组成的,一个栈帧描述的就是一个J方法执行的内存模型。也就是说每个方法在执行的同时都会同步创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法的返回地址等信息。


每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。





其中,局部变量表存放了以下种类型的数据:



编译期可知的各种J虚拟机的基本数据类型:、、、、、、、
对象引用,类型:它并不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置。(关于类型,具体涉及到对象的访问定位的两种方式,使用句柄访问和使用直接指针访问,这个在后续文章中会详细介绍)
A类型:指向了一条字节码指令的地址

这些数据类型在局部变量表中的存储空间以局部变量槽(S)来表示,或者说局部变量表的基本存储单元是S,JVM为每一个S都分配了一个访问索引,通过这个索引就可以成功访问到局部变量表中存储的某个值。


其中,64位长度的和类型的数据会占用两个S,其余的数据类型都是2位只占用一个。


在《J虚拟机规范》中,对虚拟机栈这个内存区域规定了两类异常状况:



如果线程请求的栈深度大于虚拟机所允许的深度,将抛出SOE异常(栈溢出)
如果使用的JVM支持动态扩展虚拟机栈容量的话,当栈扩展时法申请到足够的内存就会抛出OOME异常(内存溢出)

线程私有:本地方法栈NMS
本地方法栈和上面我们所说的虚拟机栈作用基本一样,区别只不过是本地方法栈为虚拟机使用到的N方法服务,而虚拟机栈为虚拟机执行J方法(也就是字节码)服务。


这里解释一下N方法的概念,其不仅J,很多语言中都有这个概念。


AJ-


就是说一个N方法其就是一个接口,但是它的具体现是在外部由非J语言比如C或C++等来写的。J通过JNI来调用本地方法,而本地方法是以库文件的形式存放的(在WINDOWS平台上是DLL文件形式,在UNIX机器上是SO文件形式)。


所以同一个N方法,如果用不同的虚拟机去调用它,那么得到的结果和运行效率可能是不一样的,因为不同的虚拟机对于某个N方法都有自己的现,比如O类的C方法。


那么为什么需要N方法呢?


其主要原因就是J虽然使用起来很方便,但是有些层次的任务用J现起来不容易,或者对程序的效率有比较高的要求时,J语言可能并不是最好的选择。所以N方法使得J程序能够超越J运行时的界限,有效地扩充了JVM。


与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出SOE和OOME异常


线程共享:堆H
J堆是虚拟机所管理的内存中最大的一块。堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象例,几乎所有的对象例都在这里分配内存。


注意!这里我们用的是几乎,技术发展至今,其并非所有的对象例都会分配到堆上,比如逃逸技术,这个我们后续文章我再做解释~


堆是垃圾收集器管理的内存区域,因此一些资料中它也被称作GC堆(GCH)。


对于堆这个概念小伙伴们肯定还听说过各种诸如新生代、老年代、永久代、E空间、FS空间、TS空间等词,需要注意的是,这些区域划分仅仅是一部分垃圾收集器的共同特性或者说设计风格而已,只是为了通过这种分代设计来更好地回收内存,或者更地分配内存,而非某个J虚拟机具体现的固有内存布局,更不是《J虚拟机规范》里对J堆的进一步细致划分


根据《J虚拟机规范》的规定,J堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的,这点就像我们用磁盘空间去存储文件一样,并不要求每个文件都连续存放。但对于大对象(典型的如数组对象),多数虚拟机现出于现简单、存储高效的考虑,很可能会要求连续的内存空间。


J堆既可以被现成固定大小的,也可以是可扩展的,当前主流的J虚拟机都是按照可扩展来现的(通过参数-X和-X设定)


如果在堆中没有内存来完成对象例的分配,并且堆也法再扩展时,JVM就会抛出OOME异常


线程共享:方法区MA
方法区通俗点理解就是,在虚拟机完成类加载之后,存储这个类相关的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。


I--,,,它存储每个类的结构,如运行时的常量池、字段和方法数据,以及方法和构造函数的代码,包括类和例初始化和接口初始化中使用的特殊方法


举个简单的小例子:





方法区其本身很好理解,但是《J虚拟机规范》《深入理解J虚拟机》提到的一句话:方法区是堆的一个逻辑部分,真的是让我困惑了很长时间。


下面我来结合我的理解给大家解释下,我觉得这个方法区是堆的一个逻辑部分应该适用于JDK8以前,而不适用JDK8


先来看JDK8之前:





可以看到,JDK8之前,堆和方法区其是连在一起的,或者说,方法区就是堆的一部分。


但是呢,方法区存储的东西又有些特别,在过去自定义类加载器使用不普遍的时候,类几乎是静态的并且很少被卸载和回收,因此类也可以被看成永久的(这也就是永久代的含义),另外由于类作为JVM现的一部分,它们不由程序来创建,所以为了和堆区分开来呢,就给了方法区这样一个字用来存储类的信息,也有人把方法区称为非堆。


需要注意的是,论是方法区还是非堆,其都只是一个逻辑上的概念,在JDK8之前,其具体的现方法是永久代。


永久代是HS虚拟机给出的现,但是对于其他虚拟机现,譬如BEAJR、IBMJ9等来说,是不存在永久代的概念的。





永久代是一段连续的内存空间,我们在JVM启动之前可以通过设置-XX:MPS的值来控制永久代的大小,2位机器默认的永久代的大小为64M,64位的机器则为85M。


永久代的垃圾回收和老年代的垃圾回收是绑定的,一旦其中一个区域被占满,这两个区都要进行垃圾回收。


显然这种设计并不是一个好的主意,由于我们可以通过?XX:MPS设置永久代的大小,一旦类的元数据超过了设定的大小,程序就会耗尽内存,并出现内存溢出错误(OOMEG)。


而且有极少数的方法(例如适用S的()方法可以在运行过程中手动的将字符串添加到字符串常量池中,在JDK17之前的HS虚拟机中,字符串常量池被存储在永久代中)会因永久代的原因而导致不同虚拟机下有不同的表现


所以我们总结下HS在JDK8抛弃永久代,转而用元空间来现方法区的两大原因:



由于永久代的垃圾回收和老年代的垃圾回收是绑定的,一旦其中一个区域被占满,这两个区都要进行垃圾回收,增大了OOM发生的概率
有少数的方法例如S的()方法会因永久代的原因而导致不同虚拟机下有不同的表现,不利于代码迁移

那么元空间到底是个啥,和方法区有啥区别?


元空间与永久代之间最大的区别在于:元空间不再与堆连续,并且是存在于本地内存(N)中的。





运行时数据区域的对比如下图:





元空间存在于本地内存,意味着只要本地内存足够,它就不会OOM,不会出现像永久代中的OOMEG


运行时常量池RCP
运行时常量池是方法区的一部分。上面我们说过方法区包含类信息,而描述类信息的C文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(CPT),用于存放编译期生成的各种字面量(字面量相当于J语言层面常量的概念,如文本字符串,声明为的常量值等)与符号引用。有一些文章会把常量池表称为静态常量池。


都是常量池,常量池表和运行时常量池有啥关系吗?运行时常量池是干嘛的呢?


运行时常量池可以在运行期间将常量池表中的符号引用解析为直接引用。简单来说,常量池表就相当于一堆索引,运行时常量池根据这些索引来查找对应方法或字段所属的类型信息和称及描述符信息


为什么需要常量池这个东西呢?主要是为了避免频繁的创建和销毁对象而影响系统性能,其现了对象的共享。以字符串常量池为例,字符串S既然作为J中的一个类,那么它和其他的对象分配一样,需要耗费高昂的时间与空间代价,作为最基础最常用的数据类型,大量频繁的创建字符串,将会极大程度的影响程序的性能。为此,JVM为了提高性能和减少内存开销,在例化字符串常量的时候进行了一些优化:



为字符串开辟了一个字符串常量池SP,可以理解为缓存区
创建字符串常量时,首先检查字符串常量池中是否存在该字符串
若字符串常量池中存在该字符串,则直接返回该引用例,需重新例化;若不存在,则例化该字符串并放入池中。

需要注意的是,字符串常量池的位置在JDK17前后有所变化,可以参考下面这张表:





最后放上这道题的背诵版:


面试官:讲一下JVM运行时数据区域


小牛肉:JVM在执行J程序的过程中会把它所管理的内存划分为若干个不同的数据区域。线程共享的区域是方法区和堆,线程私有的区域是虚拟机栈、本地方法栈和程序计数器。


所谓线程私有就是每个线程都会创建一个属于自己的空间,每个线程之间的这块私有空间互不影响,独立存储。


先来说线程私有的个区域:


程序计数器:程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。


这个内存区域是唯一一个在《J虚拟机规范》中没有规定任何OOME情况的区域。


虚拟机栈:虚拟机栈其是由一个一个的栈帧(SF)组成的,一个栈帧描述的就是一个J方法执行的内存模型。也就是说每个方法在执行的同时都会同步创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法的返回地址等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。


虚拟机栈这个内存区域有两种异常状况:



如果线程请求的栈深度大于虚拟机所允许的深度,将抛出SOE异常(栈溢出)
如果使用的JVM支持动态扩展虚拟机栈容量的话,当栈扩展时法申请到足够的内存就会抛出OOME异常(内存溢出)

本地方法栈:本地方法栈和虚拟机栈作用基本一样,区别只不过是本地方法栈为虚拟机使用到的N方法服务,而虚拟机栈为虚拟机执行J方法(也就是字节码)服务


本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出SOE和OOME异常


再来说线程共享的两个区域:


堆:J堆是虚拟机所管理的内存中最大的一块。堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象例,几乎所有出来的对象例都在这里分配内存。


J堆既可以被现成固定大小的,也可以是可扩展的,如果在堆中没有内存来完成对象例的分配,并且堆也法再扩展时,JVM就会抛出OOME异常


方法区:方法区就是在虚拟机完成类加载之后,存储这个类相关的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。


在JDK8之前,堆和方法区其是连在一起的,或者说方法区其就是堆的一部分,HS虚拟机给出的具体现是永久代,永久代是一个连续的内存空间,由于永久代的垃圾回收和老年代的垃圾回收是绑定的,一旦其中一个区域被占满,这两个区都要进行垃圾回收,增大了OOM发生的概率,另外,有少数的方法例如S的()方法会因永久代的原因而导致不同虚拟机下有不同的表现,不利于代码迁移。这两个原因呢,促使HS在JDK8之后将方法区的现更换成了元空间。





元空间与永久代之间最大的区别在于:元空间不再与堆连续,并且是存在于本地内存(N)中的,这意味着只要本地内存足够,它就不会发生OOM
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

收藏:1 | 帖子:8万



侵权举报:本页面所涉内容均为用户发表并上传,岭南都会网仅提供存储服务,岭南都会网不承担相应的法律责任;如存在侵权问题,请权利人与岭南都会网联系删除!