Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,有的区域随着虚拟机进程的启动而存在,有些区域则是依赖用户线程的启动和结束而建立和销毁。根据《Java虚拟机规范》的规定,Java虚拟机所管理的内存会包括以下几个区域:
备注:方法区和堆是所有线程共享的内存区域,而虚拟机栈、本地方法栈以及程序计数器是每个线程所独享的一片区域。
程序计数器
程序计数器是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。在JAVA虚拟机中,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、跳转、循环、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。每一个线程都需要有一个独立的程序计数器,各个线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
虚拟机栈
虚拟机栈描述的是JAVA方法执行的内存模型。每个方法在执行时,都会创建一个栈帧,用于存储局部变量(基础类型的变量,如int/short/long/float等)、对象句柄、操作数栈、动态链接、方法出口等信息。虚拟机栈中有很多的栈帧,因为方法是嵌套调用的嘛。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
举例来说,有如下方法被调用时,
1 | public int test(int a, int b) { |
如果局部变量是Java的8种基本基本数据类型,则存在局部变量表中,如果是引用类型。如new出来的Object,局部变量表中存的是引用,而实例在堆中。
在C++内存管理中,我们知道“栈内存”和“堆内存”,JAVA中“栈内存”其实指的就是虚拟机栈。JAVA虚拟机栈也是线程私有的,它的生命周期与线程相同。
本地方法栈
本地方法栈与虚拟机栈所发挥的作用是非常类似的,它们之间的区别不过是虚拟机栈为虚拟机执行JAVA方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。
Java堆
Java堆是Java虚拟机所管理的内存中最大的一块,Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。Java堆内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。Java堆是Java GC的最主要考虑的内存空间。因此Java堆也被称为“GC堆”。
从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以Java堆还可以细分为新生代和老年代,新生代又可以细分为Eden空间、From Survivor、To Survivor空间等。之所以对Java堆内存进一步划分是为了更好地回收内存,或者更快地分配内存。
在JDK 1.8中移除整个永久代,取而代之的是一个叫元空间(Metaspace)的区域(永久代使用的是JVM的堆内存空间,而元空间使用的是物理内存,直接受到本机的物理内存限制)。
如下是JVM在创建一个新对象时分配堆内存的过程图如下:
JVM默认情况下,堆内存中不同的内存区域的大概占比如下:
新生代
所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。年轻代分三个区,一个Eden区,两个Survivor区(一般而言)。大部分对象首先在Eden区中生成。为了整理内存的需要,对象会在Minor GC时在两个Survivor区复制来复制去,Survivor总有一个是空的。同时,根据程序需要,Survivor区是可以配置为多个的(多于两个),这样可以增加对象在年轻代中的存在时间,减少被放到年老代的可能。
年老代
在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
持久代
用于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=
方法区/元空间MetaSpace
JDK1.8之后方法区就改为元空间MetaSpace。
方法区用于存储已被虚拟机加载的类信息、方法、常量、静态变量、即时编译器编译后的代码等数据。
需要明确说明的是,程序计数器、虚拟机栈以及本地方法栈三个区域随线程而生,随线程而灭。因此这几个区域的内存分配和回收都具备确定性,在这几个区域内就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随回收了。而JAVA堆和方法区则不一样,这部分的内存的分配和回收都是动态的,垃圾收集器所关注的是这部分的内存。
运行时常量池
运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池信息(用于存放编译期生成的各种字面量和符号引用)。既然运行时常量池时方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常。
JDK1.7及之后版本的JVM已经将运行时常量池从方法区中移了出来,在Java堆(Heap)中开辟了一块区域存放运行时常量池。
直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致OutOfMemoryError异常出现。
JDK1.4中新加入的NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓存区(Buffer)的I/O方式,它可以直接使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在Java堆和Native堆之间来回复制数据。
本机直接内存的分配不会受到Java堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。