前言
说到Java内存区域,可能很多人第一反应是"堆栈"。首先堆栈不是一个概念,而是两个概念,堆和栈是两块不同的内存区域,简单理解的话,堆是用来存放对象而栈是用来执行程序的。对于Java程序员来说,在虚拟机自动内存管理机制的帮助下,不再需要为每一个new操作去配对delete/free代码, 不容易出现内存泄露和内存溢出问题。但是,也正是因为Java把内存控制权交给了虚拟机,一旦出现内存泄露和内存溢出的问题,就难以排查,因此一个好的 Java程序员应该去了解虚拟机的内存区域以及会引起内存泄露和内存溢出的场景。
运行时数据区域
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,这些区域都有各自的用途,以及创建和销毁的时间。有的区域随着虚拟机进程的启动而存在,有的区域则依赖于用户线程的启动和结束而建立和销毁。Java虚拟机所管理的内存将会包括以下几个运行时数据区域。
1、线程独有的内存区域
(1)PROGRAM COUNTER REGISTER,程序计数器
这块内存区域很小,它是当前线程所执行的字节码的行号指示器,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。分支,循环,异常处理等基础功能都需要依赖这个计数器来完成!如果线程正在执行的是一个Java方法时这个计数器记录的是正在执行的虚拟机字节码指令的地址,如果执行的是一个Native方法,那这个计数器是空的。
由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现,为了线程切换之后能恢复到正确执行的位置,每个线程都需要一个独立的程序计数器,各个线程之间的程序计数器互不影响,独立存储。我们称这类内存区域是线程的私有内存!
程序计数器是唯一一个在java虚拟机规范中没有规定任何OOM情况的区域!
(2)JAVA VIRTUAL MACHINE STACK,虚拟机栈
生命周期和线程相同。虚拟机栈描述的是java方法执行的内存模型。每个方法执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法从调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。栈的大小和具体JVM的实现有关,通常在256K~756K之间。
在java虚拟机规范中,对这个区域规定了两种异常状况,如果线程请求的栈深度大于虚拟机所允许的深度,将会抛出StackOverflowError异常。如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OOM异常!
(3)NATIVE METHOD STACK,方法栈
和虚拟机栈起的作用一样,只不过虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务。虚拟机规范并没有对这个区域有什么强制规定,因此我们使用的HotSpot虚拟机,就干脆没有这块区域了,它和虚拟机栈是一起的。异常情况同虚拟机栈
2、线程间共享的内存区域
(1)HEAP,堆
大多数应用,堆都是Java虚拟机所管理的内存中最大的一块,它在虚拟机启动时创建,被所有线程共享!此内存唯一的目的就是存放对象实例。Java堆是垃圾收集器管理的主要区域,由于现在垃圾收集器采用的基本都是分代收集算法,所以堆还可以细分为新生代和老年代,再细致一点还有Eden区、From Survivior区、To Survivor区,细分堆只是为了更好地回收内存,或者更快地分配内存;
如果堆中没有内存完成实例的分配并且堆也无法再扩展时将会抛出OOM!
(2)METHOD AREA,方法区
这块区域是各个线程共享的区域,用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,虚拟机规范是把这块区域描述为堆的一个逻辑部分的,但实际它应该是要和堆区分开的。从上面提到的分代收集算法的角度看,HotSpot中,方法区≈ 永久代。不过JDK 7之后,我们使用的HotSpot应该就没有永久代这个概念了,会采用Native Memory来实现方法区的规划了。
(3)RUNTIME CONSTANT POOL,运行时常量池
上面的图中没有画出来,因为它是方法区的一部分。Class文件中除了有类的版本信息、字段、方法、接口等描述信息外,还有一项信息就是常量池,用于存放编译期间生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中,另外翻译出来的直接引用也会存储在这个区域中。这个区域另外一个特点就是动态性,Java并不要求常量就一定要在编译期间才能产生,运行期间也可以在这个区域放入新的内容,String.intern()方法就是这个特性的应用。
3、直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。 但是这部分内存也被频繁地使用,而且也可能导致内存溢出问题。JDK1.4中新增加了NIO,引入了一种基于通道与缓冲区的I/O方式,它可以使用 Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些 场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内 存,肯定还是会受到本机总内存(包括RAM、SWAP区)大小以及处理器寻址空间的限制。