JVM的内存划分

May 7, 2016


  JAVA和C++的一个重要区别就是JAVA虚拟机拥有自动内存管理机制,写JAVA时不需要像C++一样为new去配delete来自己管理内存。这样非常方便,可以说是减轻了开发过程中繁琐的工作,但为了更好地排查各种错误,了解JAVA虚拟机的内存划分是很有必要的。

  为了展现JAVA如何划分内存,这里引入一张非常著名的图片:

图片2

  需要指出的是,这种划分方式只是JVM规范给出的划分,是逻辑上的分区方式,物理上区分一般都只分为堆和栈。对于方法区,不同的虚拟机实现不同,比如在HotSpot虚拟机上方法区在物理上就是属于堆的。

  由上图可知JAVA虚拟机运行时,内存被划分为五个区域:方法区、虚拟机栈、本地方法栈、堆和程序计数器。它们有些是线程共享,有些是线程隔离的。线程隔离的分区随着线程创建而建立,随着线程结束而消亡,生命周期与线程一致,线程共享的分区生命周期则是与JVM一致。下面就来逐个了解这些分区的功能:

方法区:方法区是一个线程共享的区域,主要存储的都是已经加载好的类的信息、常量和静态变量等。这个区域是较少出现GC的,JAVA虚拟机规范并没有强制规定方法区需要实现GC,因为此区域进行的GC通常效果不佳。比方说一个加载好的类,要让它被回收,必须保证这个类完全不可触及,也就是这个类的ClassLoader被GC,且已经没有任何实例(通过反射获取也不行),要同时满足这么多个条件挺难的。方法区内存不够的时候,会抛出OOM。

虚拟机栈:JAVA虚拟机栈是线程私有的区域,线程每执行一个方法就会在自己的栈空间开辟一个栈帧,用来保存局部变量表等方法相关的信息。栈帧大小一般都是128k或者256k,所以如果在一个栈帧里不断调用方法(比如无限递归),就会造成栈帧空间不足,报StackOverFlow,这时候还不会报OOM。很多栈帧的大小是可以动态拓展的,拓展的时候空间不够,才会报OOM。 本地方法栈:本地方法栈其实可以类比虚拟机栈,也是线程私有,用来存储调用的方法的局部变量表的。本地方法栈与虚拟机栈的最主要区别是本地方法栈是给本地方法使用的,比如开发中调各种第三方库时引入的.so文件。因为Native方法实现多样,JAVA虚拟机规范对本地方法栈的实现也没做什么强制规定。在最常用的HotSpot虚拟机中本地方法栈和虚拟机栈甚至都合并了。

堆:这块内存是线程共享的,由于它主要存的是对象的实例,所以是JAVA虚拟机内存分片中最大的一块。各种new出来的对象——前面提到的局部变量表里面reference指向的对象,各种类的实例对象等等,都要在堆中分配内存。这块内存是GC的主要区域,在使用分代收集算法的收集器时堆还会再被分成不同区域。堆被填满的时候也会报OOM,想报这个错其实很简单,不断new新对象就可以了。

程序计数器:这块区域也是线程私有的,用来标记当前线程执行到的位置,以便CPU切走再切回来的时候知道从哪开始执行。不过这个标记是对于JAVA字节码而言的,如果线程执行的是Native方法,计数器的值是空的。这个区域又小又简单(相对于其它区域而言),不会报OOM异常。

  接下来可以用一个例子来做一个总结,当我们new一个对象时会发生什么呢?JVM首先会在方法区寻找这个对象所属的类的信息,看看这个类之前是否被加载过了,如被加载过就直接开始分配内存,否则要加载这个类。类加载完后分配内存,因为加载完后需要的内存大小就知道了,所以需要的堆内存大小也知道了。因为给对象分配内存这种情况十分普遍,堆内存又是线程共享的区域,通常每个线程会在堆中预先拿一小块自己专属的区域,叫做TLAB,TLAB没用完时线程直接在其上分配内存,不需要加各种锁。分配内存完成后把得到的空间初始化为空,这个时候new出来的引用变量就不是null了而是分配的地址,但是它指向的内存里面的值是空的。然后再执行构造块和构造方法,将数据填充到分配好且已经清零的堆内存中去。