likes
comments
collection
share

浅析JVM内存分区

作者站长头像
站长
· 阅读数 3

内存管理是开发者必须掌握的基本功,不然程序总是会在各种难以捉摸的错误中崩溃,一些语言,例如C、C++开发者们自己申请内存,使用完自己释放,但是不当的代码书写习惯往往导致内存泄露,引用空指针等等错误,而Java借助于虚拟机帮我们完成了许多工作,使开发者从内存管理的深坑中爬出来了,但是由于隔着这层虚拟机,出现问题时的应对策略更显功力,需要对虚拟机内存管理机制的深入了解。

这篇文章只是粗浅的介绍下虚拟机内存区域的大概分布,让初学者在脑海中有个大概印象,而印象的开始则借助于下面的一幅图:

浅析JVM内存分区

Java内存区域分为大的两个区域,一部分是线程共享的,另一部分则是每个线程所独有的。刚开始了解编程时,大致就有印象,栈内存存在于方法体中,定义的那些变量什么的都是栈内存,方法结束就没了,堆内存则是使用new关键字申请出来的。当然这只是粗线的认识,下面一块块的说上图中的内存分布。

程序计数器

类似于CPU中的PC寄存器,用于存放下一条指令的地址,但是虚拟机不使用CPU的程序计数器,而是自己在内存里设立一片区域模拟CPU的程序计数器。改变计数器的值来选取下一条需要执行的字节码指令,包括分支、循环、跳转、异常、线程恢复等基础功能都依赖于计数器。

Java的每个线程都有其独立的计数器,计数器之间互不影响,这样当多线程操作时,一个挂起的线程在恢复时,仍然能够从计数器中恢复之前运行到的地方,继续执行。所以说程序计数器也是线程隔离的。执行Java方法时,计数器中存放的是虚拟机字节码的地址,而运行Native方法时则是空(undefined)。该区域不会产生OutOfMemoryError。

虚拟机栈

Java虚拟机栈也是线程私有的,它的生命周期等同于线程的生命周期。虚拟机栈描述的是Java方法执行时的内存模型。当方法执行时,会创建一个栈帧,用于存储方法执行期间所用到的数据结构,包含局部变量表,操作数栈,动态链接,方法出口等信息。

当一个方法执行时,一个包含以上元素的栈帧入栈,当方法退出时,栈帧出栈。一般我们都会知道内存区分为堆区和栈区,实际上也是个粗浅的分法,栈指的就是虚拟机栈,而其中最重要的部分就是局部变量表。

Java的垃圾收集器是不会去回收栈上的内容的,因为栈上的内容总是随着方法的结束自动释放。局部变量表包含着各种编译期已知的基本数据类型对象引用returnAddress。基本数据类型就是Java的8大基本数据类型(boolean,byte,char, short, int, float, long, double),对象引用,你可以把它当成指向实际对象地址的指针或者一个代表对象的句柄,returnAddress则是一条字节码指令的地址。当进入一个方法时,它所需要分配的空间在编译期就是已知的了。

在虚拟机栈中可能会报以下两种异常:

  • StackOverflowError: 线程请求的栈深度大于所允许的深度
  • OutOfMemoryError: 大多数虚拟机栈是可以动态扩展的,如果无法申请到足够内存,就会抛出。

本地方法栈

区别于虚拟机栈执行的是Java方法,本地方法栈则是虚拟机使用的Native方法服务,我们在看一些库的源码时正常定位到最后就是用Native方法实现的,但是在虚拟机规范里对本地方法使用的语言,使用方式进行硬性规定,所以虚拟机可以任意实现它。HotSpot中本地方法栈和虚拟机栈是合在一起的。

至少从学习C语言时,我们就听说malloc方法会在堆上分配空间。Java的堆上也是分配实例对象的。虚拟机规范上讲,基本所有的对象实例和数组都分配的堆上,例如上面栈上对象引用指向的对象,都是在堆上分配的。但是随着编译器技术的发展,所有对象都在堆上分配就不是那么纯粹了。

堆也是垃圾收集(GC)的主阵地。现代收集器基本都采用了分代收集算法,所以堆也可以划分为新生代和老年代,再细致点还有Eden空间,From Survivor空间和To Survivor空间。由于虚拟机实现了自动垃圾收集,所以在Java中,堆中new出的对象是不需要手动释放的。

我们可以通过-Xmx-Xms控制堆的默认大小,如果在堆中再也申请不到内存,则会抛出OutOfMemoryError异常。

方法区

方法区也是各个线程间共享的区域,一般存储已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。一般很多人也把方法区称为永久代。其实仅仅是HotSpot团队把GC分代收集也扩展到方法区了,让垃圾收集器可以一块回收方法区的内存,但是其它的虚拟机实现没有这么做,也不存在永久代的,HotSpot自身也已经在JDK1.7上移除了永久代中的常量池。

相对而言,垃圾收集在方法区是不怎么出现的。这个区域主要回收的是常量池和类型的卸载,但是实际上对二者的回收发生的条件极为苛刻,很少发生收集。