Java虚拟机在执行java程序的过程中会把他管理的内存划分为若干个不同的数据区域各自用途、创建以及销毁时间各不相同。有的随着虚拟机进行的启动而存在,有的区域依赖于线程的启动和结束而建立以及销毁。如图:
1.程序计数器
Jvm将这个计数看作当前线程(意味着只能支持单线程)执行某条字节码的行号指示器,会根据计数器的值来选取下一条需要执行的字节码指令。这个属于线程私有,不可共享,如果共享会导致计数混乱,无法准确的执行当前线程需要执行的语句。
如果一个线程正在执行java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果执行的是Native方法,这个计数器的值为空(Undefined)。
该区域是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError的情况,因为只需要表示计数器的值,这个值大小是恒定的。
注:一个Native Method就是一个Java调用非Java代码的接口。一个Native Method是这样一个Java的方法:该方法的实现由非Java语言实现,比如C或C++。
2.Java虚拟机栈
栈内存就是指虚拟机栈。Java中每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
虚拟机栈表述的是Java方法执行的内存模型:每个方法在执行的同时,都会创建一个栈帧(Stack Frame),用于存放局部变量表、操作数栈、动态链接、方法出口等。口头所说的“栈内存”就是虚拟机栈中局部变量表部分,局部变量表存放了编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,不同于对象本身,可能指向一个对象起始地址的引用指针,也可能指向一个代表对象的句柄或其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。
其中64位长度的long和double类型的数据会占用两个局部变量空间,其余数据类型只有一个。局部变量表所需的内存空间在编译期间完成分配。
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出*Error异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
3.本地方法栈
虚拟机栈用来执行java方法,而本地方法栈用来执行本地方法。
抛出异常的情况和虚拟机栈一样会抛出*Error异常和OutOfMemoryError异常。
4.Java堆
是jvm中内存最大、线程共享的一块区域。所有线程共享的一块内存区域,在虚拟机启动时创建。唯一的目的是存储对象实例。这里也是垃圾收集器主要收集的区域(GC堆:GarbageCollected Heap)。由于现代垃圾收集器都采用的是分代收集算法,所以java堆也分为新生代和老年代。
线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB),与存放内容无关,都存的是对象实例。
Java堆可以处于物理上不连续的内存空间中,只要逻辑上连续即可。可以通过参数-Xmx(jvm最大可用内存)和-Xms(jvm初始内存)来调整堆内存,如果在堆中没有内存完成实例分配,并且堆也扩大至无法继续扩展时,会出现OutOfMemoryError的错误。
5.方法区("永久代")
Jvm中内存共享的一片区域,用来存储类信息、常量、静态变量、class文件。垃圾收集器也会对这部分区域进行回收,比如常量池的清理和类型的卸载,但是效果不理想。其 实现想像堆一样-XX:MaxPermSize设定管理这部分内存,而不用专门为该区进行内存管理编写代码。但是容易造成内存溢出,有逐步改为Native Memory来实现方法区的规划了,JDK1.7中已经把原本放在永久代的字符串常量池移出。
方法区内存不够用的时候,也会抛出OutOfMemoryError错误。
6.运行时常量池
运行时常量池是方法区的一部分(Runtime Constant Pool)。Class文件除了有类的版本、字段、方法、接口等信息,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这些内容在类加载后进入方法区的运行时常量池存放。
Java虚拟机堆Class文件有严格规定,但是对运行时常量池没有,一般来说除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。运行期间也可将新的常量放入池中,如String类的inter()方法。
代码示例:
public class Test { public static void main(String[] args){
String str1 = new StringBuilder("计算机").append("软件").toString();
System.out.println(str1.intern() == str1); String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern() == str2);
}
}
在jdk1.6中,代码输出结果为false,false;在jdk1.7中,代码输出结果为true,false。 为什么会出现不一样的结果呢? 因为在jdk1.6中,intern()方法会把首次遇到的字符串实例复制到方法区中,然后返回方法区中这个字符串实例的引用,而StringBuilder创建的字符串实例在堆上,
因此两者返回的肯定不是同一个引用,所以两个数据结果都为false。 而在jdk1.7中,intern()方法不再复制实例,只是在常量池中记录首次出现的实例的引用,因此如果该字符串是首次出现的,那么intern()返回的结果
和StringBuilder创建的是同一实例。所以第一个输出的结果为true。但是因为“java”这个字符串在执行StringBuilder.toString()以前已经出现过了,
常量池中存的不是str2实例的引用,不符合“首次出现”原则,因此返回的结果不是指向同一个实例,即输出为false。
属于方法区,自然也受方法区限制,当常量池无法再申请到内存时会抛出OutOfMemoryError错误。
7.直接内存
直接内存(Direct Memory)不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的数据区域。
在JDK1.4中加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,可以使用Native函数库直接分配堆外内存,通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。在一些场景中避免在Java堆和Native堆中来回复制数据。
本级直接内存不会受Java堆大小限制,受本机总内存大小以及处理器寻址空间限制,动态扩展时超过物理内存限制会出现OutOfMemoryError异常。
总体概括如图: