Java把控制内存的权力交给虚拟机,如果出现内存泄漏和溢出的问题,不了解虚拟机使用内存的,排查错误和修正错误会非常的艰难。
虚拟机管理的内存包括:

运行时数据区域

程序计数器

是一块较小的内存空间,通过改变这个计数器的值选取下一条需要执行的字节码指令,是程序控制流的指示器。
每个线程都有自己的程序计数器,用于线程切换后能恢复到正确执行的位置。(“线程私有”的内存)。该区域是唯一一个在《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域。

虚拟机栈

也是线程私有内存,生命周期和线程相同。描述的是Java方法执行的线程内存模型。每个方法执行的时候创建帧栈,存储局部变量表、操作栈、动态连接、方法出口等信息。
数据类型在局部变量表中的存储空间以局部变量槽来表示,long和double的数据类型会占用两个变量槽,其余的占一个。
线程请求的栈深度大于虚拟机的深度:StackOverFlowError
Java虚拟机栈扩展无法申请到足够的内存区域:OutOfMemoryError

Java堆

Java堆是虚拟机管理的内存最大的一块,是被所有线程锁共享的一块内存区域。所有的对象实例都是在这里分配内存的(GC堆,垃圾收集器管理的内存区域)。Java堆可以按照(-Xms和-Xms设定)。如果在Java堆无法扩展,虚拟机会抛出OutOfMemroyError。(所有对象实例及数组都应当在堆上分配)

方法区

各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等数据。(非堆、Non-Heap)方法区无法满足新的内存分配需求时,将抛出OutOfMemoryError异常。

运行时常量池

是方法区的一部分,编译器生成的字面量和符号引用,在类加载后存放到方法区的运行时常量池中。(运行期间也可以将新的常量放入池中,如使用String的intern()方法)。

直接内存

在JDK1.4中,加入NIO,引入了基于通道与缓存区的IO方式,可以使用Native函数库直接分配堆外内存,能在一些场景显著提高性能,避免Java堆和Native堆中来回复制数据。服务器参数配置时,会经常忽略直接内存,使得各个内存区域总和大于物理内存限制,从而导致OutOfMemoryError异常。

常见问题

Java堆溢出

Java堆内存的OutOfMemoryError异常是实际中最常见的内存溢出异常情况。
异常堆栈信息“java.lang.OutOfMemoryError”,进一步提示“Java heap space”。
常规处理方式:
1)通过内存映像分析工具对Dump出来的堆转储进行分析。分析确认是内存泄露还是内存溢出。
2)如果是内存泄露,可以通过工具查看泄露对象的GC Roots的引用链,分析引用路径,如何导致了垃圾收集器无法回收他们。
3)如果是对象都必须存活即内存溢出的情况,则应设置虚拟机的堆参数(Xmx和xms)。看是否有需要向上调整的空间。

虚拟机栈和本地方法栈溢出

栈容量由Xss参数来设定。异常情况包括:
1)如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverFlowError
a.不断调用方法
b.线程局部变量过大过多
c.不限于单线程,通过不断建立线程的方式,在HotSpot上也可以产生内存溢出的异常。【每个线程分配到的栈内存越大,可以建立的线程数量就越少,建立线程时就越容易把剩下的内存耗尽,可以通过减少最大堆和减少栈容量来换取更多的线程】,提示消息如:unable to create native thread
如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出OutOfMemoryError异常。

方法区和运行时常量池溢出

1)抛出异常消息为“OutOfMemoryError”,在JDK 1.6或以下版本为PermGen space。说明运行时常量池属于方法区。
2)在JDK7或者更高的版本中,已经将字符串常量放置在Java堆中。
3)当前主流的框架如Spring、Hibernate对类进行增强时,都会使用到CGLib这类字节码技术,当增强类越多,就越需要越大的方法区以保证动态生成的新类型可以载入内存。
在经常运行时生成大量动态类的应用场景里,需要特别关注类的回收情况(类被垃圾回收期回收的条件是比较苛刻的)。

本机直接内存溢出

1)如果不指定内存,默认最大容量与Java堆最大值(Xmx一致)。
2)由直接内存溢出的明显特征是在Heap Dump文件中不会看见有明显的异常,如果内存溢出后产生的Dump文件很小,又直接使用了Directory或者NIO,则可以多考虑直接内存方面的原因。