我们都知道内存非为堆栈两部分,今天就为大家来讲解下Java内存区域划分。
1.虚拟机内存区域
运行时数据区
程序计数器:线程私有,较小的内存空间,功能类似于PC寄存器,字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令。不会发生OutOfMemoryError异常
虚拟机栈:线程私有,它的生命周期与线程相同,描述的是java方法执行的内存模型:每个方法在执行的同时都会创建栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用到执行完成对应着一个栈帧在虚拟机栈中入栈到出栈的过程。这个区域规定了两种异常状况:线程请求的栈深度大于虚拟机所允许的深度抛出StackOverflowError异常;虚拟机栈动态扩展无法申请足够的内存抛出OutOfMemoryError异常。
本地方法栈:线程私有,与虚拟机栈的区别为虚拟机执行的时java方法服务,本地方法栈则为虚拟机使用的Native方法服务。此区域也会抛出StackOverflowError和OutOfMemoryError异常。
堆:线程共享,用来存放对象实例或数组,几乎所有的对象实例都在这里分配内存,可以处于不连续的内存空间。堆是垃圾收集器管理的主要区域,因此也称为GC堆。如果堆中没有内存用来完成分配实例,并且无法扩展则会抛出OutOfMemoryError异常。
方法区:线程共享,用来存放已被虚拟机加载的类信息、常量、静态变量、即使编译器编译后的代码等数据。很多人称之为“永久区”,垃圾收集行为在这个区域比较少见,但数据并非永久存在。当方法区无法满足内存分配需求时将抛出OutOfMemoryError异常。
2.栈帧
虚拟机栈结构
每一个方法从调用到执行完成对应着一个栈帧在虚拟机栈中入栈到出栈的过程。每个方法在执行的同时都会创建栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
局部变量表:是一个以字长为单位的数组,通过索引从0开始访问(0位置一般为this引用),用来存放方法内定义的局部变量(包括this引用)和方法参数,可存放boolean、byte、char、short、int、float、long、double、reference(对象的引用)或returnAddress(指向下一条字节码指令的地址)类型的数据
操作数栈:是一个以字长为单位的数组,通过压栈和出栈访问。操作数栈的每一个元素可以是任意Java数据类型。虚拟机把操作数栈作为它的工作区——大多数指令都要从这里弹出数据,执行运算,然后把结果压回操作数栈。举个例子,整数加法的字节码指令iadd在运行的时候要求操作数栈中接近栈顶的两个元素已经存入了两个int型的数值,当执行这个指令时,会将这两个int值和并相加,然后将相加的结果入栈。
动态链接:每个栈帧都包含一个执行运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。Class 文件中存放了大量的符号引用,字节码中的方法调用指令就是以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或第一次使用时转化为直接引用,这种转化称为静态解析。另一部分将在每一次运行期间转化为直接引用,这部分称为动态连接。
方法出口:方法执行后只有返回字节码指令或抛出异常才会退出,无论方法如何退出都需要返回到方法被调用的位置,程序才能继续执行。方法退出等同于当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令等。
3.Java内存区域划分补充
运行时常量池:是方法区的一部分,加载class文件时会将class文件中用于存放编译器生成的各种字面量和符号引用的内容存放在常量池中,运行期间也可以将新的常量放入常量池,例如String.intern()方法,受限于方法区,当方法区无法满足内存分配需求时将抛出。OutOfMemoryError异常
直接内存:不是虚拟机内存区域,受限于本机总内存,无法动态扩展时出现OutOfMemoryError异常。(Native函数库可以直接分配堆外内存,然后通过java堆中得DirectByteBuffer对象作为这块区域的引用进行操作,避免java堆和Native堆来回复制)。
4.注意
很多人吧java内存划分为堆(Heap)和栈(Stack),这里的栈指的是虚拟机栈或者是虚拟机栈中得局部变量表部分。
热点新闻