Java虚拟机规范定义了Java程序在运行时数据的存放区域,一些数据是在Java虚拟机启动时创建的,并且在在虚拟机退出时才会销毁,如方法区等,另外一些数据伴随着线程的创建和退出随之产生和销毁,如栈中存在的数据。Java虚拟机运行时数据区包含以下几个部分。
方法区
方法区(Method Area)与Java堆(Heap)一样,是由各个线程共享的内存区域,用来存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。《Java虚拟机规范》中把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫作“非堆”(Non-Heap),目的是与Java堆区分开来。这里还需要说明一个永久代(Permanent Generation)的概念,很多人喜欢把永久代和方法区混淆,JDK 7的HotSpot,已经把原本放在永久代的字符串常量池、静态变量等移出,而到了JDK 8,完全废弃了永久代的概念,改用与JRockit、J9一样在本地内存中实现的元空间(Meta-space)来代替。如果方法区的内存空间不足以分配,会抛出OutOfMemoryError错误。
方法区的大小不是固定的可以根据情况来调整,在JDK 7以前通过-XX:PermSize设置永久代初始大小,-XX:MaxPermSize设置永久代最大可分配空间,JDK8及以后可以使用-XX:MetaspaceSize和-XX:MaxMetaspaceSize设置元空间初始大小以及最大可分配大小。
这里还要理解一个常量池的概念,通常所说的常量池可以细分为Class文件常量池、运行时常量池、字符串常量池,具体区别如下。
Class常量池
Class文件定义了除了包含类的版本、字段、方法、接口等描述信息外,还包含常量池表(Constant Pool Table),通常所说的常量池(Constant Pool)所指代的是Class文件中所定义的常量池,这部分内容主要存放字面量(Java语言中定义的常量,如使用final修饰的值)和符号引用(表示JVM定义的Java关键字或基本类型与实际结构转换关系),符号引用主要包含
- 类和接口的全限定名(Fully Qualified Name)
- 字段的名称和描述符(Descriptor)
- 方法的名称和描述符
具体的可以参考Java虚拟机规范中所描述的常量池结构。
Constant Type | Value |
---|---|
CONSTANT_Class | 7 |
CONSTANT_Fieldref | 9 |
CONSTANT_Methodref | 10 |
CONSTANT_InterfaceMethodref | 11 |
CONSTANT_String | 8 |
CONSTANT_Integer | 3 |
CONSTANT_Float | 4 |
CONSTANT_Long | 5 |
CONSTANT_Double | 6 |
CONSTANT_NameAndType | 12 |
CONSTANT_Utf8 | 1 |
CONSTANT_MethodHandle | 15 |
CONSTANT_MethodType | 16 |
CONSTANT_InvokeDynamic | 18 |
运行时常量池
运行时常量池(Runtime Constant Pool)属于方法区的一部分。Class文件中的常量池部分内容在类加载后存放在方法区的运行时常量池中。运行时常量池和常量池的区别在于,常量池中的内容在编译器产生,存储在类文件中的;运行时常量池是在方法区,而且可在JVM运行期间动态向运行时常量池中写入数据,具备动态特性。运行时常量池存在于方法区也就受到方法区的约束,当创建一个类或者接口时,Java虚拟机不能分配出足够的内存空间时,同样会抛出OutOfMemoryError错误。
字符串常量池
字符串常量池是Java为String开辟的一块内存缓冲区,String本身具有不可变性,常量池的设计在提高性能同时减少内存开销。在不同版本的JDK中实现也有所不同,在JDK7以前字符串常量池存在于永久代,在JDK7起字符串常量池被迁移到堆中,在HotSpotVM中字符串常量池通过hash表实现,默认长度为1009,如果字符串常量池存在数据过多,则有可能引起hash冲突,从JDk7字符串常量池可以通过-XX:StringTableSize指定容量大小。StringTable存储的实例被所有类所共享,比如String的intern()方法,这是一个native方法,当调用此方法时,会通过equals方法判断字符串常量池中是否存在相同字符串,如果存在直接返回引用,如果不存在会添加到字符串常量池并返回引用。
堆
堆(Heap)是Java虚拟机所管理的一块最大的内存区域,由所有的线程共享的一块内存区域;堆内存在虚拟机启动时创建,用来存放对象实例,数组;Java堆是垃圾收集器管理的内存,在G1垃圾收集器之前,堆内存普遍采用分代设计思想,新生代,老年代,现代垃圾收集器已经不主张采用分代设计理论概念;Java堆既可以被实现成固定大小的,也可以是可扩展的,不过当前主流的Java虚拟机都是按照可扩展来实现的(通过参数-Xmx和-Xms设定)。如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError错误。
虚拟机栈
虚拟机栈(VM Stack)为线程私有,每个方法执行时,虚拟机都会创建栈帧存储局部变量表(包含Java的基本数据类型,以及对象的引用,非对象本身)、操作数栈、动态连接方法出口等信息,方法从被调用到执行结束,对应着一个栈帧在虚拟机中从入栈到出栈的过程。基本数据类型在局部变量表中的存储空间以局部变量槽(slot)来表示,64位长度的long和double占用两个变量槽,其余数据类型占用一个,局部变量表所需要的内存空间在编译期完成,因此进入方法时,每个方法在栈帧需要分配的空间时确定的,运行期间并不会改变局部变量表的大小(即变量槽的数量),每个槽的空间大小根据虚拟机的实现而定。
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常。
本地方法栈
本地方法栈(Native Method Stack)类似于虚拟机栈的作用,区别在于虚拟机栈用来执行Java的方法,本地方法栈为虚拟机使用到的笨的方法服务。
程序计数器
程序计数器(Program Counter Register)是Java内存中较小的一部分内存空间,由每个Java线程所独享,可以理解为当前线程执行的字节码行号指示器,Java中程序的执行往往是多线程的,在某一个确切的时刻,一个处理器内核直会执行线程中的一条指令,每个线程都是在不停的切换执行,为了保证切换后可以执行到正确的位置,每个线程都要有一个独立的程序计数器,每个计数器之间互不影响。
虚拟机栈、本地方法栈、程序计数器伴随着线程的产生销毁而产生销毁,栈中的栈帧随着方法的进入和退出执行着入栈和出栈的操作,基本上每个栈帧的内存大小是在类结构确定下来时就是已知的,这几个区域的内存分配和回收都具有确定性,方法或者线程结束时,内存就自然被回收了。堆和方法区的内存分配和回收是具有动态特性的,接口的实现类,和对象创建的多少只有在运行时才可以感知,垃圾收集器所管理的也就是这部分内存区域。