我的编程空间,编程开发者的网络收藏夹
学习永远不晚

JAVA jvm系列--java内存区域

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

JAVA jvm系列--java内存区域

JVM: Java Virtual Machine,Java虚拟机,包括处理器、堆栈 、寄存器等,是用来执行java字节码(二进制的形式)的虚拟计算机。

一、JVM的组成

JVM由以下四部分组成(两个子系统和两个组件):

 类加载器(ClassLoader)
执行引擎(Execution Engine)
运行时数据区(Runtime Data Area)
本地库接口(Native Interface)

结构如图:

在这里插入图片描述

(1)运行时数据区域我们在本文进行详解;

(2)类加载机制会在后续文章中依次分析,本文主要介绍运行时数据区域;

(3)执行引擎:

JIT编译器:编译执行;将字节码指令变成机器指令。将机器指令放在方法区缓存。

解释器:逐行解释字节码。

垃圾回收器:内存回收的具体实现。

(4)本地方法库:

有时java应用需要与java外面的环境、操作系统交互。这是本地方法存在的主要原因,你可以想想java需要与一些底层系统如操作系统或某些硬件交换信息时的情况。

jre大部分是用java实现的,它也通过一些本地方法与外界交互。例如:类java.lang.Thread 的 setPriority()方法是用java实现的,但是它实现调用的是该类里的本地方法setPriority0()。这个本地方法是用C实现的,并被植入JVM内部,在Windows 95的平台上,这个本地方法最终将调用Win32 SetPriority() API。这是一个本地方法的具体实现由JVM直接提供,更多的情况是本地方法由外部的动态链接库(external dynamic link library)提供,然后被JVM调用。

本地方法可以通过 JNI(Java Native Interface)来访问虚拟机运行时的数据区,甚至可以调用寄存器,具有和 JVM 相同的能力和权限。 当大量本地方法出现时,势必会削弱 JVM 对系统的控制力,因为它的出错信息都比较黑盒。对内存不足的情况,本地方法栈还是会抛出 nativeheapOutOfMemory。

二、JVM运行流程

在这里插入图片描述

(1)程序在执行之前先要把java代码转换成字节码(class文件);

(2)jvm首先需要把字节码通过类加载器(ClassLoader) 把文件加载到 运行时数据区(Runtime Data Area) ;

(3)字节码文件不能直接交个底层操作系统去执行,因此需要特定的命令解析器 执行引擎(Execution Engine) 将字节码翻译成底层系统指令再交由CPU去执行;

(4)第三步过程中需要调用其他语言的接口 本地库接口(Native Interface) 来实现整个程序的功能。

注:Java 虚拟机与 Java 语言没有什么必然的联系,它只与特定的二进制文件.Class 文件有关 。 因此无论任何语言只要能编译成.Class 文件,就可以被 Java 虚拟机识别并执行,比如Groovy、Kotlin。

三、java内存区域详解(运行时数据区域)

我们说的Java内存区域,一般都指运行时数据区域,其组成如图所示:

在这里插入图片描述

JDK1.8之后的内存区域布局如下:

在这里插入图片描述

参考文章:Java内存区域(运行时数据区域)和内存模型(JMM)

(一)程序计数器

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

对于一个单核cpu(或者是一个内核)来说,只能同时执行一条指令,而JVM通过快速切换线程执行指令来达到多线程的,真正处理器就能同时处理一条指令,只是这种切换速度很快,我们根本不会感知到。为了线程切换后能恢复到正确的执行位置,每条线程都有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。——线程私有,多线程的实现

如果线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器值则为空(Undefined)。此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。——无内存溢出

(二)java虚拟机栈

线程私有:Java 虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同,与线程同时创建。线程的生命周期请参考我的另一篇文章:线程的生命周期。

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

在活动线程中,只有位于栈顶的帧才是有效的,称为当前栈帧。正在执行的方法称为当前方法,栈帧是方法运行的基本结构。在执行引擎运行时,所有指令都只能针对当前栈帧进行操作。

在这里插入图片描述

(1)局部变量表

局部变量表是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量。局部变量要显示初始化,没有默认值。
存放了编译期间可知的基本数据类型、对象引用类型(引用指针)和returnAddress类型(程序就是存储在方法区的字节码指令,指向特定指令内存地址的指针)。

32位的数据类型占用一个局部变量空间(Slot),64位的long和double占2个。

在Java程序被编译为Class文件时,就在方法的Code属性(Java程序方法中的代码经过javac编译之后形成字节码存在了Code属性内)的max_locals数据项中确定了方法所需的分配的局部变量表的最大容量。

(2)操作数栈

操作栈是个初始状态为空的桶式结构栈。在方法执行过程中, 会有各种指令往栈中写入和提取信息。JVM 的执行引擎是基于栈的执行引擎, 其中的栈指的就是操作栈。

虚拟机把操作数栈作为它的工作区——大多数指令都要从这里弹出数据,执行运算,然后把结果压回操作数栈。


i++ 和 ++i 的区别:
i++:从局部变量表取出 i 并压入操作栈(load memory),然后对局部变量表中的 i 
自增 1(add&store memory),将操作栈栈顶值取出使用,如此线程从操作栈读到的是自增之前的值。
++i:先对局部变量表的 i 自增 1(load memory&add&store memory),然后取出并压入操作
栈(load memory),再将操作栈栈顶值取出使用,线程从操作栈读到的是自增之后的值。

(3)动态链接

每个栈帧中包含一个在运行时常量池中对所在方法的引用, 目的是支持方法调用过程的动态连接。

现有动态链接,再有栈帧。

(1)每一个栈帧当中都包含指向运行时常量池栈帧所属方法的引用(invokedynamic指令);

(2)在java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用保存在class文件的常量池里;
比如:描述一个方法调用的另外的其它方法时,就是通过常量池中指向该方法的符号引用来表示,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。

参考:https://www.zhihu.com/question/347395101

知乎上参考到的理解:

比如类里有个a方法,加载到了元空间的内存地址:0x0000 0001号单元 然后运行时常量池里把这个方法的符号引用转换为直接引用: a — 0x0000 0001。

然后调用a方法,创建栈帧,里面保存了常量池里指向a方法的这个直接引用 0x0000 0001。就可以从这个直接引用找到a方法代码的入口执行a方法。

线程切换恢复后也可以根据程序计数器(偏移量)结合这个引用,再次找到a方法在内存中上次执行到的位置,继续执行代码。

什么是符号引用:

符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。例如,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。在Java中,一个java类将会编译成一个class文件。在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如org.simple.People类引用了org.simple.Language类,在编译时People类并不知道Language类的实际内存地址,因此只能使用符号org.simple.Language(假设是这个,当然实际中是由类似于CONSTANT_Class_info的常量来表示的)来表示Language类的地址。

(4)方法返回地址

方法出口。

方法执行时有两种退出情况:


正常退出,即正常执行到任何方法的返回字节码指令;
异常退出。

无论何种退出情况,都将返回至方法当前被调用的位置。方法退出的过程相当于弹出当前栈帧,退出可能有三种方式:


返回值压入上层调用栈帧。
异常信息抛给能够处理的栈帧。
PC计数器指向方法调用后的下一条指令。

(三)本地方法栈

本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。Sun HotSpot 虚拟机直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出 StackOverflowError 和 OutOfMemoryError 异常。

(四)java堆

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

jdk1.8之后,字符串常量池从方法区移到了堆中。

堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”(Garbage Collected Heap)。

(1)从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以 Java 堆中还可以细分为:新生代和老年代;再细致一点的有 Eden 空间、From Survivor 空间、To Survivor 空间等。

(2)从内存分配的角度来看,线程共享的 Java 堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。

Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,当前主流的虚拟机都是按照可扩展来实现的(通过 -Xmx 和 -Xms 控制)。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出 OutOfMemoryError 异常。——内存溢出

(五)方法区

作用:用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

回收:垃圾收集行为在这个区域是比较少出现的,其内存回收目标主要是针对常量池的回收和对类型的卸载。

异常:当方法区无法满足内存分配需求时,将抛出 OutOfMemoryError 异常。

JDK8 之前,Hotspot 中方法区的实现是永久代(Perm),JDK8 开始使用元空间(Metaspace),以前永久代所有内容的字符串常量移至堆内存,其他内容移至元空间,元空间直接在本地内存分配,元空间的大小取决于本地内存的大小。

运行时常量池

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

一般来说,除了保存 Class 文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。

运行时常量池相对于 Class 文件常量池的另外一个重要特征是具备动态性,Java 语言并不要求常量一定只有编译期才能产生,也就是并非预置入 Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是 String 类的 intern() 方法。(当调用 intern() 方法时,编译器会将字符串添加到常量池中(stringTable维护),并返回指向该常量的引用。)

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

(六)直接内存

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域。

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

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

总结

如图所示:

在这里插入图片描述

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注编程网的更多内容!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

JAVA jvm系列--java内存区域

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

基于jvm java内存区域的介绍

jvm虚拟机在运行时需要用到的内存区域.广泛一点就是堆和栈,其实不然,堆和栈只是相对比较笼统的说法,真正区分有如下几个
2023-05-31

Java内存区域与内存模型详解

这篇文章主要介绍“Java内存区域与内存模型详解”,在日常操作中,相信很多人在Java内存区域与内存模型详解问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Java内存区域与内存模型详解”的疑惑有所帮助!接下来
2023-06-02

JVM内存区域的示例分析

这篇文章主要介绍了JVM内存区域的示例分析,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。JVM内存区域我们在编写程序时,经常会遇到OOM(out of Memory)以及内存
2023-06-05

如何理解Java内存区域

如何理解Java内存区域,相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。Java 内存划分: 在Java内存分配中,java将内存分为:方法区,堆,虚拟机栈,本地方法栈,程序计数
2023-06-17

java中内存区域的划分

什么是JVM?JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机包括一套字节码指令集、一组寄存器、一个栈、一个
java中内存区域的划分
2016-07-30

深入浅出JVM内存数据区域

JAVA程序运行于虚拟机之上,运行时需要内存空间。虚拟机执行JAVA程序的过程中会把它管理的内存划分为不同的数据区域方便管理。虚拟机管理内存数据区域划分如下图:java学习视频推荐:java在线教程一、程序计数器(Program Counter Registe
深入浅出JVM内存数据区域
2021-02-14

如何解析JVM内存区域组成

这篇文章将为大家详细讲解有关如何解析JVM内存区域组成,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。在方法(代码块)中定义一个变量时,java就在栈中为这个变量分配JVM内存空间,当超过变量
2023-06-17

JVM中内存区域与内存溢出的示例分析

小编给大家分享一下JVM中内存区域与内存溢出的示例分析,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!Java内存区域与内存溢出异常运行时数据区域程序计数器当前线程
2023-06-17

java jvm内存模型有哪些

Java虚拟机(JVM)内存模型主要有以下几个部分:1. 堆内存(Heap):用于存储Java对象的实例以及数组。堆内存是所有线程共享的,是Java程序运行时的动态数据区。堆内存被划分为新生代(Young Generation)和老年代(O
2023-10-12

JVM内存区域划分相关原理详解

JVM内存区域划分是指将JVM中的内存划分为不同的区域,每个区域有不同的用途和管理方式。JVM内存区域的划分主要有以下几个方面:1. 程序计数器(Program Counter Register):程序计数器是一块较小的内存区域,用于存储当
2023-08-11

JVM内存区域划分的原理是什么

JVM内存区域划分的原理是根据不同的用途和功能将JVM的内存划分为不同的区域,以便更有效地管理和利用内存资源。JVM内存区域主要分为以下几个部分:1. 程序计数器(Program Counter Register):用于记录当前线程执行的字
2023-08-11

Java内存区域与内存溢出异常知识讲解

本篇内容介绍了“Java内存区域与内存溢出异常知识讲解”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!正文一. 基本概念在开始讲解之前, 需要
2023-06-05

Java常见知识点中Jvm内存结构、Java内存模型、Java对象模型的区别是什么

这篇文章将为大家详细讲解有关Java常见知识点中Jvm内存结构、Java内存模型、Java对象模型的区别是什么,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。我们都知道,Java代码是要运行在
2023-06-05

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录