Java运行时数据区

  • 程序计数器
  • Java虚拟机栈
  • 本地方法栈
  • 方法区
  • 运行时变量池
  • 直接内存

程序计数器

用来标记该线程下一跳需要执行的指令(类似CPU寄存器的pc)。多线程的情况下,线程切换回来时需要根据程序计数器来恢复到正确的执行位置,各线程间互相独立(线程私有)。程序计数器是唯一一块不会OOM的内存区域。 程序计数器在JAVA方法中有效,在Native方法中为空(Undefined)。Native方法是指在Java中调用非Java代码,或者用Java直接操作硬件的代码。

Java虚拟机栈

类似C函数的栈,线程私有。每个Java方法被执行的时候,都会对应一个栈帧,存储着局部变量表、操作数栈、动态链接、方法出口信息。其中局部变量表中存放了在编译时就已经确定大小的变量。 Java虚拟机栈会出现两种异常:

  • StackOverFlow: 若线程所请求的栈深度超过虚拟机允许的最大深度,则会抛出StackOverFlow异常。例如递归调用过深。
  • OOM: 若虚拟机栈是动态扩展的,如果在扩展的时候申请不到内存,则会抛出OOM异常。例如申请大量线程。

本地方法栈

本地方法栈类似Java虚拟机栈,但它主要是用于Native方法。虚拟机规范中没有规定本地方法栈的实现,有的如HotSpot就将其与虚拟机栈合二为一。

JAVA堆

Java堆内存是虚拟机中最大的一块内存,所有线程共享。虚拟机规范要求所有的对象实例、数组,都要从堆上分配内存(不过有例外)。堆内存是GC主要管理的部分,按照收集的时机,可以分为新生代和老年代内存。对内存进行划分,是为了更好的回收,或者说是更快的分配。 堆内存在物理上可以是不连续的。当在堆上无法申请到内存,并且堆无法扩展的时候,会抛出OOM错误。

方法区

线程共享,方法区存放加载的类、全局变量、静态变量、即时编译器编译后的二进制代码。虚拟机规范比较宽松,从HotSpot虚拟机的实现来看可以把方法区看做为永久区,即GC永远不会光顾方法区(但方法区是有可能出现内存泄露的)。方法区实际上是在堆上的。 当无法在方法区上申请内存时,则会抛出OOM异常。

运行时常量池(runtime constant poll)

占用方法区,用于在类加载时存储class文件中的字面量和符号引用(?)。例如:String s0=”kvill”;其中kvill会存储到运行时常量池中,并且如果有多个变量都被设置为kvill时,他们的内存地址是一致的,即,在运行时常量池中同样的字符串只会有一份拷贝,包括字符串相加结果相同的情况。也可以使用String类的intern方法,将该String的字符串加到运行时常量池中,并返回常量池中的字符串的地址。更详细的说明见这里 运行时常量池占用方法区的内存,也可能会触发OOM异常。

直接内存

JDK1.4的NIO,引入了一种基于通道和缓冲区的IO方式,可以使用Native函数来分配虚拟机之外的内存(例如C语言的malloc函数),避免了虚拟机堆内存和Native堆内存之间来回拷贝。 直接内存也有可能触发OOM异常。

对象访问

如: Object obj = new Object(),obj在虚拟机栈的局部变量表中,其类型为reference;而new Object则从虚拟机堆上申请内存。从对象的引用访问对象有两种方式,一是使用句柄,二是使用指针。 第一种方法需要引入一个句柄池,其中存储着句柄和具体对象指针的对应关系,而reference变量只要存储不变的句柄即可。在GC时对象移动时,使用句柄池可以避免去修改reference对象的值。 第二种方法则更直接,效率更高。但对GC不够友好。

问题定位

1、堆内存溢出OOM 是否内存泄露?若是,查找对象到GC Roots的引用链 是否内存溢出?若是,设置-Xmx和-Xms

2、虚拟机栈内存溢出之StackOverFlow 递归的次数太多会导致栈溢出。 对策:设置更大的-Xss

3、虚拟机栈内存溢出之OOM 对于单线程来说,内存无法分配时,虚拟机抛的仍然是StackOverFlow异常;只有多线程时,不停的拉起线程,导致栈空间逐渐减少,最终才会抛OOM。 对策:设置更大的-Xss

4、运行时常量池OOM 大量的字符串intern()会造成运行时常量池OOM,会提示OOM: PermGen space 对策:设置更大的-XX:PermSize= 和 -XX:MaxPermSize=

5、方法区OOM 动态生成大量class的应用中会出现,提示OOM: PermGen space(如spring、Hibernate框架) 对策:设置更大的-XX:PermSize= 和 -XX:MaxPermSize=

啰里啰嗦了这么多不如一幅图: