高亮的杂货铺

JVM从0到1-内存划分

2020-03-13 · 5 min read
JVM Java

jvm运行时会把内存分为多个数据区域,根据《java虚拟机规范》规定,可以划分为方法区,堆,虚拟机栈,本地方法栈和程序计数器,其中方法区和堆是所有线程所共享的,其他则是每个线程隔离的。

程序计数器

其中记录的是当前所执行的字节码的行号,字节码解释就是通过改变这个计数器来完成包括分支、跳转、循环、异常、线程恢复等基本功能的。当线程执行的是 本地方法时,这个计数器的值为空。 这个区域是《java虚拟机规范》中唯一一个没有规定OOM情况的区域。

Java虚拟机栈

每个Java方法被执行时,虚拟机都会同步创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口等信息,一个方法被调用到执行结束的过程,也就对应着栈帧从入栈到出栈的过程。

《java虚拟机规范》规定这个区域会存在两种一场,栈深度大于虚拟机允许深度时,抛出StackOverFlowError,如果栈容量可以动态扩展,则当无法扩展时,抛出OOM异常。我们常用的HotSpot虚拟机栈容量是不会动态扩展的,但是这并不意味着就不会抛出OOM异常,如果线程申请栈空间失败了,也是会抛出OOM的。

本地方法栈

和Java虚拟机栈类似,只是执行的是本地方法,虚拟机规范没有堆这一部分有强制规定,有些虚拟机(例如HotSpot直接把Java虚拟机栈和本地方法栈合并了)

Java堆

堆的唯一目的就是存放对象实例。其内部结构并没有进一步的细致划分,我们常说的新生代,老年代,永久代,Eden,Survivor等都是一部分垃圾收集器的设计风格,和虚拟机规范乃至虚拟机实现没有关系。

从分配对象的角度看,所有线程共享的Java堆可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)用于提升对象分配的效率。 当Java堆中没有空间完成实例分配,而且堆的大小也没法继续扩展时,虚拟机会抛出OOM异常。

方法区

方法区和堆一样,也是各个线程共享的内存区域,这个区域用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器(JIT)编译后的代码缓存等数据。Java虚拟机规范中,把方法区描述为堆的一个逻辑部分,但是他取有一个别名“非堆”(Non-Heap)。
说到方法区,就要提到“永久代”的概念,在JDK8以前,由于HotSpot虚拟机的流行,很多人直接把方法区称之为“永久代”,这种说法是错误的,仅仅是因为在当时HotSpot设计选择把垃圾回收分代的设计扩展到了方法区,或者说,是使用“永久代”来实现了方法区。 原则上,如何设计和实现方法区,Java虚拟机规范并没有要求。在JDK8时,HotSpot虚拟机已经完全抛弃了永久代的概念,改用在本地内存中实现的原空间(Meta Space)来代替。
Java虚拟机规范要求,当方法去无法满足新内存分配需求的时候,会抛出OOM

运行时常量池

运行时常量池是方法区的一部分,Class文件中,除了类的版本、字段、方法、接口等描述信息外,还有一项是常量池表,用于存放在编译期间产生的各种字面量和符号引用,这部分内容会在类加载后存放到方法区的运行时常量池中。 Java虚拟机规范堆class文件的每个部分的格式都有要求,例如每个字节用于存储什么数据。 但是,对于运行时常量池,规范并没有给出细节的要求,所以不同的虚拟机实现会按照自己的设计来实现这个区域,一般来说,除了保存Class文件中的符号引用外,还会把由符号引用翻译而来的直接引用也存储到这个运行时常量池中来。

运行时常量池的一个特点是具有动态性,并不要求常量必须在编译期产生,所以并非只有class文件常量池中的内容可以放入运行时常量池,也可以在运行期间放入新的常量,例如String类的intern()方法。

直接内存

首先要注意,直接内存并不属于虚拟机运行时数据区的一部分,也不是虚拟机规范规定的内存区域,但是这一部分也会被频繁使用,也有可能导致OOM。