1. 运行时数据区
- Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。有些区域随着程序的进程的启动而一直存在,有些区域则依赖用户线程的启动结束而建立和销毁。Java虚拟机所管理的内存将会包括以下几个运行时数据区域。

1.1. 虚拟机栈(Java V irtual Machine Stack)
-
线程私有,生命周期与线程相同
-
虚拟机栈描述的是Java方法执行的线程内存模型,每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法出口等信息每个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈汇总从入栈到出栈的过程。
-
有C/C++基础的,可能经常把内存笼统的划分为堆内存和栈内存,在Java中实际内存区域划分要比这更复杂。不过我们比较关注的就是这两个信息。Java中栈通常指的就是这的虚拟机栈、或者更多的情况下是指虚拟机栈中局部变量表部分。
-
局部变量表存放了编译期可知的各种Java虚拟机基本类型数据类型(8种基本数据类型)、对象引用(referrence类型,不是对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其它与此对象相关的位置)、returnAddress类型(指向了一条字节码指令的地址)
-
数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示,其中64位长度的long和double类型的数据会占用两个变量槽,其它的数据类型只占用一个。
-
局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
- TIPS:上述说的大小指变量槽的数量,虚拟机真正使用多大的内存空间来实现一个变量槽,这是完全由具体的虚拟机实现自行决定的事情。
-
在《Java虚拟机规范》中,对虚拟机栈规定了两类异常情况(具体由虚拟机供应商决定,HotSpot虚拟机的栈容量不可以动态扩展)
- 如果线程请求的栈深度大于虚拟机所允许的深度,会抛StackOverflowError异常
- 如果Java虚拟机栈容量可以动态扩展,当栈扩展到无法申请到足够的内存就是抛出OOM异常
1.2. 本地方法栈(Native Method Stacks)
- 本地方法栈与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(字节码),本地方法栈则是为虚拟机使用到的本地方法(Native)服务。
- 《Java虚拟机规范》对本地方法栈中方法使用的语言、使用方式与数据结构并没有任何强制规定,因此具体的虚拟机可以根据需要,自由实现它。有的Java虚拟机(譬如Hot-Spot虚拟机)直接把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError异常。
1.3. 程序计数器(Program Counter Register)
- 当前线程所执行的字节码的行号指示器,字节码解释器工作时通过改变这个计数器的值选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功鞥呢都需要依赖这个计数器来完成。
- Java 虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为"线程私有"的内存。
- 如果线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址。如果线程正在执行的是本地(Native)方法,这个计数器值应为空(Undefined)。此内存区域是唯一一个在《Java 虚拟机规范中》没有规定任何 OOM 的情况。
1.4. 堆(Heap)
- Java 虚拟机管理的内存中最大的一块。在 Java 虚拟机启动时创建,此内存区域的唯一目的就是存放对象实例,Java 大多数对象实例都在这里分配内存。
- 堆是垃圾收集器管理的内存区域。从回收内存的角度,由于现代垃圾收集器大部分都是基于分代收集理论设计的,所以 Java 堆中经常会出现新生代、老年代、eden区和survial区等概念。其实这些区域划分其实仅是一部分垃圾收集器的共同特性或者说设计风格,并非某个 Java 虚拟机具体实现固有的内存布局,也不是 《Java 虚拟机规范》里对 Java 堆的进一步划分。 以G1收集器的出现为分界,作为业界绝对主流的HotSpot虚拟机,它内部的垃圾收集器全部都基于"经典分代"来设计,需要新生代、老年代收集器搭配才能工作。在这种背景下,上述说法还算是不会产生太大歧义。但是到了今天,垃圾收集器技术与十年前已不可同日而语,HotSpot里面也出现了不采用分代设计的新垃圾收集器,再按照上面的提法就有很多需要商榷的地方了。
- 从内存分配的角度看,所有线程共享的 堆中可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB),以提升对象分配时的效率。不过无论如何划分,都不会改变堆中存储内容的共性。
- 根据《Java 虚拟机规范》的规定,Java 堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的,这点就像我们用磁盘空间区存储文件一样,并不要求每个文件都连续存放。但对于大对象(如数组对象),多数虚拟机出于实现简单、存储高效的考虑,可能会要求连续的内存空间。
1.5. 方法区(Method Area)
- 线程共享区域
- 用于存储已被虚拟机加载的类型信息、常量、静态变量、即使编译器后的代码缓存等数据。
- jdk8 以前许多人喜欢将永久代与方法区混为一谈,当时其实是使用永久代来实现方法区而已,这样就可以管理堆一样去管理方法区。方法区的实现不受《Java 虚拟机规范》管束。到了 JDK7 的 HotSpot,已经把原本放在永久代的字符串常量池、静态变量等移出,到了 JDK8 完全废弃永久代,改为在本地内存中实现的元空间来代替,并把永久代还剩余的内容(主要是类型信息)全部移到元空间中。
1.5.1. 运行时常量池(Runtime Constant Pool)
- 运行时常量池是方法区的一部分(无法申请到内存时,受到方法区控制也会抛出 OOM),Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表,用于存放编译期生成的各种字面量与符号引用,这部分数据将在类加载后放到方法区的运行时常量池中。
- 运行时常量池具有动态性,Java 语言并不要求常量一定很自由编译器才能产生,运行期间也可以将新的常量放入池中(参考 String 类的intern()方法)。
- 例如当 IDEA 整个工程特别大,在编译 OOM 时,需要将内存设置大一些。

1.6. 直接内存(Direct Memory)
- 不是《Java 虚拟机规范》中定义的内存区域,但是这部分内存也被频繁的使用,也可能斗志 OOM 异常出现。
- NIO 引入基于通道(Channel)与缓冲区的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在堆里的DirectByteBuffer 对象作为内存的引用进行操作。这样可以在某些场景中提高性能(避免 Java 堆和 Native 堆中来回复制数据)。
- 本地内存的分配不会受到 Java 堆大小的限制,但是还是会受到物理内存的约束。许多研发配置虚拟机参数时,会根据实际内存去设置-Xmx 等参数,忽略直接内存,从而使程序的内存总和大于物理内存限制,导致动态扩展时出现 OOM 异常。
TIPS
- 为了更好的让大家更好的理解相关概念,此文章还会继续补充图片,便于大家的理解。
- 可能会有些小伙伴还是有很多不理解的概念,可以自行先查阅相关内容。我有时间还会继续整理JVM相关的概念。