1 程序计数器
程序计数器是什么?
一块较小的内存空间,看作当前线程所执行的字节码的行号指示。
为什么需要程序计数器?
为了线程1切换后能恢复到正确执行位置,每条线程都需要一个独立的程序计数器。各条线程的程序计数器之前互不影响,独立存储,称这类内存区域为“线程私有”的内存。
程序计数器记录什么?
线程正在执行 Java 方法,计数器记录的是正在执行的虚拟机字节码指令的地址;线程正在执行本地方法,计数器值应为空。
此内存区域是唯一一个在《Java 虚拟机规范》中没有规定任何 OutOfMemoryError 情况的区域。原因是程序计数器所维护的仅仅是代执行的命令地址,是一个可预见大小的值,所以不存在 OutOfMemoryError 异常。
2 Java 虚拟机栈
Java 虚拟机栈也是线程私有,生命周期与线程相同。
虚拟机栈描述的是 Java 执行的线程内存模型:每个方法被执行的时候,虚拟机会同步创建一个栈帧—用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每个方法被调用到执行完毕的过程,都对应一个栈帧在虚拟机栈中从入栈到出栈的过程。
对这个内存区域规定了两类异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常;如果 Java 虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出 OutOfMemoryError 异常。(如果不允许动态扩展,如 HotSpot 虚拟机,不会由于无法扩展抛异常,只要线程申请空间成功就不会有 OOM,申请失败才会有 OOM)
3 本地方法栈
本地方法栈与虚拟机栈发挥的作用相似,区别只是虚拟机栈为虚拟机执行 Java 方法服务,而本地方法栈则是为虚拟机使用的本地方法服务。
4 Java 堆
Java 堆是虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域。在虚拟机启动时创建,此内存区域的唯一目的是存放对象实例,Java 世界几乎所有的对象实例都在这里分配内存。
Java 堆是垃圾收集器管理的内存区域,也被称作“GC堆”(Garbage Collected Heap)。无论从哪个角度2,如何划分,也不会改变 Java 堆中存储内容的共性,无论哪个区域,存储的只能是对象的实例。
Java 堆可以处于物理上不连续的空间,但它应在逻辑上被视为是连续的。但是对于大对象(典型的如数组对象),出于实现简单、存储高效的考虑,很可能会被多数虚拟机要求连续的内存空间。
Java 堆可以是固定大小的,也可以是可扩展的。不过当时主流的都是按可扩展实现的(通过参数 -Xmx 和 -Xms 设定)。如果在 Java 堆中没有内存完成实例分配,并且堆也无法再扩展,会抛出 OOM 异常。
5 方法区
方法区与 Java 堆一样,都是各个线程共享的内存区域,用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。虽然《Java 虚拟机规范》中把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫作“非堆”(Non-Heap),目的是与 Java 堆区分开来。
《Java 虚拟机规范》对方法区的约束是非常宽松的,除了和 Java 堆一样不需要连续的内存和可以选择固定大小或者可扩展外,甚至还可以选择不实现垃圾收集。这区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收有时又确实是必要的。如果方法区无法满足新的内存分配需求时,将抛出 OutOfMemoryError 异常。
运行时常量池
运行时常量池方法区的一部分。常量池表用于存放编译期生成的各种字面量与符号引用,在类加载后存放到方法区的运行时常量池中。一般来说,既保存 Class 文件3中的符号引用,也会把由符号引用翻译出来的直接引用也存储在运行时常量池中。
运行时常量池相对于 Class 常量池的另一个重要特征是具备动态性。Java 语言并不要求常量一定只有编译期才能产生,即并非预置入 Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中,这种特性被开发人员利用得比较多的便是 String 类的 intern() 方法4。
直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是《Java 虚拟机规范》中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致 OutOfMemoryError 异常出现(不受 Java 堆大小限制,但受本机总内存大小和处理器寻址空间限制,根据实例内存设置 -Xmx 等参数信息时,经常忽略掉直接内存,会使各个内存区域总和大于物理内存限制,导致动态扩展出现 OOM 异常)。
在 JDK 1.4 中新加入了 NIO(New Input/Output)类,引入了一种基于通道 (Channel)与缓冲区 (Buffer) 的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里面的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。
总结
Java 虚拟机所管理的内存运行时包括的数据区
Java 虚拟机多线程是通过线程切换,分配处理器执行时间的方式来实现的。任何一个确定的时刻,一个处理器或多核处理器的一个内核都只能执行一条线程中指令。 ↩︎
回收内存角度:现代垃圾收集器大部分都是基于分代收集理论设计的,Java 堆中经常会出现“新生代”“老年代”“永久代”“Eden 空间”“From Survivor 空间”“To Survivor 空间”等名词。分配内存角度:所有线程共享的 Java 堆中可以划分出多个线程私有的分配缓冲区,以提升对象分配时的效率。 ↩︎
Class 文件除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表。 ↩︎
String::intern() 是一个本地方法,它的作用是如果字符串常量池中已经包含一个等于此 String 对象的字符串,则返回代表池中这个字符串的 String 对象的引用;否则,会将此 String 对象包含的字符串添加到常量池中,并且返回此 String对象的引用。 ↩︎