一篇认清Java虚拟机运行时数据区

  • j3_liuliang
  • 今天来和大家唠唠这个Java虚拟机,这段时间都是在看底层类的东西,逻辑性太强了,不多动动小手总结的话怕辛辛苦苦学到的东西,一夜就烟消云散了;

说起虚拟机,我天真的以为是VMware虚拟机(这面子丢大了,想个法子找回来);

本身就是走Java方向的,不了解Java虚拟机那就愧对这条路了,所以这段时间就有事没事的摸鱼(别学我,我是完成工作闲暇之时)看书。不过不知道你们离开学校,步入社会之后还有没有学生时代那种看书一看就是一下午或者几个小时,还贼精神得那种状态,反正我没有。你看哈!要是我摸鱼看书看的还贼精神,要是被领导看见了说“这小伙子不错,工作精神状态很好,提拔加薪有你一份了”,那不得乐开了花,从此走向人生巅峰,迎娶白富美还不是指日可待;

扯远了,回到主线…

其实以前也学过Java虚拟机,仅仅是学过,就是根据网上的教学视频过了一下(应付面试,必问)。现在有工作了,想着看本书好好的深入理解一下,所以才有了下面的博文(后期会出Java虚拟机系列,关注一波不迷路哦!),事先声明本博客是以《深入理解Java虚拟机第三版》周志明老师写的和本人聪慧过人的理解能力以及度娘的深情指点才辛辛苦苦写出来的(哎呀!不小心卖了个惨)。所以你们也可以看书或者问度娘来了解相关的内容,不过都有这么一个聪慧过人的博主写好了,你们就别费那个心思了,直接过来白嫖一下不香嘛!(三联就行);

要开始了,准备了!

在这里插入图片描述

一、一图胜千言

在这里插入图片描述

这张图可以说是经典了,只要搜一下Java虚拟机,这张图是必出现的!

相信没接触Java虚拟机的,肯定看不太明白是扎个回事,但这都没关系(还是要重视一下,知道个印象),谁让我是你们迎娶白富美,走向人生巅峰的贴身管家呢!对于这张图,你们要了解的是下面几个内容:

两个子系统为

  • Class Loader(类装载子系统)
  • Execution Engine(执行引擎)

两个组件为

  • Runtime Data Area(运行时数据区)本篇重点
  • Native Interface(本地接口)

对,就是这么简单这篇会重点介绍运行时数据区,后面的系列会逐一将这个两个系统,两个组件一一道来;

二、程序计数器 (私有

先来看看概念:

程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器;

学Java的都知道,Java文件编译的时候都会被虚拟机编译成字节码文件,那么谁来处理这个字节码文件?

在Java虚拟机概念模型中,有一个字节码解释器它就是通过改变这个计数器的值来选取下一条需要执行的字节码指令;

程序计数器是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成;

对了你们注意看标题,他是私有的哦!就是线程私有,当应用程序中有多个线程在执行任务时,每个线程中的程序计数器独立存储不被其他线程访问到就是线程私有;

由于Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

既然程序计数器是指向虚拟机下一个要执行的字节码的行号地址,那么聪明的小伙伴可能就会问了,Java中不是有很多不是用Java写的类库而是通过调用第三方的本地方法执行的,那它将会怎么指向呢!

看到有小伙伴可以提出这样问题,很棒哦!其实我们能想到的,Sun公司的大佬看到也想到了(是一定想到了)所以他们做了下面的处理:

  • 如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;

  • 如果正在执行的是本地(Native)方法,这个计数器值则应为空(Undefined)。

讲到这里,程序计数器可以说是了解完了,最后再说一下此内存区域是唯一一个在《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域;

在这里插入图片描述

三、Java虚拟机栈 (私有

看到这个,学过数据结构的小伙伴是不是第一时间就会想到“”,不错这说明你们还是有点东西的;

与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stack)也是线程私有的,并且它的生命周期与线程相同;

既然是栈,那么Java虚拟机栈的入栈到出栈对应的就是Java方法被调用直至执行完毕的过程。当然在一切皆对象的Java世界中进入栈的当然不是一个单单的Java方法, 当方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息;

这里说一下栈帧存储的结构:

  • 储局部变量表
  • 操作数栈
  • 动态连接
  • 方法出口等信息

这里的每一个结构都有对应的功能,但在这里我们不过多的深入,就先只要了解一个栈深帧中有这些东西就行,下面看一下我画的生动形象的图

在这里插入图片描述

在虚拟机栈中我们关注最多的那就是局部变量比表了,它里面存放了编译期可知的各种数据类型(基本类型,引用类型,特殊类型)

  • 基本数据类型(boolean、byte、char、short、int、float、long、double)
  • 对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)
  • returnAddress类型(指向了一条字节码指令的地址)

既然这个局部局部变量表可以存放多种类型数据,那么它们是如何存放的,——局部变量槽(Slot)

其实这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示,其中64位长度的long和double类型的数据会占用两个变量槽,其余的数据类型只占用一个;

局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。小伙伴们注意哦!,这里说的“大小”是指变量槽的数量,虚拟机真正使用多大的内存空间(譬如按照1个变量槽占用32个比特、64个比特,或者更多)来实现一个变量槽,这是完全由具体的虚拟机实现自行决定的事情

简单来做个小总结

局部变量表 -> 存储空间 -> 局部变量槽(Slot)

局部变量表 -> 所需内存空间 -> 编译期间完成分配确定下来

方法进栈 -> 形成栈帧 -> 分配确定大小的局部变量空间(大小:变量槽数量)

最后,我们再来看看这个Java虚拟机栈中会抛出什么异常

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

  1. 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常
  2. 如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常。

这里要说明一下虚拟机动态扩容

HotSpot虚拟机的栈容量是不可以动态扩展的,以前的Classic虚拟机倒是可以。所以在HotSpot虚拟机上是不会由于虚拟机栈无法扩展而导致OutOfMemoryError异常——只要线程申请栈空间成功了就不会有OOM,但是如果申请时就失败,仍然是会出现OOM异常的

在这里插入图片描述

四、本地方法栈 (私有

来到这里,不知道有没有小伙伴记得上面我们讲的程序计数器中提到过本地方法,我相信冰雪聪明的你们(比我还差一点点)肯定是想到了就是不想和我一样的表现出来,那我只能说你们 to young!!

其实本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务;

虚拟机栈:Java方法

本地方法栈:Native方法

在《Java虚拟机规范》中对本地方法栈中方法使用的语言、使用方式与数据结构并没有任何强制规定,因此具体的虚拟机可以根据需要自由实现它,甚至有的Java虚拟机(譬如Hot-Spot虚拟机)直接就把本地方法栈和虚拟机栈合二为一

Java虚拟机(JVM)是一种规范,其有很多实现实例,而HotSpot虚拟机就是其中之一

小伙伴们了解一下这些内容就行,知道本地方法栈进出栈的不是由Java语言实现的方法,线程私有就差不多了;

最后说一下与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError异常;

五、Java堆 (共享

在这里,首先我们需要知道的是,Java堆(Java Heap)是虚拟机所管理的内存中最大的一块,也是被所有线程共享的一块内存区域,在虚拟机启动时创建;

Java堆存在的唯一目的就是存放对象实例,Java世界里“几乎”所有的对象实例都在这里分配内存;

看到我的几乎一词是不是标亮了,其实在《Java虚拟机规范》中对Java堆的描述是:“所有的对象实例以及数组都应当在堆上分配”,而这里我用“几乎”是指从实现角度来看;

随着Java语言的发展,现在已经能看到些许迹象表明日后可能出现值类型的支持,即使只考虑现在,由于即时编译技术的进步,尤其是逃逸分析技术的日渐强大,栈上分配标量替换优化手段已经导致一些微妙的变化悄然发生,所以说Java对象实例都分配在堆上也渐渐变得不是那么绝对了;

说到堆,那就不得不提一下垃圾收集器(后面讲)了,它所操作的区域主要就是这个Java堆;

在G1垃圾收集器出现之前作为业界绝对主流的HotSpot虚拟机,它内部的垃圾收集器全部都基于“经典分代”(指新生代(其中又包含一个Eden和两个Survivor)、老年代这种划分。但是到了今天垃圾收集器技术与十年前已不可同日而语,HotSpot里面也出现了不采用分代设计的新垃圾收集器,再按照上面的分类,就会显得不太恰当;

之所以讲上面的话,就是让小伙伴们明白这些区域划分仅仅是一部分垃圾收集器的共同特性或者说设计风格而已,而非某个Java虚拟机具体实现的固有内存布局,更不是《Java虚拟机规范》里对Java堆的进一步细致划分;

在这里插入图片描述

如果从分配内存的角度看,所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB),以提升对象分配时的效率。不过无论从什么角度,无论如何划分,都不会改变Java堆中存储内容的共性,无论是哪个区域,存储的都只能是对象的实例,将Java堆细分的目的只是为了更好地回收内存,或者更快地分配内存

这里我先提两个名词:

  • 物理内存:在应用中,真实存在的,插在主板内存槽上的内存条的容量的大小
  • 逻辑(虚拟)内存:使程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换

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

最后Java堆既可以被实现成固定大小的,也可以是可扩展的,不过当前主流的Java虚拟机都是按照可扩展来实现的(通过参数-Xmx和-Xms设定)。如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常。

六、方法区 (共享

对于方法区,先了解下面两点:

  1. 各个线程共享的内存区域
  2. 存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据

有意思的是在《Java虚拟机规范》中把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫作“非堆”(Non-Heap),目的是与Java堆区 分开来;

在这里说一个方法区的另一个别名:“永久代”,在JDK 8以前,许多Java程序员都习惯在HotSpot虚拟机上开发、部署程序,很多人都更愿意把方法区称呼为“永久代”(Permanent Generation),或将两者混为一谈;

首先,本质上这两者并不是等价的哦!因为仅仅是当时的HotSpot虚拟机设计团队选择把收集器的分代设计扩展至方法区,或者说使用永久代来实现方法区而已,这样使得HotSpot的垃圾收集器能够像管理Java堆一样管理这部分内存,省去专门为方法区编写内存管理代码的工作(这里是不是要吐槽,程序员就是会偷懒呢);

不过,到JDK 8的时候HotSpot开发团队放弃永久代的概念,而用一个元空间(Meta-space)来代替并将其存储的所有内容都移植到其中,要注意的是元空间可是存储在本地的哦!并不是在Java虚拟机中;

《Java虚拟机规范》对方法区的约束是非常宽松的,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,甚至还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域的确是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收效果比较令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收有时又确实是必要的。以前Sun公司的Bug列表中,曾出现过的若干个严重的Bug就是由于低版本的HotSpot虚拟机对此区域未完全回收而导致内存泄漏;

最后还要提的是,如果方法区无法满足新的内存分配需求时,将抛出OutOfMemoryError异常;

七、运行时常量池

要明白一点,运行时常量池(Runtime Constant Pool)是方法区的一部分,用于存放编译期生成的各种字面量和符号引用;

在这里插入图片描述

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

对于这段话,再来一张图

在这里插入图片描述

Java虚拟机对于Class文件每一部分(自然也包括常量池)的格式都有严格规定,如每一个字节用于存储哪种数据都必须符合规范上的要求才会被虚拟机认可、加载和执行,但对于运行时常量池, 《Java虚拟机规范》并没有做任何细节的要求,不同提供商实现的虚拟机可以按照自己的需要来实现这个内存区域,不过一般来说,除了保存Class文件中描述的符号引用外,还会把由符号引用翻译出来的直接引用也存储在运行时常量池中;

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

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

八、直接内存

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现,所以我们放到这里一起讲解;

直接内存不在java堆内,并且java堆内存往外写需要拷贝到native堆。

在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的 DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据;

显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括物理内存、SWAP分区或者分页文件)大小以及处理器寻址空间的限制,一般服务器管理员配置虚拟机参数时,会根据实际内存去设置-Xmx等参数信息,但经常忽略掉直接内存,使得 各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现 OutOfMemoryError异常;

  • 本机直接内存的分配不会受到Java 堆大小的限制,受到本机总内存大小限制
  • 配置虚拟机参数时,不要忽略直接内存 防止出现OutOfMemoryError异常

在这里插入图片描述

结束语

  • 最后,能坚持看完的小伙伴我相信一定是会有收获的,那么一起期待我的后续文章吧!
  • 由于博主才疏学浅,难免会有纰漏,假如你发现了错误或偏见的地方,还望留言给我指出来,我会对其加以修正。
  • 如果你觉得文章还不错,你的转发、分享、点赞、留言就是对我最大的鼓励。
  • 感谢您的阅读,十分欢迎并感谢您的关注。
  • 5
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

J3code

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值