自动内存管理机制

JVM承担着管理Java程序运行时内存的责任,程序员无需关心内存分配和释放的问题,但正因为如此,一旦出现了内存泄露和溢出的问题,排查错误将会变得很艰难。

运行时的数据区域

在执行java程序的过程当中,JVM会把其所管理的内存划分为不同的数据区域,有些区域是随着java进程的启动而创建,进程的结束而销毁,有些则是和线程的生命周期绑定。包括如下几个数据区域:

  • 方法区 Method Area

  • 虚拟机栈 VM Stack

  • 本地方法栈 Native Method Stack

  • 堆 Heap

  • 程序计数器 Program Counter Register

程序计数器

他是一块较小的内存空间,作用是当前线程所执行的字节码的行号指示器,通过改变计数器的值来确定线程中程序执行到的位置。Java中线程轮流切换并分配CPU时间,为了确保线程切换后能够恢复到上次执行的位置继续执行,每个线程需要计数器来记录执行到得位置。程序计数器是线程私有的

Java虚拟机栈

它同样是线程私有的,生命周期与线程相同。每个方法的执行同时都会创建一个栈帧(Stack Frame),用于存储局部变量,操作栈,动态链接方法出口等信息,方法调用的开始和结束对应着栈帧的入栈和出栈。局部变量表存放着编译期间可知的数据类型,基本类型和引用(对象的指针或对象的句柄)。局部变量表的所需的内存空间会在编译期间确定。

在栈中,存在两种异常状况:StackOverflowError异常,即请求栈深度大于虚拟机允许的的深度;OutOfMemoryError异常,拓展栈空间时无法申请足够的内存。

本地方法栈

执行Native方法时使用的栈,同样会抛出上面两种异常。(使用native关键字的定义的方法,表示改方法的实现是非java代码实现的)

堆是内存中最大的一块空间,它是所有线程共享的,它随着虚拟机的启动而生成。几乎所有的对象实例都会分配到堆上。为什么是几乎所有而不是所有呢?因为有例外,对象除了在堆上分配,还可以在分配缓冲区(Thread Local Allocation Buffer, TLAB)和栈上分配。

对于TLAB,在Eden中会开辟一个线程私有的空间,一般占用Eden的1%,在存放某些线程私有的数据,由于是线程私有,因此分配时不用上锁,效率高

对于栈上,Java会通过逃逸分析,分析出一个对象是否永远在一个方法里,在一个线程里,如果分析的结果是是,那么可以考虑将其分配到栈上。

另外,由于堆是线程共享的,所以new时会加锁,效率低。

方法区

方法区用于存储类信息,静态变量(static),常量,即时编译器编译后的代码等数据,虽然它也算是堆的一部分,但是常用(No Heap)来描述。

运行时常量池

运行时常量池属于方法区的一部分。Class文件中除了有类的版本,字段,方法,接口等描述信息外,还有常量池,用于存放编译期生成的各种字面量和符号引用。运行期间也会放入新的常量。

直接内存

这部分内存不是虚拟机运行时的数据部分,自从JDK1.4引入NIO后,它可以通过native函数库直接分配堆外内存,然后通过存储在Java堆中的DirectByteBuffer对象来操作这块内存。因此,来分配虚拟机内存的时候,应该预留可用的内存给直接内存。

对象访问

在Java中,对象访问时如何进行的呢?即使最简单的访问,也会涉及Java栈,堆和方法区等重要的内存区域。主流的访问方式有两种:使用句柄直接指针

  • 句柄池,在堆中划分一块内存在作为句柄池,在栈上的引用将指向句柄地址,其中包含了对象的实例数据地址(堆中)和类型数据地址(方法区中)。

  • 直接指针,引用直接指向堆中数据的直接地址,而在堆中的对象实例数据中,有保存到对象类类型数据的指针。

两种方式各有优势:句柄池的好处是稳定的句柄地址,对象移动时(在垃圾回收时移动对象非常普遍),只会改变句柄中的实例数据指针,而引用不需要改变。而直接指针的方式是速度快,省去了一次定位的时间开销。

outOfMemoryError异常

在Java虚拟机中,除了程序计数器外,其他区域都有可能会出现OOM异常

堆内存溢出

首先要设置Java虚拟机参数,-Xms和-XMmx分别对应堆的最大值和最小值,两种相同表示堆不支持动态拓展。当出现堆内存异常时:

1
2
3
4
List<Object> list = new ArrayList();
while(true){
list.add(new Object());
}

会提示java.lang.OutOfMemoryError: java heap space

首先要分析是内存泄漏还是内存溢出,也就是开该对象是否是必要的。如果是必要的,则是内存溢出,不必要则是内存溢出。对于内存泄漏,可通过GC Roots 引用链信息定位错误代码位置,内存溢出则需要调参数。

栈空间溢出

当请求的栈深度大于虚拟机的最大深度,则会提示StackOverFlowError错误,如无穷递归等

常量池溢出

由于常量池属于方法区,因此通过配置-XX:Premsize和-XX:MaxPremSize来设定方法区的大小,同时,使用String.intern()方法将变量存储在常量池中,如果发生溢出,则会出现:

会提示java.lang.OutOfMemoryError: PermGen space

方法区溢出

本机直接内存溢出

OutOfMemroyError总结

程序运行中容易出现OutOfMemoryError,出错的原因有很多,常见的如下:

  • 内存中一次性加载的数据量过大,比如一次性从数据库中读出很多数据。在数据库查询时,如果查询的数据量过大,很容易造成内存溢出,这类型的问题一般比较隐蔽,因为上线前测试的数据量较小,不会溢出,而上线后随着数据量变大,很可能会溢出,因此,对于大量的数据查询,应使用分页查询

  • 集合类中有对象的引用,当不需要里面的实体对象时,为清除集合类的引用。

  • 循环产生大对象。

  • 内存容量过小,应调整内存的大小。