前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JVM老生常谈之运行时数据区

JVM老生常谈之运行时数据区

作者头像
飞翔的竹蜻蜓
发布2020-07-07 18:27:51
1.4K0
发布2020-07-07 18:27:51
举报

困而学,学而知 好记性不如烂笔头

之前在学习过程中,很反复的看过关于Java虚拟机关于运行时数据区的一些文章,但是都没有很深刻的记忆,导致一看就忘,看完了一般用不了不久就会忘记。所以我决定自己写一篇笔记,即使后面忘了,也回来翻一翻。

运行时数据区

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为个不同的数据区。这些区域有各自的用途,以及创建和销毁事件。

JVM用来存储加载的类信息、常量、静态变量、编译后的代码等数据。

Java运行时数据区
Java运行时数据区

运行时数据区在虚拟机规范中这是一个逻辑区域。具体实现根据不同虚拟机来实现。

如: Oracle的HotSpot在 Java7 中方法区在永久代,Java8 放在元数据空间,并且通过GC机制对这个区域进行管理。

接下来我们就来依次上图中运行时数据区中的内存空间。

虚拟机栈

Java虚拟机栈也是线程私有的。 它的生命周期与线程相同,也就说每一个线程都会有一个虚拟机栈。

虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。

有人把Java内存区分为堆内存和栈内存,这里的“栈”就是指的虚拟机栈

局部变量表

局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身,可能是一个指向对象其实地址的引用指针,也可能指向一个代表对象的句柄或其它与此对象相关联的位置)和returnAddress类型(指向了一条字节码指令的地址)。

异常情况

  • 如果线程请求的栈深度大于虚拟机所允许的深度,就会抛出StackOverflowError异常
  • 如果虚拟机可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机),如果扩展时无法申请到足够的内存,就会抛出OutOfMemortyError异常

本地方法栈

本地方法栈与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈为虚拟机所使用到Native方法服务。

本地方法栈也会抛出StackOverflowErrorOutOfMemoryError异常。

虚拟机规范对本地方法栈所使用到的语言,使用方式和数据结构没有强制规定,因此不同的虚拟机可以自由实现,比如HotSpot虚拟机就把本地方法栈和虚拟机栈合二为一。

程序计数器

既然程序在不同的栈之间切换,那么系统怎么知道程序执行到那个地方了呢?这个时候就有了一个新的内存空间,程序计数器了。

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

如果线程正在执行的一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器值则为空(Undefined)。

此内存区域是唯一一个在Java虚拟机规范中没有任何OutOfMemortyError情况的区域。

程序计数器
程序计数器

可以看到,程序计数器也是因为线程而产生的,与虚拟机栈配合完成计算操作。程序计数器还存储了当前正在运行的流程,包括正在执行的指令、跳转、分支、循环、异常处理等。

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

在虚拟机规范中描述:所有的对象以及数组都要在堆上分配,但随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也逐渐变得不是那么绝对了。

那一个对象创建的时候,到底是在堆上分配,还是在栈上分配呢?这和两个方面有关:对象的类型和在 Java 类中存在的位置。

Java 的对象可以分为基本数据类型和普通对象。

对于普通对象来说,JVM 会首先在堆上创建对象,然后在其他地方使用的其实是它的引用。比如,把这个引用保存在虚拟机栈的局部变量表中。

对于基本数据类型来说(byte、short、int、long、float、double、char),有两种情况。

我们上面提到,每个线程拥有一个虚拟机栈。当你在方法体内声明了基本数据类型的对象,它就会在栈上直接分配。其他情况,都是在堆上分配。

注意,像 int[] 数组这样的内容,是在堆上分配的。数组并不是基本数据类型

Java堆是垃圾收集器管理的主要区域,所以也被称为GC堆 从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以Java堆中还可以细分为:新生代和老年代;新生代再细分就是:Eden空间、From Survivor空间、ToSurvivor空间等。从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。不论如何划分,都与存放内容无关,无论哪个区域,存放的都仍然是对象实例;进一步划分的目的是为了更好的回收内存,或者更快地分配内存。

根据Java虚拟机规范的规定,Java堆可以除以物理上不连续的内存空间中,只要逻辑上连续即可。

关于垃圾回收可以看看下一篇文章

方法区

方法区也是线程共享的内存区域。它用于存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

运行时常量池

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

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

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

异常

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

元空间

在 Java 8 之前,类的信息是放在一个叫 Perm 区的内存里面的。更早版本,甚至 String.intern 相关的运行时常量池也放在这里。这个区域有大小限制,很容易造成 JVM 内存溢出,从而造成 JVM 崩溃。

Perm 区在 Java 8 中已经被彻底废除,取而代之的是 Metaspace。原来的 Perm 区是在堆上的,现在的元空间是在非堆上的,这是背景。关于它们的对比,可以看下这张图。

元空间
元空间

然后,元空间的好处也是它的坏处。使用非堆可以使用操作系统的内存,JVM 不会再出现方法区的内存溢出;但是,无限制的使用会造成操作系统的死亡。所以,一般也会使用参数 -XX:MaxMetaspaceSize 来控制大小。

方法区,作为一个概念,依然存在。它的物理存储的容器,就是 Metaspace。我们将在后面的课时中,再次遇到它。现在,你只需要了解到,这个区域存储的内容,包括:类的信息、常量池、方法数据、方法代码就可以了。

直接内存

直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但是这部分内存也被频繁的使用,而且也可能导致OutOfMemoryError。

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

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

总结

Java虚拟机栈是运行时数据区域,保存了局部变量表、操作数栈、动态链接、方法出口等信息,每个线程在运行时都会创建一个栈帧,通过程序计数器作为当前执行字节码的行号指示器,也就是指示当前程序执行到哪里了。堆是存储区域,几乎所有的对象都在堆上分配,其他使用的地方只是保存了堆中对象的引用。Java 在 8 之后取消了永久代,取而代之的是元数据区

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2019-12-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 运行时数据区
  • 虚拟机栈
    • 局部变量表
      • 异常情况
      • 本地方法栈
      • 程序计数器
      • 方法区
        • 运行时常量池
          • 异常
      • 元空间
      • 直接内存
      • 总结
      相关产品与服务
      容器服务
      腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档