Java虚拟机内存区域划分详解

2023-11-10

背景

  JVM是Java开发的必备技能,JVM相当于Java的操作系统。

  JVM(java virtual machine)即Java虚拟机,是运行java class文件的程序。

  Java代码经过Java编译器编译,会编译成class文件, 是一种与平台无关的代码格式。

  class文件按照JVM规范,包括java代码运行所需要的【元数据】和【代码】等内容。

  JVM加载class文件后,就可以执行java代码了。

  JVM有不同的实现,有熟悉的Hotspot虚拟机,JRockit等。

  在各个操作系统上,又回有各自的虚拟机实现,从而形成了:Java代码 > class文件 > JVM规范 > JVM实现的层次。

  再加上其他语言如scala、groovy也能够生成class文件,这样不仅实现了平台无关性,也实现了语言无关性。

一、概述

   对于 C 和 C++程序开发的开发人员来说,在内存管理领域,程序员对内存拥有绝对的使用权,但是也要注意到正确的使用和清理内存,这就要求程序员有较高的水平。

  而对于 Java 程序员来说,在虚拟机的自动内存管理机制的帮助下,不再需要为每一个 new 操作去写配对的 delete/free 代码,而且不容易出现内存泄漏和内存溢出问题,看起来由虚拟机管理内存一切都很美好。

  不过,也正是因为 Java 程序员把内存控制的权力交给了 Java 虚拟机,一旦出现内存泄漏和溢出方面的问题,如果不了解虚拟机是怎样使用内存的,那排查错误将会成为一项异常艰难的工作。

二、Java运行时数据区域

   我们一般在开发中认为JVM不过有堆和栈两部分组成,但实际的Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。

   这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则是依赖用户线程的启动和结束而建立和销毁。

   如下图:

   

另一个图也可以说明:

 

下边是网上摘下来的图,仅供参考:

  

接下来详细解说上图中的各区域的功能: 

(1)程序计数器(线程私有)

      CPU在执行多线程程序的时候,通过时间片轮转的方式,根据程序计数器来调度线程的执行。 

      程序计数器(program counter register)是一块较小的内存空间。

      它的作用可以看做是:当前线程所执行的字节码的行号指示器。

      在虚拟机概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。

      分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

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

因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

  如果线程正在执行的是一个:

     Java方法,这个计数器记录的是正在执行的虚拟机【字节码指令的地址】;

     Native方法,这个计数器值则为空(Undefined)。

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

(2)Java虚拟机栈(线程私有)

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

         虚拟机栈描述的是:

            Java方法执行的内存模型:每个方法执行的时候都会同时创建一个【栈帧(stack frame)】用于存储局部变量表、操作数栈、动态链接、方法出口等信息。

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

经常有人把 Java 内存区分为堆内存( Heap)和栈内存( Stack),这种分法比较粗糙, Java 内存区域的划分实际上远比这复杂。

这种划分方式的流行只能说明大多数程序员最关注的、与对象内存分配关系最密切的内存区域是这两块。

其中所指的“堆”在后面会专门讲述,而所指的“栈”就是现在讲的虚拟机栈,或者说是虚拟机栈中的局部变量表部分。

   局部变量表:

       ①. 编译期可知的各种基本数据类型( boolean、 byte、 char、 short、 int、 float、long、 double)

       ②. 对象引用(reference类型)(它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)

       ③. returnAddress类型(指向了一条字节码指令的地址)

其中 64 位长度的 long 和 double 类型的数据会占用 2 个局部变量空间(Slot),其余的数据类型只占用 1 个。

局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

在 Java 虚拟机规范中,对这个区域规定了两种异常状况:

   ①. 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常;

   ②. 如果虚拟机栈可以动态扩展(当前大部分的 Java 虚拟机都可动态扩展,只不过 Java 虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出 OutOfMemoryError 异常。

(3)本地方法栈

     本地方法栈( Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的。

     区别:

       ①. 虚拟机栈为虚拟机执行【Java方法(也就是字节码)服务】

       ②. 本地方法栈则是为虚拟机使用到的【Native方法服务】

虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。

甚至有的虚拟机(譬如 Sun HotSpot 虚拟机)直接就把本地方法栈和虚拟机栈合二为一。

与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError 和OutOfMemoryError 异常。

(4)Java堆

         对于大多数应用来说,Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。

        Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。

        此内存区域的唯一目的就是:存放对象实例,几乎所有的对象实例都在这里分配内存。

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

        Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC 堆( ” Garbage Collected Heap,幸好国内没翻译成“垃圾堆”)。

        如果从内存回收的角度看,由于现在收集器基本都采用的【分代收集算法】,所以Java堆中还可以细分为:

            【新生代】和【老年代】;

              再细致一点的有【Eden空间】、【From Survivor空间】、【To Survivor空间】等。

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

        不过,无论如何划分,都与存放内容无关,无论哪个区域,存储的都仍然是对象实例,进一步划分的目的是为了更好地回收内存,或者更快地分配内           存。

        根据 Java 虚拟机规范的规定, Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。

        在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx 和-Xms 控制)。

        如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出 OutOfMemoryError 异常。

(5)方法区

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

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

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

          Java 虚拟机规范对这个区域的限制非常宽松,除了和 Java 堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收               集

          相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。

          这个区域的内存回收目标主要是针对【常量池的回收】和对【类型的卸载】,一般来说这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸                 载,条件相当苛刻,但是这部分区域的回收确实是有必要的。

         根据 Java 虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出 OutOfMemoryError 异常。

(6)运行时常量池

          运行时常量池(Runtime Constant Pool)是方法区的一部分。

          Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table)。

          常量池用于存放【编译期】生成的各种【字面量】和【符号引用】,这部分内容将在类加载后放到方法区的运行时常量池中。

          Java 虚拟机对 Class 文件的每一部分(自然也包括常量池)的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才           会被虚拟机认可、装载和执行。

          但对于运行时常量池, Java 虚拟机规范没有做任何细节的要求,不同的提供商实现的虚拟机可以按照自己的需要来实现这个内存区域。不过,一般            来说,除了保存 Class 文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。

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

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

(7)直接内存

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

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

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Java虚拟机内存区域划分详解 的相关文章

随机推荐

  • Tensorflow 激活函数 activation function

    激活函数 activation function 线性模型的局限性 只通过线性变换 任意层的全连接神经网络和单层神经网络的表达能力并没有任何区别 线性模型能解决的问题是有限的 激活函数的目的是去线性化 如果将每一个神经元的输出通过一个非线性
  • nrm指令报错解决

    今天打算安装切换公司内部的npm源 出现报错 以下为全过程与解决方案 先全局安装nrm npm install nrm g 添加公司的registry nrm add 出现报错 Error ERR REQUIRE ESM require o
  • 多个字段同时去重

    首先创建一个表结构 其中数据如下 根据上面的name age sex三个字段进行去重 去重思想 说到去重 大家想到的肯定是distinct这个关键字 但这个关键字他只能对一个字段进行去重 那么如何同时根据这三个字段去重呢 办法就是把这三个字
  • 用户登录的详细流程(一)

    用户登录的详细流程 1 流程概述 1 首先在进行用户登录的时候 要进行一些必要的准备工作 比如说要对用户登录表进行设计 一般是userId userName phone password salt remark等等 通常数据库存储的密码是m
  • vue3中ts全局声明再.vue文件中显示no-undef

    显示错误xxx is not defined eslintno undef 解决方法 参考 Why Eslint don t see global TypeScript types in vue files no undef 在 eslin
  • 目前流行前端几大UI框架排行榜

    在前端项目开发过程中 总是会引入一些UI框架 已为方便自己的使用 很多大公司都有自己的一套UI框架 下面就是最近经常使用并且很流行的UI框架 一 Mint UI 流行指数 Mint UI是 饿了么团队开发基于vue js的移动端UI框架 它
  • python实现:提取文件夹中子文件夹的图片

    提取文件夹中子文件里的图片的方法 主要运用到的函数 import os import shutil 首先需要获取内部文件夹的文件名 os chdir D 作业 python 数据集 if masked AFDB masked face da
  • vue面试题汇总

    HTML篇 CSS篇 JS篇 TypeScript篇 React篇 微信小程序篇 前端面试题汇总大全 含答案超详细 HTML JS CSS汇总篇 持续更新 前端面试题汇总大全二 含答案超详细 Vue TypeScript React 微信小
  • 受标签影响的最大值

    题目描述 我们有一个 n 项的集合 给出两个整数数组 values 和 labels 第 i 个元素的值和标签分别是 values i 和 labels i 还会给出两个整数 numWanted 和 useLimit 从 n 个元素中选择一
  • Android之屏幕适配方案

    在说明适配方案之前 我们需要对如下几个概念有所了解 屏幕尺寸 屏幕分辨率 屏幕像素密度 屏幕尺寸 屏幕尺寸指屏幕的对角线的物理长度 单位是英寸 1英寸 2 54厘米 比如常见的屏幕尺寸 5 0 5 99 6 0等等 屏幕分辨率 屏幕分辨率是
  • java基础 谈谈方法?

    1 什么是方法 方法是类或对象的行为特征的抽象 是类或者对象中最重要的组成部分 谈到类时 组成部分就是两块 属性 方法 如果是学过c语言的同学 方法就像是函数 需要注意的是 方法在java中不能独立存在 需要定义在类中 或依附于类 或依附于
  • 第七章 区分鸟和飞机

    本章主要内容 1 构建前馈神经网络 2 使用Dataset和DataLoader加载数据 3 了解分类损失 1 cifar10数据集 1 1 数据集下载 Cifar10数据集 下载数据集 from torchvision import da
  • java代码如何读取一个JVM的参数呢?

    下文笔者将使用java代码读取JVM参数的方法分享 如下所示 实现思路 使用 ManagementFactory getRuntimeMXBean 中的getInputArguments 方法即可获取JVM运行时的参数 package co
  • ubuntu nginx 安装和启动和自启动

    ngx http core module 模块http tengine taobao org nginx docs cn docs http ngx http core module html 目录 1前置依赖软件 Linux 安装 mys
  • python开发需要掌握哪些知识-Python的8个基础知识点,新手必须背下来!

    Python是一个面向对象的解释型的交互式高级脚本语言 Python被设计成一种高可读性的语言 因为它大量地使用了英语中的单词作为关键字 而且不像其他语言使用标点符号构成复杂的语法结构 Python的语法结构非常少 Python是一种面向对
  • 全新推出Bard,谷歌google或许可以靠它打败微软OpenAI ChatGPT

    目录 前言 Bard优势 Bard新功能 更直观的Bard互动 Bard深度集成google search Help me write in Gmail 谷歌地图路线的全新沉浸式视图 谷歌照片全新Magic Editor体验 Bard与其他
  • vue监听本地存储token不存在自动刷新页面

    我们在代码编写的时候 为了更好的体验 有时候需要清除本地存储的token来自动刷新页面跳到登陆页面 这时候就需要一个监听器来监听本地存储的变化来执行操作 下面我们会用到一个JavaScript的addEventListener 事件监听方法
  • python 将列表里面的内容写入到txt文件中

    方法一 将列表写入txt文件中 如下代码所示 a是一段二维列表 需要把它写入一个txt文件中 a 1 9 2 5 3 3 2 4 4 3 1 8 1 9 t with open N a txt w as q for i in a for e
  • html5 jquery拍照显示,HTML5+Canvas+jQuery调用手机拍照功能实现图片上传(二)(示例代码)...

    上一篇仅仅讲到前台操作 这篇专门涉及到Java后台处理 前台通过Ajax提交将Base64编码过的图片数据信息传到Java后台 然后Java这边进行接收处理 通过对图片数据信息进行Base64解码 之后使用流将图片数据信息上传至server
  • Java虚拟机内存区域划分详解

    背景 JVM是Java开发的必备技能 JVM相当于Java的操作系统 JVM java virtual machine 即Java虚拟机 是运行java class文件的程序 Java代码经过Java编译器编译 会编译成class文件 是一