java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外的人想进去,而墙内的人想出来。[来自深入理解java虚拟机]
对于java程序员来说,在虚拟机自动内存管理机制的帮助下,不需要对每个new的对象手动释放内存,也不容易触发内存泄漏和内存溢出的问题,但是一旦触发这个问题,如果我们不了解虚拟机对内存的使用原理,是很难找到问题的。
所谓java的内存区域,主要是运行时数据区域。java虚拟机在运行java程序时,将它所管理的内存划分成不同的区域,每个区域有各自的作用对象和生存周期。根据java虚拟机规范1.7的规定,把内存划分成这几个部分:程序计数器,java虚拟机栈,本地方法栈,java堆,方法区。如图一所示,
图一 java运行时数据区域
接下来就分别来描述这几块区域的定义以及作用:
程序计数器
程序计数器时一块很小的内存区域,用来记录线程执行到的位置。因为java虚拟机的多线程时通过线程轮流切换和分配处理器执行的方式实现的,所以当一个线程恢复时,能够找到到正常的执行位置。由此可见,此区域也是线程私有的,各个线程之间互不影响。
java虚拟机栈
虚拟机栈和程序计数器一样也是线程私有的,生命周期与所属的线程同步。当虚拟机在执行一个方法的同时会创建一个栈帧,用来存储局部变量,方法出口,动态链接等信息。一个方法的调用和执行,就对应着一个栈帧从虚拟机栈中入栈和出栈的过程。
局部变量表中存放基本数据类型,int,boolean,byte,char,short,long,double,float,对象的引用,这些在编译期间她们的大小就是已知的,当执行一个方法时,需要在栈帧中分配多少内存也是固定的,在方法执行的过程中时不变的。
这个区域可能会出现两种异常:StackOverflowError,OutOfMemoryError,当线程请求的栈深度大于虚拟机允许的最大深度会抛出StackOverflowError异常,不过一般虚拟机都会动态扩展,但是如果扩展时没有足够的内存就会抛出OutOfMemoryError。
本地方法栈
本地方法栈和虚拟机栈的作用时相似的,只不过本地方法栈为虚拟机使用到的Native方法服务。有些虚拟机就把本地方法栈和虚拟机栈合二为一了。
java堆
java堆与前几块区域不同,它是虚拟机中所有线程共享的一块区域,在虚拟机启动时创建。对于大部分应用来说,java堆时虚拟机所管理的内存中最大的一块。用来存放对象实例和数组。
java堆也是垃圾收集器所管理的主要的区域,因此有时它也被称作GC堆(Garbage Collected Heap)。由于现在的垃圾收集器一般都采用分代收集算法,所有java堆又被分为新生代和老年代,新生代中存活的对象比较少,老年代中存活的对象占大部分,垃圾回收这部分内容在后续的文章中将会描述。
另外java堆物理上可以分配在不连续的内存空间中。当没有足够的内存分配实例或数组,并且无法再扩展时,就会抛出OutOfMemoryError。
方法区
方法区和堆一样也是线程共享的区域,用于存放已经被虚拟机加载的类信息,常量,静态变量等。它在java虚拟机规范中被描述为java堆的一个逻辑部分,而且对它的限制也比较宽松,例如可以分配在不连续的内存空间上,大小可以扩展也可以固定(不同虚拟机实现不一样)。有时,方法区又被称作永久代,但是进入到此区域的数据一样会被回收。这个区域的内存回收主要针对常量池的回收和类卸载。不过这个部分的回收限制比较多,所以回收的效率比较低。它也会抛出OutOfMemoryError。
运行时常量池
运行时常量池时方法区的一部分。它用于存放编译期间生成各种字面量和符号引用,在类加载后进入方法区的运行时常量池。java虚拟机对Class文件的格式又严格的规定,每个字节用于存放那种数据都符合规范时才能被虚拟机认可,装载和执行,这部分内容在后续文章也会有描述。
运行时常量池还具有动态性,并非只有在编译期间生成的常量才会放入运行时常量池,运行期间也可能将生成的常量放入其中,例如String类的intern()方法。另外它也会抛出OutOfMemoryError
直接内存
直接内存不是java运行时数据去的一部分,也不是虚拟机规范中定义的内存区域,但是它也被频繁的调用,有时也会抛出OutOfMemoryError。在JDK1.4之后引用了一种基于通道和缓冲区的I\O方式,它使用Native函数库直接分配内存,然后通过存储在java堆中的DirectByteBuffer对象作为内存的引用进行操作,避免了java堆和Native堆中来回复制,提高了性能。
直接内存不受java堆内存大小的限制,但是也会受设备总内存大小以及处理器寻址空间的限制。