Java内存区域

运行时数据区域

Java虚拟机(JVM)在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。而在JDK1.8前后数据区域的划分略有不同,下面会介绍到。
JDK1.8之前:
JDK1.8:
因此根据上面的运行时数据区划分图可以看出:

线程私有的:

  • 程序计数器
  • 虚拟机栈
  • 本地方法栈

线程共享的:

  • 方法区
  • 直接内存(非运行时数据区的一部分)

下面就按照上面的顺序逐个进行了解。

程序计数器

程序计数器是一块较小的内存空间。在虚拟机的概念模型里面,字节码解释器工作时需要知道该执行哪一条字节码指令,而程序计数器的作用就是,通过改变程序计数器的值,来让字节码解释器知道,下一条需要执行的指令是什么。

其次,Java虚拟机的多线程执行,是通过线程之间轮流执行,而对于一个处理器(如果是多核处理器,那么就是一个内核),在任意一个确定的时刻,只会执行一条线程中的指令。因此,为了避免一个线程过长时间(可能因为计算时间过长或者陷入死循环等原因)占用处理器,导致系统崩溃,所以处理器会给每个线程分配执行的时间,如果当分配的时间结束时,该线程的任务还没有执行完,处理器会被剥夺并分配给另一个线程,直到到达下一次该线程的时间片,处理器才会切换回来,继续执行该线程。因此,为了线程切换后能恢复到正确的执行位置,所以每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

从上面的介绍中我们知道程序计数器主要有两个作用:

  • 字节码解释器通过改变程序计数器,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理等。
  • 在多线程情况下,程序计数器用于记录当前线程执行的位置,从而当线程切换回来的时候,能够知道该线程上次执行到哪里,接下来该执行什么指令。

注意:

  • 如果线程正在执行的是一个Java方法,那么这个程序计数器是正在执行的方法的虚拟机字节码指令的地址。
  • 如果线程正在执行的是一个Native方法,那么这个程序计数器则为空(Undefined)。因此程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。

Java虚拟机栈

Java虚拟机栈描述的是Java方法的内存模型,每次方法调用的数据都是通过栈传递的。而栈中储存的是一个个的栈帧,栈帧就是每个方法在执行的时候都会创建一个栈帧(Stack Frame),栈帧用于储存局部变量表、操作数栈、动态链接、方法出口等信息。因为线程每调用一个方法从开始到结束,都意味着一个栈帧在虚拟机栈中从入栈到出栈的过程。

Java内存可以粗糙的分为堆内存(Heap)和栈内存(Stack),其中的栈就是Java虚拟机栈,或者说是Java虚拟机栈中的局部变量表部分。

局部变量表存放了编译器可知的各种(八种)基本数据类型(boolean、byte、char、short、int、float、double、long)、对象引用(不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和returnAdress类型(指向一条字节码指令地址)。
其中64位的长度的double和long类型的数据都会占用两个局部变量空间,其余数据只会占用一个局部变量空间。局部变量表所需内存空间在编译期间完成分配,因此当进入一个方法时,这个局部变量表的大小就已经完全确定了,运行期间不会改变其大小。

Java 虚拟机栈会出现两种异常:StackOverFlowError 和 OutOfMemoryError

  • StackOverFlowError:若Java虚拟机栈的内存大小不允许动态扩展,那么如果线程请求的栈深度大于虚拟机所允许的最大深度,那么就会抛出StackOverFlowError异常。
  • OutOfMemoryError:若Java虚拟机栈的内存大小允许动态扩展,那么如果线程在扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。

Java方法的返回方式有两种:return语句抛出异常,不管哪种方法,都会导致栈帧出栈。

本地方法栈

本地方法栈的作用与Java虚拟机栈的结构和作用几乎完全一样,可以认为二者唯一的区别就是:Java虚拟机栈为虚拟机执行Java方法(也就是字节码)服务;而本地方法栈为虚拟机执行Native方法服务。甚至在HotSpot虚拟机栈中将两者合二为一。

总结得到一点:程序计数器、Java虚拟机栈和本地方法栈都是线程所私有的,故而他们的生命周期和线程相同,它们的生命周期随着线程的创建而创建,随着线程的结束而死亡。

Java堆

Java堆(Heap)是Java虚拟机所管理的内存中最大的一块。Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存的唯一目的就是:存放对象实例,几乎所有的对象实例都在这里分配,在Java虚拟机规范中的描述是:所有对象的实例以及数组都要在堆上分配。,但是随着JIT编译器的发展,这种情况也不是那么绝对的了。

java堆也是垃圾收集器管理的主要区域,因此也被称为 GC堆 ,从垃圾回收的角度看,Java堆中还可细分为:新生代和老生代;再度细分可分:Eden 空间、From Survivor、To Survivor 空间等为;大部分情况下,对象都会首先在Eden区域分配,再一次新生代垃圾回收后,如果对象还存活,则会进入s0或是s1在,并且对象年龄还加一,当他的年龄增加到一定程度(默认为15岁)时,就会被划分到老年代中。

不论如何划分,都与存放的内容无关;不论哪个区域,存储的都是对象的实例。进一步划分的目的是为了更好的回收内存,更快的分配内存。

Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。

Java堆的内存大小可以是固定大小的,也可以是可扩展的(大部分都是)。如果在堆中没有内存来完成实例的分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

方法区

方法区同样是各个线程共享的内存区域,它用于储存已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区有一个别名叫“非堆(Non-Heap)”,目的就是为了将其与Java堆区分开来。

仅在HotSpot虚拟机中,方法区也被称为“永久代”,仅仅是因为在HotSpot虚拟机中把GC分代收集扩展至方法区,这样可以省去专门为方法区编写内存管理代码的工作。但是问题也因此而来,因为永久代有大小上限,所以当触碰到内存大小的上限时,会抛出OutOfMemoryError异常。

所以在JDK1.8之后,永久代被彻底删除了,取而代之的是元空间(MetaSpace),与永久代有JVM本身内存大小上限的限制不同的是,元空间使用的是直接内存,受到的是本机可用内存的上限限制,只有当触碰到本地内存的极限时,才会抛出OutofMemoryError异常(概率极小)。

与java堆一样,方法区同样不需要连续的内存和可以选择固定大小或可扩展外,还可以选择不实现垃圾收集。相对而言垃圾收集行为在该区域比较少见,因为该区域内存回收目标主要是针对常量池的回收和对类型的卸载。

运行时常量池

JDK1.7之前,运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池信息,用于存放编译期生成的各种字面量符号引用
运行时常量池相对于Class文件常量池还有一个重要特征是具备动态性,将运行期间可能得到的新的常量放入池中。
因此既然运行时常量池是方法区的一部分,所以当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。

但是在JDK1.7及其之后版本的JVM中,将运行时常量池从方法区中移了出来,在Java堆中开辟了一块内存存放运行时常量池,这样也更加方便于垃圾回收的工作。

直接内存

直接内存既不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这一部分内存被频繁的使用,而且也可能导致OutofMemoryError异常。

在JDK1.4中新加入了NIO类,引入了一种基于通道(Channel)于缓存区(Buffer)的I/O方式,它使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在Java堆和Native堆之间来回复制数据。

虽然本机直接内存并不会收到Java堆的内存大小限制,但是显然会受到本地总内存的大小限制,因此也可能会在动态扩展时抛出OutOfMemoryError异常。