主要介绍以下内容: Java虚拟机内存的各个区域, 这些区域的作用, 服务对象以及其中可能产生的问题
运行时数据区域
概览图
程序计数器(Program Counter Register)
可以看做当前线程所执行的字节码的行号指示器. 在多线程程序中, 为了线程切换后能恢复到正确的执行位置, 每条线程都需要一个独立的程序计数器, 独立存储, 我们称这类内存区域为线程私有的内存.
如果正在执行一个Java方法, 则指向正在执行的虚拟机字节码指令的地址;
如果正在执行一个Native方法(Native方法通常指的是一些系统的底层方法, 有可能是其他语言编写额), 则计数器的值为未定义(undefined)
注: 本内存区域是唯一不会出现OutOfMemoryError
的区域.
Java虚拟机栈(Java Virtual Machine Stacks)
也是线程私有的, 生命周期与线程相同. 虚拟机栈描述的是Java方法执行的内存模型: 每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表, 操作数栈, 动态链接, 方法出口等信息. 每一个方法从调用到执行完成的过程, 就对应着一个栈帧在虚拟机栈从入栈到出栈的过程.
局部变量表: 存放编译期可知的基本数据类型(boolean, byte, char, short, int, float, long, double
), 对象引用和returnAddress
类型. 局部变量表所需的内存空间是在编译期完成分配的, 在方法运行期间不会改变.
本区域可能出现两种异常:
-
*Error
: 线程请求的栈深度大于虚拟机所允许的深度; -
OutOfMemoryError
: 栈扩展时无法申请到足够的内存.
本地方法栈(Native Method Stack)
与Java虚拟机栈非常类似, Java虚拟机栈为执行Java字节码服务, 本地方法栈为执行Native方法服务. 有的虚拟机的实现直接将二者合为一, 比如HotSpot虚拟机. 本区域可能出现的异常与Java虚拟机栈相同.
Java堆(Java Heap)
被所有线程共享, 用于存放对象实例. 是垃圾收集器管理的主要区域. 很多时候也叫做GC堆(Garbage Collected Heap).
从内存回收的角度来说, 现在的收集器采用分代收集算法, 所以可以分为: 新生代和老年代, 再细分可以分为Eden空间, From Survivor空间, To Survivor空间.
从内存分配的角度来看, Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB). 所有划分方式的目的都是为了更好的回收内存, 或者更快的分配内存.
此区域只是一个逻辑上连续的内存区域, 当堆无法扩展的时候, 会抛出OutOfMemoryError
异常.
方法区(Method Area)
所有线程共享, 存储被虚拟机加载的类信息(个人补充: 实际上就是类对象的存放位置), 常量, 静态变量, 即时编译器编译后的代码(也就是一段代码被多次执行后, 会被识别成热点代码, 然后被编译成机器码
)等数据.是Java堆的一个逻辑部分.
通常来说, GC分代收集方法会将方法区设置为永久代(Permanent Generation), 但是容易出现内存溢出问题(比如永久代有-XX:MaxPermSize
的上限). 这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载.
本区域可能出现的异常是OutOfMemoryError
.
运行时常量池(Runtime Constant Pool)
是方法区的一部分, 存放类在编译期生成的各种字面量和符号引用, 在类被加载后进入这里存放. 其次, 在运行期间也可以将新的常量放入池中, 比如String
类的intern()
方法.
本区域可能出现的异常是OutOfMemoryError
直接内存(Direct Memory)
注意: 本区域不是虚拟机运行时数据区的一部分, 但是因为也被频繁的使用, 也可能导致OutOfMemoryError
的出现.
通常是作为堆外内存的一种形式, 例如, Java的NIO(New Input/Output), 引入了一种基于通道(Channel)和缓冲区(Buffer)的I/O方式, 使用Native函数库直接分配堆外内存, 通过一个存储在Java堆中的DirectByteBuffer
对象作为这块内存的引用进行操作. 这样能在一些场景中显著提高性能, 因为它避免了在Java堆和Native堆中来回复制数据.
内存溢出
Java堆溢出
要解决这个区域的异常, 通常先通过内存映像分析工具对Dump出来的堆转储快照进行分析, 重点是确认内存中的对象是否是必要的, 也就是要先分清楚到底是出现了内存泄露(Memory Leak)还是内存溢出(Memory Overflow).
内存泄露 指的是本来应该回收的对象没有被正确回收, 那么对象肯定存在到GC Root的引用链, 从而找到代码泄露的位置.
内存溢出 指的是内存中的对象确实都是必要的, 此时应该检查虚拟机的堆参数(-Xms
与-Xmx
), 看是否可以调大, 不然就需要从代码上检查是否有对象的生命周期过长的情况, 尝试减少程序运行期的内存消耗.
虚拟机栈和本地方法栈溢出
在单个线程下, 无论是由于栈帧太大还是虚拟机栈容量太小, 都会抛出*Error
.
在多线程的情况下, 会出现OutOfMemoryError
的情况, 但是这不是虚拟机栈内存溢出, 而是进程的内存溢出. 究其原因, 操作系统分配给每个进程的内存是有限制的, 比如32bits的windows是2GB. 2GB-Xmx-MaxPermSize
就约等于虚拟机栈的容量了, 如果每个线程分配到的栈容量越大, 那么可以建立的线程数量自然就越少. 需要通过:
- 减小
Xmx
- 减小栈容量
来换取更多的线程. 实践经验表明, 栈深度在1000~2000完全没问题, 对于正常的方法调用(包括递归), 这个深度完全够用了.
方法区和运行时常量池溢出
在JDK 1.7之前, 运行时常量池基本上在垃圾回收中属于永久代, 所以我们可以通过设置-XX:PermSize=10M -XX:MaxPermsize=10M
将它的大小限制在10M, 不能增长.
但是JDK1.7之后, 这部分就不大容易溢出了, 究其原因, 例如在遇到String
类型的对象上进行intern()
调用时, 不是进行赋值, 而是直接调用引用.
而方法区在使用Spring, Hibernate等框架对类进行增强的时候, 会使用到CGLib这类字节码技术, 增强的类越多, 需要越大的方法区来保证动态生成的Class可以载入内存. 其次还有以下场景会引出OutofMemoryError
:
- 基于JVM的动态语言
- 大量JSP或者动态生成JSP文件的应用(JSP第一次运行需要编译为Java类);
本机直接内存溢出
直接内存的大小可以通过-XX:MaxDirectMemorySize
指定, 默认为-Xmx
的值
通常直接内存溢出抛出的异常与Java堆溢出抛出的异常类似, 不过Dump文件不会看见明显的异常, 而且文件很小, 如果符合以上特征, 而程序中又直接或者间接的使用了NIO, 那么就可能是这方面的原因.