Java内存分区

来源:互联网 发布:中国石油大学 知乎 编辑:程序博客网 时间:2024/06/05 23:51

兴趣是求知的向导,是学习的动力,于是即兴研究了有关Java的内存划分,感悟如下,如有不当之处,敬请批评指正,共同交流。
———————————————————————————————————
Java内存分区实际上指的是Java虚拟机(JVM)在执行Java程序过程中把其所管理的内存划分若干个不同数据区域的行为,所以不同的JVM有着不同的划分标准,但是基于Java虚拟机规范它们之间大同小异。
根据Java虚拟机规范,Java虚拟机所管理的内存主要包括一下几个区域,如下图所示:
这里写图片描述

那么问题来了?JVM为什么要进行分区管理呢?
我的理解是 1、分区实现了分类存储的功能,可以实现高效存取
2、分区更利于JVM的GC(在适当的时候,适当的区域进行垃圾回收)

经过查询文献资料和多方询问,以下是我对各个分区的浅见:

1、程序计数器

看到程序计数器,你可能联想到操作系统中的程序计数器(PC),操作系统中的PC主要有以下的功能:
1>记录指令执行的条数,每执行一条指令,在原有操作数上进行加一操作;
2>存储下一条(机器)指令的地址;
3>存储中断地址。
其实JVM中的程序计数器也有类似的功能。(以下所述均为JVM中的PC)
JVM中的程序计数器可以看做当前线程所执行的字节码指令的行号指示器。字节码解释器就是通过改变此计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等等
需要注意的是:JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此为了保证线程切换之后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器

2、虚拟机栈

虚拟机栈,简称栈,同程序计数器也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会建立一个栈帧(Stack Frame),栈帧由3部分组成:局部变量表、操作数栈和帧数据。每一个方法从调用到完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。这里着重讲一下栈帧。
一个线程中的方法调用链可能会很长,很多方法都同时处于执行状态。对于执行引擎来讲,活动线程中,只有栈顶的栈帧是有效的,称为当前栈帧(Current Stack Frame),这个栈帧所关联的方法称为当前方法(Current Method)。执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作。
接下来我们将详细了解一下栈帧中的局部变量表、操作数栈、帧数据各个部分的作用。

(1)局部变量表(Local Variables)
局部变量表存放了编译器可知的各种基本数据类型、对象引用和returnAddress类型(指向一条字节码指令的地址)。
下图描述了JVM中的数据类型:
这里写图片描述
虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从0开始到局部变量表最大的Slot数量。如果是32位数据类型的变量,索引n就代表了使用第n个Slot,如果是64位数据类型的变量,则说明要使用第n和第n+1两个Slot。
那么什么是slot呢?slot,全称Variable Slot,又称变量槽就是局部变量表中存储的最小单位。
《深入理解JVM》中指出:64bit的long和double类型的数据会占用两个局部变量空间(slot),其余数据类型只占一个。
还有要注意的是局部变量表的内存空间是在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量表空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

(2)操作数栈(Operand Stack)
每一个独立的栈帧中除了包含局部变量表以外,还包含一个后进先出(Last-In-First-Out)的操作数栈。操作数栈和局部变量表在访问方式上存在着较大差异,操作数栈是通过标准的入栈和出栈操作来完成一次数据访问,而非索引。
虚拟机在操作数栈中存储数据的方式和在局部变量区中是一样的:如int、long、float、double、reference和returnType的存储。对于byte、short以及char类型的值在压入到操作数栈之前,也会被转换为int。
虚拟机把操作数栈作为它的工作区,大多数指令都要从这里弹出数据,执行运算,然后把结果压回操作数栈。比如,iadd指令就要从操作数栈中弹出两个整数,执行加法运算,其结果又压回到操作数栈中,看看下面的示例,它演示了虚拟机是如何把两个int类型的局部变量相加,把结果保存到第三个局部变量的。

iload_0    // push the int in local variable 0iload_1    // push the int in local variable 1iadd       // pop two ints, add them, push resultistore_2   // pop int, store into local variable 2

在此字节码指令序列中,前两个指令iload_0和iload_1将存储在局部变量中索引为0和1的数据(在此处为int类型)压入操作数栈,然后执行iadd指令从操作数栈中弹出那两个数据做相加的操作,再将相加后的结果压入操作数栈。最后istore_2表示从操作数栈中弹出结果,并把它存储到局部变量区索引为2的位置。下图详细阐述此过程中局部变量和操作数栈的状态变化(图中没有使用的局部变量区和操作数栈区域以空白表示)。
这里写图片描述
(3)帧数据(Frame Data)
除了局部变量表和操作数栈,栈帧中还包含一些数据用来支持常量池引用,方法的正常返回,异常调度等。这部分数据被存储于栈帧中的帧数据部分。这部分经常被分为动态链接,方法返回地址等
1》动态链接
1、每个栈帧都包含一个指向运行时常量池中该栈帧所属性方法的引用,持有这个引用是为了支持方法调用过程中的动态连接;
2、在Class文件的常量池中存有许多符号引用,字节码中指令就以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为静态解析。另外一部分将在每一次的运行期期间转化为直接引用,这部分称为动态连接。
2》方法返回地址
除了常量池引用,帧数据必须协助JVM处理正常或者异常方法的返回。

3、堆

Java堆是Java虚拟机所管理的内存中最大的一块,Java堆是被所有的线程共享的一块内存区域,在虚拟机启动时创建。此内存区域唯一的目的就是存放对象实例,几乎所有的对象实例都在这里分配内存,因此Java堆是GC管理的主要区域。
Java中的堆可以处于物理上不连续的空间,只要逻辑上是连续的即可。
在堆中,对象的访问方式可分为两种:
1、使用句柄访问(堆设计将堆分为两部分:句柄池和对象池)
如果使用句柄访问的话,那么Java堆中将将会划分出一块内存作为句柄池,句柄池中有两个组件,(1)指向对象池的实例数据的指针(2)指向方法区类数据的指针
这里写图片描述
2、使用直接指针访问
这种设计使对象引用包含一个对象实例数据的数据包和指向方法区类数据的指针
这里写图片描述
比较两种访问方式

这两种方式各有优劣,使用句柄最大的好处就是reference中存储的是稳定的句柄地址,在对象池中的对象被移动(垃圾收集时移动对象是非常普遍的行为)时只需要更新一个指针,该对象的新地址是句柄池中的相关指针,而reference本身不需要修改。

使用直接指针访问的好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java中是非常频繁,因此此类开销积少成多后也是一种非常可观的执行成本。(Sun HotSpot就是以第二种方式进行对象访问的)。但是在对象移动时,它必须在运行时数据区域的任何地方更新对该对象的每个引用,这时候就显得捉襟见肘了。

4、方法区

在JVM进行类加载的时候,它使用类加载器找到对应的类文件,类加载器以二进制流的形式读取类文件,并将其传递给虚拟机。虚拟机从二进制数据中提取有关类型的信息,并将该信息存储在方法区中。
方法区与Java堆一样,是线程共享的内存区域,它用于存储已被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
这里可以参考(http://www.artima.com/insidejvm/ed2/jvm5.html)
这篇文章讲的很详细,这里我就不赘述了。

5、本地方法栈

本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用非常相似,它们的区别不过是虚拟机栈是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。在虚拟机规范中对本地方法栈中方法使用的语言,使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。

其中Native Method就是java调用非java代码的接口。一个Native Method是这样一个java的方法:该方法的实现由非java语言实现,比如C。这个特征并非java所特有,很多其它的编程语言都有这一机制,比如在C++中,你可以用extern “C”告知C++编译器去调用一个C的函数。

虚拟机栈和本地方法栈特性的对比

Java虚拟机栈的特征:线程私有后进先出(LIFO)栈存储栈帧,支持Java方法的调用、执行和退出可能出现OutOfMemoryError异常和StackOverflowError异常Java本地方法栈的特征:线程私有后进先出(LIFO)栈作用是支撑Native方法的调用、执行和退出可能出现OutOfMemoryError异常和StackOverflowError异常有一些虚拟机(如HotSpot)将Java虚拟机栈和本地方法栈合并实现 

总结:
1、Java运行时数据区域可以分为
程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区,其中方法区和Java堆是所有线程共享的区域,而Java虚拟机栈和程序计数器是线程私有。
2、程序计数器(PC):当前线程所执行字节码指令的行号指示器。
3、Java虚拟机栈:线程私有,与线程的生命周期相同,描述的是Java方法执行的内存模型:每个方法执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈和帧数据。
4、Java堆:线程共享,此区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。接着介绍了堆中对象访问的两种方式(直接引用和句柄的方式)。
5、方法区:线程共享,用于存储已被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
6、本地方法栈:与虚拟机栈的功能非常相似, 区别就是本地方法栈的服务对象为本地方法(Native Method),而虚拟机栈的服务对象为Java方法(也就是字节码)服务。
7、虚拟机栈中栈帧的组成(局部变量表、操作数栈、动态链接和方法出口等)。

1 0