JVM中的Stack和Heap

来源:互联网 发布:clevo 知乎 编辑:程序博客网 时间:2024/05/16 17:31
转载自:http://www.cnblogs.com/jiayi/archive/2010/06/08/1753867.html

       在JVM中,内存分为两个部分,Stack(栈)和Heap(堆),这里从JVM的内存管理原理的角度来认识Stack和Heap,并通过这些原理认清Java中静态方法和静态属性的问题。
       Stack(栈)是JVM的内存指令区。Stack管理很简单,push一定长度字节的数据或者指令,Stack指针压栈相应的字节位移;pop一定字节长度数据或者指令,Stack指针弹栈。Stack的速度很快,管理很简单,并且每次操作的数据或者指令字节长度是已知的。所以Java 基本数据类型,Java 指令代码,常量都保存在Stack中。
       Heap(堆)是JVM的内存数据区。Heap 的管理很复杂,每次分配不定长的内存空间,专门用来保存对象的实例。在Heap 中分配一定的内存来保存对象实例,实际上也只是保存对象实例的属性值,属性的类型和对象本身的类型标记等,并不保存对象的方法(方法是指令,保存在 Stack中),在Heap 中分配一定的内存保存对象实例和对象的序列化比较类似。而对象实例在Heap 中分配好以后,需要在Stack中保存一个4字节的Heap 内存地址,用来定位该对象实例在Heap中的位置,便于找到该对象实例。
       由于Stack的内存管理是顺序分配的,而且定长,不存在内存回收问题;而Heap 则是随机分配内存,不定长度,存在内存分配和回收的问题;因此在JVM中另有一个GC进程,定期扫描Heap,它根据Stack中保存的4字节对象地址扫描Heap,定位Heap 中这些对象,进行一些优化(例如合并空闲内存块什么的),并且假设Heap 中没有扫描到的区域都是空闲的,统统refresh(实际上是把Stack中丢失了对象地址的无用对象清除了),这就是垃圾收集的过程;关于垃圾收集的更深入讲解请参考《JVM内存模型及垃圾收集策略解析》。

       需要搞清楚的是什么是数据以及什么是指令。然后要搞清楚对象的方法和对象的属性分别保存在哪里。
       1)方法本身是指令的操作码部分,保存在Stack中;
       2)方法内部变量作为指令的操作数部分,跟在指令的操作码之后,保存在Stack中(实际上是简单类型保存在Stack中,对象类型在Stack中保存地址,在Heap 中保存值);上述的指令操作码和指令操作数构成了完整的Java 指令。
       3)对象实例包括其属性值作为数据,保存在数据区Heap 中。

       非静态的对象属性作为对象实例的一部分保存在Heap 中,而对象实例必须通过Stack中保存的地址指针才能访问到。因此能否访问到对象实例以及它的非静态属性值完全取决于能否获得对象实例在Stack中的地址指针。

非静态方法和静态方法的区别:    
       非静态方法有一个和静态方法很重大的不同:非静态方法有一个隐含的传入参数,该参数是JVM给它的,这个隐含的参数就是对象实例在Stack中的地址指针。因此非静态方法(在Stack中的指令代码)总是可以找到自己的专用数据(在Heap中的对象属性值)。当然非静态方法也必须获得该隐含参数,因此非静态方法在调用前,必须先new一个对象实例,获得Stack中的地址指针,否则JVM将无法将隐含参数传给非静态方法。
       静态方法无此隐含参数,因此也不需要new对象,只要class文件被ClassLoader load进入JVM的Stack,该静态方法即可被调用。当然此时静态方法是存取不到Heap 中的对象属性的。

       总结一下该过程:当一个class文件被ClassLoader load进入JVM后,方法指令保存在Stack中,此时Heap区没有数据。然后程序技术器开始执行指令,如果是静态方法,直接依次执行指令代码,当然此时指令代码是不能访问Heap 数据区的;如果是非静态方法,由于隐含参数没有值,会报错。因此在非静态方法执行前,要先new对象,在Heap 中分配数据,并把Stack中的地址指针交给非静态方法,这样程序技术器依次执行指令,而指令代码此时能够访问到Heap 数据区了。

静态属性和动态属性:
       前面提到对象实例以及动态属性都是保存在Heap 中的,而Heap必须通过Stack中的地址指针才能够被指令(类的方法)访问到。因此可以推断出:静态属性是保存在Stack中的,而不同于动态属性保存在Heap中。正因为都是在Stack中,而Stack中指令和数据都是定长的,因此很容易算出偏移量,也因此不管什么指令(类的方法),都可以访问到类的静态属性。也正因为静态属性被保存在Stack中,所以具有了全局属性。所以,在JVM中,静态属性保存在Stack指令内存区,动态属性保存在Heap数据内存区。



内存栈(Stack)的概念:

       当一个新线程启动的时候,JVM会为Java线程创建每个线程的独立内存栈,Java的内存栈是由栈帧构成,栈帧是内存栈里面的最小单位,是内存栈里面每一个最小内存存储单元,栈帧本身处于游离状态,在JVM里面,栈帧的操作只有两种:出栈和入栈。
       栈帧:栈帧主要包括三个部分:局部变量、操作数栈帧(操作帧)和帧数据(数据帧,用于支持常量池、普通的方法返回以及异常抛出等,这些数据都是存储在Java内存栈帧的数据帧中的)。本地变量和操作数帧的大小取决于需要,这些大小是在编译时就决定的,并且在每个方法的类文件数据中进行分配,帧的数据大小则不一样,它虽然也是在编译时就 决定的但是它的大小和本身代码实现有关。当JVM调用一个Java方法的时候,它会检查类的数据来确定在本地变量和操作方法要求的栈大小,它计算该方法所 需要的内存大小,然后将这些数据分配好内存空间压入到内存堆栈中。

       正在被线程执行的方法一般称为当前线程方法,而该方法的栈帧就称为当前帧,而在该方法内定义的类称为当前类,常量池也称为当前常量池。当执行一个方法如此的时候,JVM保留当前类和当前常量池的跟踪,当虚拟机遇到了存储在栈帧中的数据上的操作指令的时候,它就执行当前帧的操作。当一个线程调用某个Java方法时,虚拟机创建并且将一个新帧压入到内存堆栈中,而这个压入到内存栈中的帧成为当前栈帧,当该方法执行的时候,JVM使用内存栈来存储参数、局部变量、中间计算结果以及其他相关数据。
       方法在执行过程有可能因为两种方式而结束:如果一个方法返回完成就属于方法执行的正常结束,如果在这个过程抛出异常而结束,可以称为非正常结束,不论是正常结束还是异常结束,JVM都会弹出或者丢弃该栈帧,则上一帧的方法就成为了当前帧。
       在JVM中,Java线程的栈数据是属于某个线程独有的,其他的线程不能够修改或者通过其他方式来访问该线程的栈帧,正因为如此这种情况不用担心多线程同步访问Java的局部变量,当一个线程调用某个方法的时候,方法的局部变量是在方法内部进行的Java栈帧的存储,只有当前线程可以访问该局部变量,而其他线程不能随便访问该内存栈里面存储的数据。内存栈内的栈帧数据和方法区以及内存堆一样,Java栈的栈帧不需要分配在连续的堆栈内,或者说它们可能是在堆,或者两者组合分配,实际数据用于表示Java堆栈和栈帧结构是JVM本身的设计结构决定的,而且在编程过程可以允许程序员指定一个用于Java堆栈的初始大小以及最大、最小尺寸。
0 0