堆和栈
堆
Java堆是Java虚拟机管理的最大的一块内存,是一块被所有线程共享的内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放数据实例,Java世界里“几乎”所有的对象实例都在这里分配内存。(《Java虚拟机规范》中说到:“所有的对象实例以及数组都应该在堆上分配”)。
栈
Java中栈通常指Java虚拟机栈,或者更多时候指的是局部变量表部分。
局部变量表存放了编译期可知的各种Java虚拟机基本数据类型,对象引用和ReturnAddress类型(指向了一条字节码指令的地址)。
StackOverFlowError
当线程请求的栈深度超过大于虚拟机所允许的深度时,会抛出StackOverFlowError异常(如果Java虚拟机栈容量可以动态扩展(HotSpot虚拟机的选择是不支持扩展),那么就不会有这个异常,当扩展到内存不够时,就会是OutOfMemoryError异常)。
为什么HotSpot默认是不支持扩展:个人认为是因为StackOverFlowError的影响比OutOfMemoryError影响小一点。因为StackOverFlowError的影响范围发生该异常的线程自己,如果发生OutOfMemoryError异常,因为内存是所有线程都在使用的,如果发生OutOfMemoryError,那么会影响到其他功能。
OutOfMemoryError
除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError的可能。
Java堆溢出
Java堆是用来存储对象实例的,只要我们不断的创建对象并保证GC Roots到对象之间有可达路径,那么当对象数量触及最大堆的容量时,就会出现内存溢出异常。
简单解决思路
通过参数-XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出的时候Dump出当前的内存堆转储快照以便进行分析。
通过内存映像分析工具对Dump出的堆转储快照进行分析。
- 先确认导致OOM的对象是否必要,即分清楚是出现了内存泄露(不必要,没回收)还是内存溢出(必要)。
- 如果是内存泄露,那么进一步通过工具查看泄露对象到GC Roots的引用链,通过泄露对象的类型信息和引用链信息,找到具体代码位置,并且分析出导致无法回收的原因,进行解决。
- 如果是内存溢出,那么就要比较Java虚拟机的堆参数和机器的内存,然后分析是否能向上调整空间。在从代码上检查是否存在对象生命周期过长,持有状态时间过长,存储的结构设计不合理等情况。
虚拟机栈和本地方法栈溢出
如果线程请求的栈深度大于虚拟机所允许的最大深度,那么就会发生StackOverFlowError
如果虚拟机栈空间允许动态扩展,当扩展栈容量无法申请到足够的内存时,将出想OutOfMemoryError。
因为HotSpot不支持动态扩展,所以除非是创建线程申请内存时就因无法获得足够内存导致OutOfMemoryError,否则都是StackOverFlowError。
StackOverFlowError异常时,会有明确的错误堆栈可供分析,相对而言比较容易定位到问题所在。
方法区和运行时常量池溢出
可以通过String::intern()进行运行时常量池的测试。
在JDK1.6及其以前版本,因为运行时常量池在方法区中,所以当常量池中的数据过多时,就会发生OutOfmemoryError。
而在JDK1.7以后,比如JDK1.7中,原本存在永久代的运行时常量池移Java堆中,此时限制方法区的容量是无意义的。而在JDK1.8中,将方法区中的所有东西都移到元空间中(本地内存),废弃了永久代的概念。
直接内存溢出
一个明显的特征就是heap dump 文件中不会看出什么明显异常情况。
如果发现发生内存溢出后产生的Dump文件很小,而程序中又直接或间接(典型的为NIO)的使用了directMemory(直接内存),那么就应该考虑重点检查一下直接内存。