JAVA的运行时数据区,老生常谈。
我们常说的JAVA的运行时数据区包括:程序计数器、虚拟机栈、本地方法栈、方法区、堆
,其中前三者是线程私有的,后二者是线程公有的。这里为什么以线程为划分私有/公有的依据,是因为线程是JVM调度的最小单位。
线程私有
程序计数器
在汇编语言中,程序计数器是指CPU中的寄存器,它保存的是程序当前执行的指令地址(也可以说保存下一条指令的所在存储单元的地址)。在JVM中的程序计数器也是类似的功能,用于记录当前线程执行到的字节码的行号,字节码的解释器工作的时候就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
由于在JVM中,多线程是通过线程轮流切换来获得CPU执行时间的,因此,在任一具体时刻,一个CPU的内核只会执行一条线程中的指令,因此为了能够使得每个线程都在线程切换后能够恢复在切换之前的程序执行位置,每个线程都需要有自己独立的程序计数器,并且不能相互干扰
,否则就会影响到程序的正常执行次序。因此,可以这么说,程序计数器是每个线程所私有的。
注意:此内存区域是JVM里面唯一一个不会发生内存溢出(OOM OutOfMemoryError)的区域。
虚拟机栈
虚拟机栈也就是我们常说的JVM的栈,栈中存放着栈帧,每一个方法执行会产生一个栈帧。
方法开始执行时,会在虚拟机栈中压入一个栈帧,方法执行结束时,会将栈帧弹出栈。栈帧中包含局部变量表、操作数栈、动态链链接、方法出口等信息构成
。
局部变量表:存放编译器可知的各种基本数据类型、对象引用类型和returnAddress类型(指向一条字节码指令的地址:函数返回地址)。long、double、占用两个局部变量控件的Slot。局部变量表所需要的内存空间在编译器确定,当进入一个方法时,方法在栈帧中所需要分配的局部变量控件是完全确定的,不可动态改变大小
。详细内容可以使用JDK自带的工具javap对class进行反编译查看。
操作数库:后进先出LIFO,最大深度由编译期决定。栈帧刚建立时,操作数栈为空,执行方法操作时,操作数栈用于存放JVM从局部变量表复制的常量或者变量,提供提取,及结果入栈,也用于存放调用方法需要的参数及接受方法返回的结果。操作数栈可以存放一个JVM中定义的任意数据类型的值。在任意时刻,操作数栈都有一个固定的栈深度,基本类型除了long、double占用两个深度,其他占用一个深度。
动态链接:当一个方法被执行后,有两种方式退出该方法:执行引擎遇到任意一个方法返回的字节码指令 或遇到了 异常 ,并且该异常没有在方法体内得到处理。无论采用何种方式退出,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行。方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说方法正常退出时,调用者的程序计数器的值就可以作为返回地址,栈帧中很可能保存了这个计数器的值,而方法异常退出时,返回地址是要通过异常处理器来确定的,栈帧中一般不保存这部分信息。
如果线程请求的栈深入大于虚拟机所允许的深度,将抛出StackOverflowError异常。如果虚拟机栈可以动态扩展(大部分虚拟机允许动态扩展,也可以设置固定大小的虚拟机栈),但是无法申请到足够的内存,会抛出OutOfMemorError。
本地方法栈
本地方法栈与虚拟机栈所发挥的作用很相似,他们的区别在于虚拟机栈为执行Java代码方法服务,而本地方法栈为Native方法服务。
线程公有
堆
因为是线程公有的,几乎所有的线程都把自己产生的实例对象放在堆上。所以堆是JVM运行时数据区中所在空间最大的一块,我们说的垃圾回收也主要发生在堆上。如果按GC的不同回收算法,堆又可以划分为“老年代”,“新生代”,“新生代”再划分为Eden空间、From Survivor空间、To Survivor空间。关于垃圾回收,在这里再讨论。
堆可以是固定大小的,也可以通过设置配置文件来设置该为可扩展的。如果堆上没有内存进行分配,并无法进行扩展时,将会抛出OutOfMemoryError异常
方法区
方法区中存储的是每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码
等。在Class文件中除了类的字段、方法、接口等描述信息外,还有一项信息是常量池,用来存储编译期间生成的字面量和符号引用。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却又一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来.
原则上,如何实现方法区属于虚拟机实现细节,不受虚拟机规范约束,但是使用永久代来实现方法区,现在看来并不是一个好主意,因为这样更容易遇到内存泄漏问题(永久代有-XX:MaxPermSize的上限,J9和JRockit只要没有触碰到进程可用内存的上限,例如:32位操作系统中的4GB,就不会出现问题),而且有极少的方法(例如String.intern())会因为这个原因导致不同虚拟机下有不同的表现.因此,对于HotSpot虚拟机,根据官方发布的路线图信息,现在也有放弃永久代并逐步采用Native Memory来实现方法区的规划了,在目前已经发布的JDK1.7的HotSpot中,已经把原本放在永久代的字符串常量池移出.
运行时常量池(Runtime Constant Pool)是方法区的一部分.Class文件中除了有类的版本/字段/方法/接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将类在加载后进入方法区的运行时常量池中存放.
运行时常量池相对于Class文件常量池的另外一个重要特征就是具备动态性,Java语言并不要求常量一定只有在编译期才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法。
public String intern()
返回字符串对象的规范表示。
最初为空的字符串池由String类String 。
当调用intern方法时,如果池已经包含与equals(Object)方法确定的相当于此String对象的字符串,则返回来自池的字符串。 否则,此String对象将添加到池中,并返回对此String对象的引用。
由此可见,对于任何两个字符串s和t , s.intern() == t.intern()是true当且仅当s.equals(t)是true 。
所有文字字符串和字符串值常量表达式都被实体化。 字符串文字在The Java™ Language Specification的 3.10.5节中定义。
结果
一个字符串与该字符串具有相同的内容,但保证来自一个唯一的字符串池。
注意:在JVM规范中,没有强制要求方法区必须实现垃圾回收,很多人习惯将方法区称为"永久代",是因为HotSpot虚拟机以永久代来实现方法区,从而JVM的垃圾处理器可以像堆区一样管理这部分的区域,从而不需要专门为这部分设计垃圾回收机制。不过JDK8之后,Hotspot虚拟机将运行时常量池从永久代移除了。然后引入了一个新的概念"元空间"
。