Java 与 C++ 之间有一堵由内存动态分配和垃圾收集技术所围成的 “高墙”,墙外面的人想进去,墙里面的人却想出来。——《深入理解Java虚拟机:JVM高级特性与最佳时实践(第二版)》周志明
Java 虚拟机作为运行 Java 程序抽象出来的计算机,具有内存管理的能力,像内存分配、垃圾回收等这些相关的内存管理问题,Java 虚拟机都会帮我们解决,所以作为一个 Java 程序员要比 C++ 程序员幸福,但是内存方面一旦出现问题,如果对虚拟机怎样使用内存不了解,就很难排查错误。
这段时间看周志明先生的《深入理解Java虚拟机:JVM高级特性与最佳时实践(第二版)》,下面就对 Java 虚拟机对内存的管理做一个系统的整理,本篇文章是该专题的第一篇。
内存是计算机中运行系统和软件的场所,而内存划分是 Java 虚拟机管理内存中人为添加的概念,是为了更好的描述 Java 虚拟机对内存的管理。下图中的的运行时数据区域即是 Java 虚拟机所管理的内存区域。
内存划分.png
在 CPU 的寄存器中有指令计数器,而在 Java 虚拟机内存管理中也有类似的程序计数器。程序计数器占用一块很小的内存空间,并且每条线程中都有独立的程序计数器。指令计数器记录的是 CPU 将要执行的下一条指令的地址,而程序计数器略有不同。在线程执行的 Java 方法时,程序计数器记录的是正在执行的虚拟机字节码指令的地址,而在线程执行 Native 方法时,程序计数器为空,因为此时 Java 虚拟机调用是和操作系统相关的接口,与 Java 语言无关。
此区域是唯一一个在 Java 虚拟机规范中没有规定会出现 OutOfMemoryError 情况的区域,对 OutOutOfMemoryError 的讲解会在后面说到。
我们常在程序运行的内存划分为堆区和栈区,但是在 Java 中,这样的划分是很粗糙的,Java 虚拟机中栈有 Hava虚拟机栈和本地方法栈。同程序计数器一样,Java 虚拟机栈也是每条线程私有的。虚拟机栈描述的是 Java 方法执行时的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。栈这种数据结构,就不多说了,特点是先进先出(FIFO),而栈帧就是栈中的数据元素,下图中是栈帧的这种数据在 Java 栈中的结构图。
栈帧数据结构.png
局部变量表中存放的是编译期可知的各种基本数据类型,包括 Java 的八大基本数据类型和对象引用(reference)类型(这种类型不在这里详细说了)。一个方法需要在帧中分配多大的局部变量空间是完全确定的,并且在其方法运行期间不会改变局部变量表的大小,进而可以知道局部变量表所需的内存空间在编译期就确定下来了。
在 Java 虚拟机规范中,对这个区域规定了两种异常出现的情况:
本地方法栈和虚拟机栈作用是相似的,他们之间的区别无非是虚拟机栈为虚拟机执行的是 Java 方法,本地方法栈为虚拟机使用的是 Native 方法。其实,不同的 Java 虚拟机,对栈区域的实现是不同的,比如主流的 HotSpot 虚拟机就把虚拟机栈和本地放栈合二为一了。
与虚拟机栈一样,本地方法栈也会抛出 StackOverflowError 和 OutOfMemoryError 异常。
Java 堆是 Java 虚拟机内存所管理的内存最大的一块,所有的线程都共享此区域,此区域可以说是 Java 对象的出生地,此区域的唯一目睹就是存放 Java 实例,几乎所有的对象实例都在这里分配内存(不同的编译器有所不同)。Java 堆也是垃圾收集器管理的主要区域,也被称为是 “GC堆”。Java 堆在物理上可以处于不连续的内存空间,只要在逻辑上是连续的就可以了,就像磁盘空间存放文件一样。Java 堆既可以是固定大小的,也可以是可扩展的,在主流的 java 虚拟机中是按照可扩展来实现的。关于 Java 堆的详细介绍将在后面说明。
如果在 Java 堆中没有足够的内存空间完成对象实例的分配,并且堆也无法再扩展,将会抛出 OutOfMemoryError 异常。
同 Java 堆一样,方法区也是各个线程共享的区域,它用于存储已经被虚拟机加载过的类信息、常量、静态变量、即时编译器编译后的代码等数据。Java 虚拟机规范中把方法区描述为堆的一个逻辑部分,也叫做 “非堆”,也是为了和 Java 堆区分开来。方法区和 Java 堆一样,也不需要连续的内存空间,在 Java 虚拟机的实现中,也是可以选择固定大小或者可扩展,并且还可以选择不实现垃圾回收,因为这个区域需要用到回收的地方很少,但是实际开发种的教训告诉我们,对方法区进行垃圾回收也是很有必要的,这个区域同样会出现内存泄漏的问题。
在方法区中,有一部分被称为是运行时常量池。常量池除了用于存放在编译期生成的各种字面量和符号引用,此外还有直接引用也被存储在运行时常量池中。运行时常量池具有动态性,常量并不一定实在编译期才被放入该常量池,在运行期间也可以有新的常量放入池中,如我们在开发中使用 String 类的 intern() 方法时。
对字面量和符号引用不清楚的小伙伴可以看下面两篇扩展阅读。
方法区中并不是像字面意思那样存放方法的,它很像一个Java世界的身份信息中心,类,常量、变量的信息都有。——个人理解
当方法区无法满足内存分配时,抛出 OutOfMemoryError 异常。
直接内存并不在 Java 虚拟机管理的内存区域内,也不是 Java 虚拟机规范中定义的内存区域。直接内存是 Java 程序不经过 Java 虚拟机分配,直接使用主机的物理内存,在一些场景(如文件赋值)中可以提高性能,但是直接在使用直接内存中也要注意主机内存大小的限制(包括物理和系统级的限制),否则也会抛出 OutOfMemoryError 异常。