在 Java VM 自动内存管理机制 的帮助下,不再需要为每个 new 操作去写配对的 delete/free 代码,不容易出现 内存泄漏 和 内存溢出 问题。
正因为把内存控制的权利交给了 Java VM。一旦出现了 内存泄漏 和 内存溢出 问题,不了解 Java VM 的话,排查问题将会成为一项艰难的工作。
运行时数据区域
Java VM 在执行 Java 程序的过程会把其管理的内存划分为若干个不同的数据区域。
这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着 Java VM 进程的启动而存在,有些区域则依赖着用户线程的启动和结束而建立和销毁。
Java VM 所管理的内存将会包括几个运行时数据区域:程序计数器、Java 虚拟机栈、本地方法栈、Java 堆 和 方法区。
程序计数器
程序计数器 是一个块较小的 内存空间。当前线程所执行的字节码的行号指示器。如果线程在执行的是一个 Java 方法,程序计数器 会 记录正在执行的 VM 字节码指令地址;如果执行的是 Native 方法,程序计数器的值会为 空。
每条线程都需要一个独立的 程序计数器,各个线程之间的 程序计数器 互相不影响,独立存储。程序计数器,属于 线程私有 的内存。
程序计数器 这块内存空间,是 VM 规范内 没有规定任何 OOM 情况 的区域。
Java 虚拟机栈
Java 虚拟机栈 也是 线程私有 的,生命周期和线程相同。
Java 虚拟机栈 描述的是 Java 方法执行 的 内存模型:每个 Java 方法在执行的同时会创建一个 栈帧 用于 存储局部变量表、操作数栈、动态链接、方法出口 等信息。每一个方法从调用直至执行完成的过程,就对应着 一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
VM 规范内,这个区域会存在两种异常情况:
1. 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常。
2. 栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。
本地方法区
和 Java 虚拟机栈 类似。区别在于,这里是为 Native 方法服务的。
VM 规范内,这个区域也会存在两种异常情况:
1. 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常。
2. 栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。
Java 堆
Java 堆 是 Java VM 所管理的 内存最大 的一块。是 线程共享 的。
所有对象的实例都在 Java 堆 中分配内存。
这块区域是 垃圾收集器管理的主要区域( GC 堆 )。现在收集器基本都是采用 分代收集算法,Java 堆 还可以分成:新生代和老年代( 新生代还可以分成 Eden 空间、From Survivor 空间、To Survivor 空间等 )。
不需要连续内存,可以通过 -Xmx
和 -Xms
来控制 动态扩展内存大小,如果动态扩展失败会抛出 OutOfMemoryError 异常。
方法区
方法区,也是 线程共享 的。
用于存放已被虚拟机加载的 类信息、常量、静态变量、即时编译器编译后的代码等数据。
和 Java 堆 一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。
对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现,HotSpot 虚拟机 把它当成 永久代 来进行垃圾回收。
运行时常量池
运行时常量池,是 方法区 的一部分。
类加载后,Class 文件中的常量池( 用于存放编译期生成的各种字面量和符号引用 )就会被放到这个区域。
在运行期间也可以用过 String # intern()
将 新的常量 放入该区域。
直接内存
在 JDK 1.4 中新加入了 NIO 类,引入了一种基于 通道(Channel)与 缓冲区(Buffer) 的 I/O 方式,它可以使用 Native 函数库直接分配 堆外内存,然后通过一个存储在 Java 堆 里的 DirectByteBuffer 对象作为 这块内存的引用 进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆 和 Native 堆 中来回复制数据。