深入理解JAVA虚拟机内存模型

java虚拟机技术的入门知识便是java虚拟机内存模型,只有了解了java虚拟机内存模型,才能更深一步对java虚拟机底层进行探索。为了学习java虚拟机内存模型,我们必须知道一段java代码(或者说一个java类)在java虚拟中是怎么被执行的。

首先我们可以从下图中了解到java类在java虚拟机中执行的过程
编译
java源代码.java文件
java字节码文件.class文件
类加载器加载
执行引擎
运行时数据区

如上图所示,首先Java源代码文件(.java后缀)会被Java编译器编译为字节码文件(.class后缀),然后由JVM中的类加载器加载各个类的字节码文件,加载完毕之后,交由JVM执行引擎执行。在整个程序执行过程中,JVM会用一段空间来存储程序执行期间需要用到的数据和相关信息,这段空间一般被称作为Runtime Data Area(运行时数据区),也就是我们常说的JVM内存。因此,在Java中我们常常说到的内存管理就是针对这段空间进行管理(如何分配和回收内存空间)。

那么本篇文章主要是要分析Runtime Data Area(运行时数据区)的结构。

1.那么运行时数据区是怎么划分的呢?

根据jvm规范,java运行时数据区一共划分为5个区域,分别是堆、虚拟机栈、方法区,本地方法栈和程序计数器。具体内存模型图如下:
深入理解JAVA虚拟机内存模型_第1张图片

名称 特征 作用 配置参数 异常
程序计数器 占用内存小,线程私有,生命周期与线程相同 大致为字节码行号指示器
虚拟机栈 线程私有,生命周期与线程相同,使用连续的内存空间 java方法执行内存模型,局部变量表,操作数栈,动态链接,方法出口 -Xss StackOverflowErrorOutOfMemoryError
线程共有,生命周期与java虚拟机相同,可以不适用连续的内存空间 保存对象实例,所有对象实例(包括数组)都要分配在堆上 -Xms -Xsx -Xmn OutOfMemoryError
方法区 线程共享,生命周期与虚拟机相同,可以不使用连续的内存地址 存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据 -XX:PermSize:16M -XX:MaxPermSize64M OutOfMemoryError
运行时常量池 方法区的一部分,具有动态性 存放字面量及符号引用
程序计数器

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

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

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

方法区

方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息常量静态变量、即时编译器编译后的代码等数据,即存放静态文件,如Java类、方法等。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。

对于习惯在HotSpot虚拟机上开发、部署程序的开发者来说,很多人都更愿意把方法区称为“永久代”(Permanent Generation),本质上两者并不等价,仅仅是因为HotSpot虚拟机的设计团队选择把GC分代收集扩展至方法区,或者说使用永久代来实现方法区而已,这样HotSpot的垃圾收集器可以像管理Java堆一样管理这部分内存、能够省去专门为方法区编写内存管理代码的工作。

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

方法区在不同虚拟机中有不同的实现,HotSpot在1.7版本以前和1.7版本,1.7版本后都有变化。
== ① jdk7版本以前的实现如下图所示: ==
深入理解JAVA虚拟机内存模型_第2张图片
② 在目前已经发布的JDK1.7的HotSpot中,已经把原本放在永久代的字符串常量池移到了Java堆中。
③ jdk8版本中则把永久代给完全删除了,取而代之的是MetaSpace,如图:
深入理解JAVA虚拟机内存模型_第3张图片
运行时常量池和静态变量都存储到了堆中,MetaSpace存储类的元数据,MetaSpace直接在本地内存中(Native memory),这样类的元数据分配只受本地内存大小的限制,OOM问题就不存在了。

参数设置:

  • -XX:PermSize设置永久代最小空间大小;
  • -XX:MaxPermSize设置永久代最大空间大小;
    参数含义解析:
    PermSize,表示永久代初始设置大小,这里初始大小表示最小大小,Perm是permanent永久的意思;

注意:

  1. JDK8没有这个参数设置。
  2. 非堆内存不会被Java垃圾回收机制进行处理,在配置之前一定要慎重考虑下自身软件所需要的非堆区内存大小。
线程栈(java虚拟机栈)

每个线程有一私有的栈,随着线程的创建而创建。栈里面存着的是一种叫“栈帧”的东西,每个方法会创建一个栈帧,栈帧中存放了局部变量表(基本数据类型和对象引用)、操作数栈、方法出口等信息。栈的大小可以固定也可以动态扩展。当栈调用深度大于JVM所允许的范围,会抛出StackOverflowError的错误,不过这个深度范围不是一个恒定的值

本地方法栈

这部分主要与虚拟机用到的 Native 方法相关,一般情况下, Java 应用程序员并不需要关心这部分的内容。

java堆

堆内存是 JVM 所有线程共享的部分,在虚拟机启动的时候就已经创建。所有的对象和数组都在堆上进行分配。这部分空间可通过 GC 进行回收。当申请不到空间时会抛出 OutOfMemoryError。

直接内存(堆外内存)

直接内存(Direct Memory),也叫堆外内存,它并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,而是Java虚拟机的堆以外的内存,直接受操作系统管理。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。使用堆外内存有两个优势,一是减少了垃圾回收,二是提升复制速度,如NIO就是采用堆外内存。可以使用未公开的Unsafe和NIO包下ByteBuffer来创建堆外内存。

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

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

参数设置:可以通过 -XX:MaxDirectMemorySize参数来设置最大可用直接内存,如果Java虚拟机启动时未设置则默认为最大堆内存大小,即与 -Xmx相同。即假如最大堆内存为1G,则默认直接内存也为1G,那么JVM最大需要的内存大小为2G多一些。当直接内存达到最大限制时就会触发GC,如果回收失败则会引起OutOfMemoryError。

你可能感兴趣的:(JAVA底层学习)