第 2 章 Java 内存区域与内存溢出异常
第 2 章 Java 内存区域与内存溢出异常
2.1 概述
2.2 运行时数据区域
Java 虚拟机运行时数据区如下图所示。
补充说明。
- 执行引擎里面包含了垃圾回收期和即时编译器。
- 类加载子系统将数据加载至运行时数据区域。
2.2.1 程序计数器
是一个较小的内存空间,当前线程所执行的行号指示器,用于记录正在执行的虚拟机字节指令地址,线程私有。
通过改变计数器的值来确定下一条指令,比如循环,分支,跳转,异常处理,线程恢复等都是依赖计数器来完成。
Java 虚拟机多线程是通过线程轮流切换并分配处理器执行时间的方式实现的。为了线程切换能恢复到正确的位置,每条线程都需要一个独立的程序计数器,所以它是线程私有的。唯一一块 Java 虚拟机没有规定任何 OutofMemoryError 的区块。
2.2.2 Java 虚拟机栈
存放局部变量表、操作数栈、动态连接、方法出口等,线程私有。
局部变量表存放了基本数据类型、对象引用类型、指向对象起始地址的引用指针、指向一个对象的句柄和 returnAddress 类型(指向了一条字节码指令的地址)。
- 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常。
- 如果 Java 虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出 OutOfMemoryError 异常。
2.2.3 本地方法栈
和虚拟栈相似,只不过它服务于 Native 方法,线程私有。
2.2.4 Java 堆
Java 内存中最大的一块,所有对象实例、数组都存放在 Java 堆,GC 回收的地方,线程共享。
2.2.5 方法区
即非堆,存放已被加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。各线程共享。回收目标主要是常量池的回收和类型的卸载。
注意“永久代”并不等于“方法区”,《Java 虚拟机规范》对方法区的约束是非常宽松的。HotSpot JDK8 完全废弃了永久代的概念。
2.2.6 运行时常量池
是方法区的一部分。
Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
常量池用于存放编译期生成的各种字节码和符号引用,常量池具有一定的动态性,里面可以存放编译期生成的常量;运行期间的常量也可以添加进入常量池中,比如 String.intern()
方法。
2.2.7 直接内存
直接内存并不是 JVM 运行时数据区的一部分, 但也会被频繁的使用: 在 JDK 1.4 引入的 NIO 提供了基于 Channel 与 Buffer 的 IO 方式, 它可以使用 Native 函数库直接分配堆外内存, 然后使用 DirectByteBuffer 对象作为这块内存的引用进行操作(详见: Java I/O 扩展), 这样就避免了在 Java 堆和 Native 堆中来回复制数据, 因此在一些场景中可以显著提高性能。
2.3 HotSpot 虚拟机对象探秘
2.3.1 对象的创建
简单的了解对象创建时涉及类加载、内存分配,具体到对应小节详细介绍。
2.3.2 对象的内存布局(对象头)
对象在堆内存的存储布局可以划分为三个部分:对象头、实例数、对齐填充。
对象头包括两类信息。第一部分是存储对象自身的运行时数据,如哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,官方叫它 Mark Word。对象头的另外一部分是类型指针,即指向它的类型元数据的指针,如果是数组,数组的长度是不确定的,无法放元数据中,也必须放这里存储。
第二部分是对象真正存储的有效信息。
第三部分对齐填充应该就是注解 @sun.misc.Contended
。
2.3.3 对象的访问定位(句柄、直接指针)
- 句柄访问,Java 会在堆种划分出一块内存来作为句柄池,会多一次间接访问的开销,好处是对象被移动时只会改变句柄中的实例指针,而 reference 本身不需要被修改。
- 直接访问,reference 中存储的直接就是对象地址,好处是速度更快,少一次间接访问开销。
2.4 实战:OutOfMemoryError 异常
2.4.1 Java 堆溢出
示例代码见 com/jvm/practice/chapter02/HeapOOMTest.java
。
2.4.2 虚拟机栈和本地方法栈溢出
两种场景抛出 StackOverflowError
异常的示例代码见 com/jvm/practice/chapter02/JavaVMStackSOF.java
和 com/jvm/practice/chapter02/JavaVMStackOOM.java
。
2.4.3 方法区和运行时常量池溢出(intern)
见JVM - 一个案例反推不同 JDK 版本的 intern 机制以及 intern C++ 源码解析。
可以用 String.intern() 方法将字符串保存到常量池中进行测试。注意各版本常量池变动如下要点。
- JDK6 版本,常量池在永久代中。
- JDK7 以上版本,常量池在堆中,不会出现常量池导致的永久代或者元空间 OutOfMemoryError 异常。
因为版本问题,所以 String.intern() 返回的堆引用在 JDK7 中当 “首次遇到” 时,会返回 true。 示例代码见 com/jvm/practice/chapter02/RuntimeConstantPoolOOM.java
。
另外元空间溢出可以用 cglib 复现下,代码略。
2.4.4 本机直接内存溢出
示例代码见 com/jvm/practice/chapter02/DirectMemoryOOM.java
。
一个明显的特征就是在 Heap Dump 文件中不会看见有什么明显的异常情况。