JVM内存区域

文章目录

      • JVM内存区域
        • Java语言的虚拟机自动内存管理机制
      • JVM运行时数据区域
        • 程序计数器
        • Java虚拟机栈
          • 局部变量表
        • 本地方法栈
        • Java堆
          • 垃圾回收
          • Java堆的可拓展性
        • 方法区 Method Area
          • JDK8以前
          • 永久代存在的问题
          • JDK8以及之后
          • 运行时常量池
        • 直接内存
        • Reference

JVM内存区域

Java语言的虚拟机自动内存管理机制

得益于JVM的虚拟机自动内存管理机制,Java程序员不需要为每一个new操作去写配对的delete/free代码,不容易出现内存泄漏以及内存溢出的问题。当Java程序出现内存泄漏和溢出的问题,需要我们了解JVM是如何使用内存的才能更好的帮助我们排查错误、修正问题。

JVM运行时数据区域

在JVM执行Java程序的过程中,JVM会把它所管理的内存划分为若干个不同的数据区域。每个区域有各自的用途,当然也包括这些区域的创建和销毁的时间。

有的区域随着虚拟机进程的启动而一直存在着,有些区域则是依赖用户线程启动和结束而建立以及销毁。根据《Java虚拟机规范》的规定,JVM虚拟机所管理的内存将会包括一下几个运行时数据区域。

程序计数器

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

线程私有—各条线程之间的程序计数器互不影响,独立存储

生命周期:随着线程创建而创建,随着线程结束而结束。

无OOM的区域—唯一一个在《Java虚拟机规范》中,没有规定任何OutofMememoryError情况的内存区域。

在JVM概念模型里,字节码解释器工作时,通过改变程序计数器的值来选取下一条需要执行的字节码指令。

程序计数器是程序控制流的指示器,以下功能依赖程序计数器来完成:

  • 分支
  • 循环
  • 跳转
  • 异常处理
  • 线程恢复

由于JVM的多线程是通过线程轮流切换,分配处理器执行时间的方式来实现多线程的,在任何一个时刻,一个处理器(多核CPU时,一个处理器代表一个核心(内核),多CPU时,一个处理器代表一个CPU)都只会执行一条线程的指令。线程上下文切换后,为了确保能够恢复到正确的执行位置,每条线程都需要一个独立的程序计数器。

线程执行的是Java方式时:

计数器记录的是正在执行的虚拟机字节码指令的地址。

线程执行的是本地(Native)方法时:

计数器值则应该为空(Undefined)。

Java虚拟机栈

线程私有—每个线程有独自的虚拟机栈

生命周期:随着线程创建而创建,随着线程结束而结束。

可能产生的错误:

OutofMememoryError和StackOverFlowError

StackOverFlowError

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

OutofMememoryError

HotSpot虚拟机的栈容量是不可以动态拓展的,所以Hotspot虚拟机不会因为虚拟机栈无法拓展而导致OutofMememoryError异常—只要线程申请栈空间成功了就不会有OOM,如果申请失败还是会出现OOM异常。

在Java虚拟机栈容量可以动态拓展的情况下,当栈拓展无法申请到足够的内存时,会抛出OutofMememoryError异常。

虚拟机栈描述的是Java方法执行的线程内存模型:

栈帧是方法运行期很重要的基础数据结构。

每个Java方法被执行的时候,JVM都会同步创建一个栈帧(Stack Frame)。栈帧用于存储以下信息:

  • 局部变量表
  • 操作数栈
  • 动态连接
  • 方法出口

等信息。每一个Java方法被调用直至执行完毕的过程 = 一个栈帧在虚拟机栈中入栈和出站的过程

局部变量表

局部变量变是一张存储了各种Java虚拟机基本类型的表:

  • boolean
  • byte
  • char
  • short
  • int
  • float
  • long
  • double
  • 对象引用(reference类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针)
  • returnAddress类型(指向一条字节码指令的地址,现在很少使用)

存储方式:—局部变量槽(Slot)

上述的基本数据类型在局部变量表的存储空间以局部变量槽来标识。

64位长度的long和double会占用两个变量槽来存储,其余的数据类型只占用一个。局部变量表所需要的内存空间在编译期间就完成分配。当操作一个方法时(进入一个方法时),这个方法需要在虚拟机栈栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

:这里的局部变量表的大小指的是变量槽的数量。一个变量槽的大小是多大的的内存空间,具体是由虚拟机实现来确定的(比如一个变量槽=32bit或者64bit或者更多)。

本地方法栈

线程私有—各条线程之间的程序计数器互不影响,独立存储

生命周期:随着线程创建而创建,随着线程结束而结束。

可能产生的错误:

OutofMememoryError和StackOverFlowError

本地方法栈和虚拟机栈的作用是类似的。区别:

本地方法栈:为虚拟机使用到本地方法服务

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

由于《Java虚拟机规范》对本地方法栈使用的语言、使用方式、数据结构都没有任何强制规定,所以具体的虚拟机可以根据需要去实现自由实现它。
Hotspot虚拟机直接将虚拟机栈和本地方法栈合二为一。

Java堆

线程共享—所有线程共享的一块区域,也是虚拟机所管理的内存中最大的一块。

生命周期生命周期:随着线程创建而创建,随着线程结束而结束。

可能产生的错误:

OutofMememoryError

当Java堆中没有足够的内存完成对Java对象实例的内存分配并且堆也无法再拓展时,会抛出OutofMememoryError异常.

唯一目的:存放实例对象,根据《Java虚拟机规范》里对Java堆的描述:所有的实例对象和数组都应当在堆上被分配。

随着Java技术的发展,现在已经能看到一些迹象表明日后可能出现值类型的支持。即使是现在,由于即使编译技术的进步,尤其是逃逸分析技术的日渐强大,类似栈上分配、标量替换等的优化手段已经导致了一些微妙的变化变成。换言之,Java实例对象都分配在堆上也渐渐变得不是那么绝对了。

垃圾回收

Java堆是垃圾收集器管理的内存区域,因此在一些资料中,也将Java堆称为GC堆(Garbage Collected Heap).

从内存内存的角度来看,由于现在垃圾收集器大部分都是基于"分代收集理论"设计的,所以在Java堆中可能会经常出现以下名词:

新生代

老年代

永久代

Eden空间

From Survivor空间

To Survivor空间

等名词

这些区域划分的名称仅仅只是因为一部分的垃圾收集器的共同特性或者说设计风格,并不是某个Java虚拟机实现的固有的内存布局, 也不是《Java虚拟机更规范》里对Java对的进一步细分.

以前,Hotspot虚拟机内部的垃圾收集器全部基于"经典分代"来设计,需要新生代、老年代收集器搭配才能工作。但是发展到现在,Hotspot也出现了其他特性和设计风格的垃圾回收器.

从分配内存角度看Java堆

从分配内存的角度来看, 所有线程共享的Java堆中可以划分出多个线程私有的"分配缓冲区".

分配缓冲区 (Thread Local Allocation Buffer, TLAB)

目的: 提升对象分配时的效率

TLAB并不会改变堆中存储内容的共性,无论从什么角度和如何划分, Java堆都只能是存储的是Java对象的实例. 有时候,将Java堆中的区域进行细分只是为了更好地回收内存或者更高效率的分配和回收内存.

《Java虚拟机规范》中规定,Java堆可以处于物理上不连续的内存空间,但在逻辑上它应该被视为是连续的.

但是对于大对象(典型的例子是数组)来说, 很多虚拟机的实现为了实现简单,存储高效的考虑,很可能会要求连续的内存对象.

Java堆的可拓展性

Java堆可以被实现成固定的大小, 也可以是可拓展的. 主流的Java虚拟机都是按照可拓展来实现,主要通过以下参数来设定:

-Xmx

-Xms

方法区 Method Area

线程共享—与Java堆一样也是线程共享的内存区域

生命周期生命周期:随着线程创建而创建,随着线程结束而结束。

可能产生的错误:

OutofMememoryError

根据《Java虚拟机规范》,当方法区没办法满足新的内存分配需求时,将抛出OutofMememoryError异常.

目的和作用:
用于存放已被虚拟机加载的类型信息、常量、静态变量、即时编译器后的代码缓存等数据。在《Java虚拟机规范》中,方法区被描述为堆的一个逻辑部分。 但是为了与Java堆区分来, 方法区也被成为"非堆"。

方法区会涉及到分代理论设计的垃圾回收期的区域—永久代

JDK8以前

JDK8以前,很多Java程序员习惯在Hotspot虚拟机上进行开发、部署程序,这个时候方法区被很多Java程序员成为"永久代"(Permanent Generation),或者将方法区和永久代混为一谈。本质上,这两者不是等价的,原因仅仅因为:

当时Hotspot的虚拟机设计团队选择将垃圾收集器的分代设计拓展至方法区,或者说使用永久代来实现方法区而已,这样是的Hotspot的垃圾收集器能够像管理Java堆一样管理这部分的内存,省去专门为方法区编写内存管理代码的工作。在其他的虚拟机实现,例如JRockit、IBMJ9来说,是不存在永久代这种概念的。

永久代存在的问题

使用永久代来实现方法区的决定并不是一个好的主意,永久代的设计导致了Java应用更容易因为遇到内存溢出的问题。

永久代存在的问题:

  1. 永久代有-XX:MaxPermanentSize的上限,即使不设置这个上限也会有默认的大小,而类似J9和JRockit只要不粗碰进程可用内存的上限(32位系统中是4GB的限制),就不会出现问题。

  2. 极少数方法(String::intern)会因为永久代的原因导致不同虚拟机下有不同的表现

JDK8以及之后

在JDK6 的时候,Hotspot的团队就有一部分已经放弃永久代了,逐步采用本地内存的(Native Memory)来实现方法区的计划。到了JDK7的时候就将原本放在永久代的字符串常量池、静态变量等移到Java堆中。

JDK8,完全废弃了永久代的概念,改用与JRockit、J9一样,在本地实现原空间(Meta-space)来替代,把JDK7中永久代还剩余的内容全部移到元空间中。

在《Java虚拟机规范》中,对于方法区的约束也很宽松,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可拓展外,甚至还可以选择不实现垃圾回收。相对于Java堆而言,

垃圾回收在方法区这个区域还是比较少出现的,但是并非数据进入了方法区就真的是像与永久代名字一样永久存在。这个区域的内存的回收目标主要是针对:

常量池的回收

对类型的卸载

但是,常量回收和类型卸载的回收效果比较难令人满意,尤其是类型卸载,条件相当苛刻,但是这部分的区域的回收又确实是有必要的。

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

运行时常量池

运行时常量池也是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表,用于

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

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

PS:注意运行时常量池和常量池不是一个概念。

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

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

直接内存

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

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

显然,本机的直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,肯定还是会受到本机总内存(物理内存+SWAP分区或者分页文件)大小以及处理器寻址空间的限制。

一般服务器管理员配置虚拟机参数时,除了根据实际内存去设置-Xmx等参数信息外,不能忽略直接内存,因为如果各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态拓展时出现OutOfMemoryError异常。

在一些场景中可以限制的的提高性能,因为避免了在Java堆和Native堆中来回复制数据。

显然,本机的直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,肯定还是会受到本机总内存(物理内存+SWAP分区或者分页文件)大小以及处理器寻址空间的限制。

一般服务器管理员配置虚拟机参数时,除了根据实际内存去设置-Xmx等参数信息外,不能忽略直接内存,因为如果各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态拓展时出现OutOfMemoryError异常。

Reference

  1. 《深入了解Java虚拟机-JVM高级特性与最佳实践》

你可能感兴趣的:(jvm,java)