#yyds干货盘点# JVM 内存结构

Posted 灰太狼_cxh

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了#yyds干货盘点# JVM 内存结构相关的知识,希望对你有一定的参考价值。

JVM 内存结构

运行时数据区

内存是非常重要的系统资源,是硬盘和 CPU 的中间仓库及桥梁,承载着操作系统和应用程序的实时运行。JVM 内存布局规定了 Java 在运行过程中内存申请、分配、管理的策略,保证了 JVM 的高效稳定运行。不同的 JVM 对于内存的划分方式和管理机制存在着部分差异。

Java 虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。另外一些则是与线程一一对应的,这些与线程一一对应的数据区域会随着线程开始和结束而创建和销毁。

线程私有:程序计数器、虚拟机栈、本地方法区

线程共享:堆、方法区, 堆外内存(Java7的永久代或JDK8的元空间、代码缓存)

程序计数器

程序计数寄存器(Program Counter Register),Register 的命名源于 CPU 的寄存器,寄存器存储指令相关的线程信息,CPU 只有把数据装载到寄存器才能够运行。

这里,并非是广义上所指的物理寄存器,叫程序计数器(或PC计数器或指令计数器)会更加贴切,并且也不容易引起一些不必要的误会。JVM 中的 PC 寄存器是对物理 PC 寄存器的一种抽象模拟。

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

作用

PC 寄存器用来存储指向下一条指令的地址,即将要执行的指令代码。由执行引擎读取下一条指令。

虚拟机栈

作用:主管 Java 程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。

特点:

栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器

JVM 直接对虚拟机栈的操作只有两个:每个方法执行,伴随着入栈(进栈/压栈),方法执行结束出栈

栈不存在垃圾回收问题

栈中可能出现的异常:

Java 虚拟机规范允许 Java虚拟机栈的大小是动态的或者是固定不变的

如果采用固定大小的 Java 虚拟机栈,那每个线程的 Java 虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过 Java 虚拟机栈允许的最大容量,Java 虚拟机将会抛出一个StackOverflowError异常

如果 Java 虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那 Java 虚拟机将会抛出一个OutOfMemoryError异常

可以通过参数-Xss来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。

局部变量表

局部变量表也被称为局部变量数组或者本地变量表

是一组变量值存储空间,主要用于存储方法参数和定义在方法体内的局部变量,包括编译器可知的各种 Java 虚拟机基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此相关的位置)和returnAddress类型(指向了一条字节码指令的地址,已被异常表取代)

由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题

局部变量表所需要的容量大小是编译期确定下来的,并保存在方法的 Code 属性的maximum local variables 数据项中。在方法运行期间是不会改变局部变量表的大小的

方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。对一个函数而言,它的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求。进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少。

局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。

参数值的存放总是在局部变量数组的 index0 开始,到数组长度 -1 的索引结束

操作数栈

每个独立的栈帧中除了包含局部变量表之外,还包含一个后进先出(Last-In-First-Out)的操作数栈,也可以称为表达式栈(Expression Stack)

操作数栈,在方法执行过程中,根据字节码指令,往操作数栈中写入数据或提取数据,即入栈(push)、出栈(pop)

某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。使用它们后再把结果压入栈。比如,执行复制、交换、求和等操作

本地方法栈

Java 虚拟机栈用于管理 Java 方法的调用,而本地方法栈用于管理本地方法的调用

本地方法栈也是线程私有的

允许线程固定或者可动态扩展的内存大小

如果线程请求分配的栈容量超过本地方法栈允许的最大容量,Java 虚拟机将会抛出一个StackOverflowError 异常

如果本地方法栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的本地方法栈,那么 Java虚拟机将会抛出一个OutofMemoryError异常

本地方法是使用 C 语言实现的

它的具体做法是 Native Method Stack 中登记 native 方法,在 Execution Engine 执行时加载本地方法库当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。它和虚拟机拥有同样的权限。

本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区,它甚至可以直接使用本地处理器中的寄存器,直接从本地内存的堆中分配任意数量的内存

并不是所有 JVM 都支持本地方法。因为 Java 虚拟机规范并没有明确要求本地方法栈的使用语言、具体实现方式、数据结构等。如果 JVM 产品不打算支持 native 方法,也可以无需实现本地方法栈

堆内存

内存划分

对于大多数应用,Java 堆是 Java 虚拟机管理的内存中最大的一块,被所有线程共享。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数据都在这里分配内存。

为了进行高效的垃圾回收,虚拟机把堆内存逻辑上划分成三块区域(分代的唯一理由就是优化 GC 性能):

新生带(年轻代):新对象和没达到一定年龄的对象都在新生代

老年代(养老区):被长时间使用的对象,老年代的内存空间应该要比年轻代更大

元空间(JDK1.8 之前叫永久代):像一些方法中的操作临时对象等,JDK1.8 之前是占用 JVM 内存,JDK1.8 之后直接使用物理内存

方法区

方法区(Method Area)与 Java 堆一样,是所有线程共享的内存区域。

虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫 Non-Heap(非堆),目的应该是与 Java 堆区分开。

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版本/字段/方法/接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将类在加载后进入方法区的运行时常量池中存放。运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的是String.intern()方法。受方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError 异常。

方法区的大小和堆空间一样,可以选择固定大小也可选择可扩展,方法区的大小决定了系统可以放多少个类,如果系统类太多,导致方法区溢出,虚拟机同样会抛出内存溢出错误

JVM 关闭后方法区即被释放



以上是关于#yyds干货盘点# JVM 内存结构的主要内容,如果未能解决你的问题,请参考以下文章

#yyds干货盘点# 20张图带你了解JVM运行时数据区(上)

JVM | 第2部分:虚拟机执行子系统《深入理解 Java 虚拟机》 #yyds干货盘点#

从JVM堆内存分析验证深浅拷贝#yyds干货盘点#

通过JConsoler监控Tomcat的JVM内存 #yyds干货盘点#

#yyds干货盘点# 单链表实现栈

#yyds干货盘点# JVM集合之开篇点题