计算机什么时候用堆栈内存组织为什么要用栈不用队

关于本站本站为360用户提供方便快捷的360产品问题解答信息,让我们理智客观的对待互联网信息。本站信息来源于互联网,如有侵权,请联系站长。(window.slotbydup=window.slotbydup || []).push({
id: '2014386',
container: s,
size: '234,60',
display: 'inlay-fix'
&&|&&0次下载&&|&&总112页&&|
您的计算机尚未安装Flash,点击安装&
试读已结束,如果需要继续阅读或下载,请使用积分()
下载:40积分
本文档由合作伙伴提供
此文档归属以下专题
5人评价150个文档
0人评价22页
0人评价82页
0人评价49页
0人评价77页
0人评价3页
所需积分:(友情提示:大部分文档均可免费预览!下载之前请务必先预览阅读,以免误下载造成积分浪费!)
(多个标签用逗号分隔)
文不对题,内容与标题介绍不符
广告内容或内容过于简单
文档乱码或无法正常显示
文档内容侵权
已存在相同文档
不属于经济管理类文档
源文档损坏或加密
若此文档涉嫌侵害了您的权利,请参照说明。
我要评价:
下载:40积分JVM学习(2)--技术文章里常说的堆,栈,堆栈到底是什么,从os的角度总结,堆栈 摄影 - 凉祖新闻网
推荐栏目: |
凉祖新闻网
JVM学习(2)--技术文章里常说的堆,栈,堆栈到底是什么,从os的角度总结
时间: 15:57
点击:4004次
来自:网络原创
俗话说,自己写的代码,6个月后也是别人的代码……复习!复习!复习!涉及到的知识点总结如下:
堆栈是栈JVM栈和本地方法栈划分Java中的堆,栈和c/c++中的堆,栈数据结构层面的堆,栈os层面的堆,栈JVM的堆,栈和os如何对应为啥方法的调用需要栈属于月经问题了,正好碰上有人问我这类比较基础的…
俗话说,自己写的代码,6个月后也是别人的代码……复习!复习!复习!涉及到的知识点总结如下:堆栈是栈JVM栈和本地方法栈划分Java中的堆,栈和c/c++中的堆,栈数据结构层面的堆,栈os层面的堆,栈JVM的堆,栈和os如何对应为啥方法的调用需要栈属于月经问题了,正好碰上有人问我这类比较基础的知识,无奈我自觉回答不是有效果,现在深入浅出的总结下:前一篇文章总结了:JVM 的内存主要分为3个分区堆区(Heap)-- 只存对象(数组)本身(引用类型的数据),不存基本类型和对象的引用。JVM只有一个堆区,这个“堆”是动态内存分配意义上的堆——用于管理动态生命周期的内存区域。JVM的堆被同一个JVM实例中的所有Java线程共享,它通常由某种自动内存管理机制所管理,这种机制通常叫做“垃圾回收”(garbage collection,GC)。JVM规范并不强制要求JVM实现采用哪种GC算法。栈区(Stack)-- 栈中只保存基础数据类型的对象和对象引用。每个线程一个栈区,每个栈区中的数据都是私有的,其他栈不能访问。栈内有帧(方法调用会生成栈帧)分三个部分:基本类型变量区,执行环境上下文,操作指令区。方法区 -- 又叫静态区,跟堆一样,被所有线程共享。方法区包含所有的class和static变量。方法区包含的都是在整个程序中永远唯一的元素。如:class,satic。堆栈是啥?是堆还是栈?之前初学c++的时候被人误导过,说堆栈是堆……其实这个是翻译的误读,堆栈,其实应该翻译成栈更合适,和堆区分开来,因为英文的stack就是堆栈的意思,位于RAM(Random Access Memory,随机访问存储区),速度仅次于寄存器。存放基本变量和引用,存在栈中的数据可以共享。但是,栈中的数据大小和生存周期必须确定,这是栈的缺点。堆栈不是堆,是栈。堆是存放了所有的java对象(逃逸分析除外)。本地方法栈和JVM栈是如何划分的?JVM规范写到:每个Java线程拥有自己的独立的JVM栈,也就是Java方法的调用栈。同时JVM规范为了允许native代码可以调用Java代码,以及允许Java代码调用native方法,还规定每个Java线程拥有自己的独立的native方法栈。都是JVM规范所规定的概念上的东西,并不是说具体的JVM实现真的要给每个Java线程开两个独立的栈。以Oracle JDK / OpenJDK的HotSpot VM为例,它使用所谓的“mixed stack”——在同一个调用栈里存放Java方法的栈帧与native方法的栈帧,所以每个Java线程其实只有一个调用栈,融合了JVM规范的JVM栈与native方法栈这俩概念。如之前文章1的结构图:数据结构层面的堆和栈数据结构里面。stack,中文翻译为堆栈,其实指的是栈,这里讲的是数据结构的栈,不是内存分配里面的堆和栈。栈是先进后出的数据的结构,好比你碟子一个一个堆起来,最后放的那个是堆在最上面的。栈数据结构比较简单。heap翻译为堆,是一种有序的树。JVM的堆,栈和c、c++的堆、栈一样么?回答这个问题之前,先得回答程序运行时的内存分配策略,编译原理的理论认为:程序运行的内存分配有三个策略:静态存储分配:在编译时就能确定每个数据目标在运行时刻的存储空间需求,因而在编译时就可以给他们分配固定的内存空间。这种分配策略要求程序代码中不允许有可变数据结构(如可变数组),也不允许有嵌套或者递归的结构出现。因为它们都会导致编译程序无法计算准确的存储空间需求。栈式存储分配:也叫动态存储分配,和静态存储分配相反,栈就是暂时!在栈式存储方案中,存储的都是局部变量,临时变量,比如基本数据类型,对象引用……从内存的分配角度来看,因为存储的是基本的数据类型,编译器事先已经知道了类型的大小,故直接可以进行有效的内存分配,比如int,计算机是知道其范围的,所以直接由系统分配在栈中,无需程序自己去申请xxx内存!而引用类型,比如自己定义一个类,很明显这个类是不知道大小的,应该有程序自己来申请内存空间,所以由堆来分配!栈分配模式规定在运行中进入一个程序模块时,必须知道该程序模块所需的数据区大小,才能够为其分配内存。和我们在数据结构所熟知的栈一样,栈式存储分配按照先进后出的原则进行分配。堆式存储分配:则专门负责在编译时或运行时模块入口处都无法确定存储要求的数据结构的内存分配,比如可变长度串和对象实例。堆由大片的可利用块或空闲块组成,堆中的内存可以按照任意顺序分配和释放。因此我们断定:堆主要用来存放对象,栈主要是用来执行程序的。而这种不同又主要是由于堆和栈的特点决定的: 例如C/C++……所有的方法调用都是通过栈来进行的,所有的局部变量,形式参数都是从栈中分配内存空间,就好像工厂中的传送(conveyor belt)带一样,Stack Top Pointer 会自动指引你放东西的位置,你所要做的只是把东西放下来就行。退出函数的时候,修改栈顶指针就可以把栈中的内容销毁。这样的模式速度最快,当然要用来运行程序了。现在言归正传,之前的文章1已经总结了——JVM是基于堆栈的虚拟机。每一个JVM实例都为每个新创建的线程分配一个栈,而多个线程共享唯一一个堆区,也就是说,对于一个Java程序来说,它的运行就是通过对栈的操作来完成的。栈以帧为单位保存线程的状态。JVM对栈只进行两种操作:以帧为单位的压栈和出栈操作。当某个线程正在执行某个方法时,我们就称此线程为当前方法,而当前方法使用的帧称为当前帧。当线程要调用一个Java方法时,JVM就会先在线程的Java栈里新压入一个帧。这个帧自然成为了当前帧。在此方法执行期间,这个帧将用来保存方法的形参,局部变量,中间计算过程和其他数据……这个帧在这里和编译原理中的活动纪录的概念是差不多的。好了,罗嗦了半天,从这个栈式分配机制来看,栈可以这样理解:栈(Stack)是os在建立某个进程或者线程(在支持多线程的操作系统中是线程)时,为这个(进程)线程建立的存储区域,该区域具有先进后出的特性。栈中的新加数据项放在其他数据的顶部,移除时你也只能移除最顶部的数据(不能越位获取)。类似这个纸:再说堆,每一个JVM的实例有且只有一个堆,这个唯一的堆被全局的线程共享!程序在运行中所创建的所有类实例或数组都放堆中,并由应用所有的线程共享。堆中的数据项位置没有固定的顺序,你可以以任何顺序插入和删除,因为他们没有“顶部”数据这一概念。如图:跟C/C++不同,Java中分配堆内存是自动化管理的(Java虚拟机的自动垃圾回收器来管理,缺点是,由于要在运行时动态分配内存,存取速度较慢)Java中所有对象的存储空间都是在堆中分配,但对象引用是在栈中分配,而堆中分配的内存才是实际的这个对象本身,栈中分配的内存只是一个指向这个对象的指针(引用)变量而已(变量的取值等于数组或对象在堆内存中的首地址)。而c++的堆内存管理,需要程序员手动管理的,new,delete运算符……内存管理中的栈分配方法有什么特点?优缺点又是什么?首先想到就是该快内存FILO的特性,还有经过前面这么罗嗦的哔哔,又得出一个结论:栈中的数据可以共享。编译器先处理int a = 3; 会在栈中创建一个变量为a的引用,然后查找栈中是否有3这个值,如果没找到,就将3存放进来,然后将a指向3。接着处理int b = 3; 在创建完b的引用变量后,因为在栈中已经有3这个值,便将b直接指向3。这样,就出现了a与b同时均指向3的情况。这时,如果再令a=4; 那么编译器会重新搜索栈中是否有4值,如果没有,则将4存放进来,并令a指向4; 如果已经有了,则直接将a指向这个地址。因此a值的改变不会影响到b的值。要注意这种数据的共享与两个对象的引用同时指向一个对象的这种共享是不同的,因为这种情况a的修改并不会影响到b, 它是由编译器完成的,它有利于节省空间。而一个对象引用变量修改了这个对象的内部状态,会影响到另一个对象引用变量。优点:速度快,不用管理内存,缺点是太小,方法调用过度,容易内存溢出,还有栈就是暂时,数据有生命周期,属于临时存储。站在实际的计算机物理内存的角度上看,栈和堆在哪儿?在通常情况下由操作系统(OS)和语言的运行时(runtime)控制吗?它们的作用范围是什么?它们的大小由什么决定?哪个更快?JVM的栈如何对应os?回答这个问题之前,必须先知道内存管理的机制根据不同的编译器和处理器架构的不同而不同!为了帮助理解,先总结几个原理:什么是局部性原理?os的教科书这样写到局部性原理:CPU访问存储器时,无论是存取指令还是存取数据,所访问的存储单元都趋于聚集在一个较小的连续区域中。我是这样理解的:计算机的存储系统从小到大,分为寄存器,一级缓存,二级缓存,三级缓存,内存,磁盘……而寄存器是CPU存放计算数据的地方,CPU要工作了,需要数据或者地址,先从一级缓存里面找,找不到就从二级缓存里面找,二级找不到就去三级找……假如找到磁盘才有了目标数据,那么该数据就会先放入内存,再存入三级缓存、二级缓存、一级缓存,最后存入寄存器,才能被CPU使用。可以说,一级缓存是寄存器的缓存,二级缓存是一级缓存的缓存,三级缓存是二级缓存的缓存……下面一层是上面一层的缓存。而局部性原理,通俗的说就是因为CPU的运转速度非常非常快!是高速存储的!而磁盘和内存之间的存取速度很慢(I/O瓶颈绕不开……),如果CPU需要的数据更多的在磁盘,内存……这样会花非常多的等待时间,故我们就设置了高速缓存!当CPU频繁的用了某块数据,计算机会遇见性的把它及其附近地址上的数据都存入高速缓存内,因为预判这些数据再次被用到的可能性很大,计算机就把它们存到越接近寄存器的层次,也就是cpu所访问的数据,都趋于集中在一个较小的连续区域中,这也才是缓存的真正意义。那么,现在的问题就变为回答:计算机怎样才能判断一个数据接下来可能被用到?时间局部性:如果一个数据正在被访问,那么在近期它很可能还会被再次访问。这当然是正确的,用过的数据当然可能再次被用到。空间局部性:在最近的将来将用到的信息很可能与现在正在使用的信息在空间地址上是临近的,正在使用的这个数据地址旁边的数据,当然也是很可能被用到的。比如数组什么的……哦了。前面几个问题已经得出这样的结论:栈和堆都是用来从底层操作系统中获取内存的。在多线程环境下每一个线程都可以有他自己完全的独立的栈,但是他们共享堆。并行存取被堆控制而不是栈。在堆上新分配(用 new 或者 malloc)内存是从空闲的内存块中找到一些满足要求的合适块。这个操作会更新堆中的块链表。这些元信息也存储在堆上,经常在每个块的头部一个很小区域。堆增加新块通常从低地址向高地址扩展,也就是说堆是向上增长的!因此可以认为堆随着内存分配而不断的增加大小。如果申请的内存大小很小的话,通常从底层操作系统中得到比申请大小要多的内存。申请和释放许多小的块可能会产生如下状态:在已用块之间存在很多小的浪费的空闲块……进而导致申请大块内存失败,虽然空闲块的总和足够,但是空闲的小块是零散的,不能满足申请的大小,这叫做“内存碎片”。当旁边有空闲块的已用块被释放时,新的空闲块可能会与相邻的空闲块合并为一个大的空闲块,这样可以有效的减少“碎片”的产生。堆的管理依赖于运行时环境,C 使用 malloc ,free,C++ 使用 new 和delete,但是很多语言有垃圾回收机制,比如Java的GC。栈:栈经常与 sp 寄存器一起工作,最初 sp 指向栈顶(栈的高地址)。栈是向下增长的!CPU 用 push 指令来将数据压栈,用 pop 指令来弹栈。当用 push 压栈时,sp 值减少(向低地址扩展)。当用 pop 弹栈时,sp 值增大。存储和获取数据都是 CPU 寄存器的值。当函数被调用时,CPU使用特定的指令把当前的 IP 压栈,接下来将调用函数的地址赋给 IP,让cpu去调用函数。当函数返回时,旧的 IP 被弹栈,CPU 继续去函数调用之前的代码。当进入函数时,sp 向下扩展,扩展到确保为函数的局部变量留足够大小的空间。如果函数中有一个 32-bit 的局部变量会在栈中留够四字节的空间。当函数返回时,sp 通过返回原来的位置来释放空间。如果函数有参数的话,在函数调用之前,会将参数压栈。函数中的代码通过 sp 的当前位置来定位参数并访问它们。函数嵌套调用,每一次新调用的函数都会分配函数参数,返回值地址、局部变量空间、嵌套调用的活动记录都要被压入栈中。函数返回时,按照正确方式的撤销。栈要受到内存块的限制,不断的函数嵌套……为局部变量分配太多的空间,可能会导致栈溢出。当栈中的内存区域都已经被使用完之后继续向下写(低地址),会触发一个 CPU 异常。这个异常接下会通过语言的运行时转成各种类型的栈溢出异常。总的来说,栈以更低层次的特性与处理器架构紧密的结合到一起,当堆不够时可以扩展空间。但是,扩展栈通常来说是不可能的,因为在栈溢出的时候,执行线程就被操作系统关闭了,这已经太晚了。现在可以回答这几个问题:在通常情况下由操作系统(OS)和语言的运行时(runtime)控制吗?如前所述,堆和栈是一个统称,可以有很多的实现方式。计算机程序通常有一个栈叫做调用栈,用来存储当前函数调用相关的信息(比如:主调函数的地址,局部变量),因为函数调用之后需要返回给主调函数。栈通过扩展和收缩来承载信息。实际上,程序不是由运行时来控制的,它由编程语言、操作系统甚至是系统架构来决定。堆是在任何内存中动态和随机分配的(内存的)统称;也就是无序的。内存通常由操作系统分配,通过应用程序调用 API 接口去实现分配。在管理动态分配内存上会有一些额外的开销,不过这由操作系统来处理。它们的作用范围是什么?调用栈是一个低层次的概念,就程序而言,它和“作用范围”没什么关系。就高级语言而言,语言有它自己的范围规则。一旦函数返回,函数中的局部变量会直接释放。在堆中,也很难去定义。作用范围是由操作系统限定的,但是编程语言可能增加它自己的一些规则,去限定堆在应用程序中的范围。体系架构和操作系统是使用虚拟地址的,然后由处理器翻译到实际的物理地址中,还有页面错误等等。它们记录那个页面属于那个应用程序。不过你不用关心这些,因为你仅仅在编程语言中分配和释放内存,和一些错误检查(出现分配失败和释放失败的原因)。它们的大小由什么决定?依赖于语言,编译器,操作系统和架构。栈通常提前分配好了,因为栈必须是连续的内存块。语言的编译器或者操作系统决定它的大小。不要在栈上存储大块数据,这样可以保证有足够的空间不会溢出,除非出现了无限递归的情况或者其它。堆是任何可以动态分配的内存的统称。它的大小是变动的。在现代处理器中和操作系统的工作方式是高度抽象的,因此你在正常情况下不需要担心它实际的大小,除非你必须要使用你还没有分配的内存或者已经释放了的内存。哪个更快一些?栈更快因为所有的空闲内存都是连续的,因此不需要对空闲内存块通过列表来维护。只是一个简单的指向当前栈顶的指针。编译器通常用一个专门的、快速的寄存器SP来实现。更重要的一点是,随后的栈上操作会遵循局部性原理。JVM的栈如何对应os?以linux 中一个进程的虚拟内存分布为例:图中0号地址在最下边,越往上内存地址越大。以32位操作系统为例,一个进程可拥有的虚拟内存地址范围为0-2^32。分为两部分,一部分留给kernel使用(kernel virtual memory),剩下的是进程本身使用, 即图中的process virtual memory。普通Java 程序使用的就是process virtual memory。上图中最顶端的一部分内存叫做user stack. 这就是栈stack,32位的栈顶指针寄存器是esp,中间有 runtime heap。就是堆,注意他们和数据结构里的stack 和 heap 不是一回事。前面总结了,stack 是向下生长的,heap是向上生长的。当程序进行函数调用时,每个函数都在stack上有一个 call frame(帧)。小结,总结了那么多,现在最后一个问题:为啥方法的调用需要栈其实确切的说:并不是方法的调用需要用栈来实现,而是它设计成用栈实现!我们知道,各个方法的活动记录(即局部或者自动变量)被分配在栈上, 这样做不但存储了这些变量,而且可以用来嵌套方法的追踪。因为我们经过观察可以知道,方法的调用过程是这样的:1,计算参数,传参2,保存方法的返回地址3,控制转移至callee4,保存必要的caller现场以上一些步骤之间的顺序是可变的,但理论上并没有哪个步骤是必须用栈来实现的。理论上如果有很多寄存器,我们完全可以抛弃栈,然而实际上我们并没有,所以从现实的角度来说,栈是一个适合的实现方法,简单说就是方法调用的局部数据的存活时间满足“先进后出(FILO)”的顺序,之所以用栈来记录是因为栈的基本操作正好就是支持这种顺序的访问。而堆是无法实现的。文章总结参考资料:《Java编程思想》、《现代操作系统》,《深入理解计算机系统》、《现代编译原理》,《深入理解Java虚拟机》、《JVM规范 7》、知乎、stackoverflow……java虚拟机内存模式。栈和堆 - rainyear - ITeye博客
博客分类:
博客分类:
我们知道,计算机CPU和内存的交互是最频繁的,内存是我们的高速缓存区,用户磁盘和CPU的交互,而CPU运转速度越来越快,磁盘远远跟不上CPU的读写速度,才设计了内存,用户缓冲用户IO等待导致CPU的等待成本,但是随着CPU的发展,内存的读写速度也远远跟不上CPU的读写速度,因此,为了解决这一纠纷,CPU厂商在每颗CPU上加入了高速缓存,用来缓解这种症状,因此,现在CPU同内存交互就变成了下面的样子。
& xmlnamespace prefix ="v" ns ="urn:schemas-microsoft-com:vml" /&& xmlnamespace prefix ="o" ns ="urn:schemas-microsoft-com:office:office" /&
同样,根据摩尔定律,我们知道单核
CPU的主频不可能无限制的增长,要想很多的提升新能,需要多个处理器协同工作, 总裁的贝瑞特单膝下跪事件标志着多核时代的到来。
基于高速缓存的存储交互很好的解决了处理器与内存之间的矛盾,也引入了新的问题:缓存一致性问题。在多处理器系统中,每个处理器有自己的高速缓存,而他们又共享同一块内存(下文成主存,
main memory 主要内存),当多个处理器运算都涉及到同一块内存区域的时候,就有可能发生缓存不一致的现象。为了解决这一问题,需要各个处理器运行时都遵循一些协议,在运行时需要将这些协议保证数据的一致性。这类协议包括MSI、MESI、MOSI、Synapse、Firely、DragonProtocol等。如下图所示
虚拟机内存模型中定义的访问操作与物理计算机处理的基本一致!
中通过多线程机制使得多个任务同时执行处理,所有的线程共享JVM内存区域main memory,而每个线程又单独的有自己的工作内存,当线程与内存区域进行交互时,数据从主存拷贝到工作内存,进而交由线程处理(操作码+操作数)。更多信息我们会在后面的《深入JVM—JVM类执行机制中详细解说》。
在之前,我们也已经提到,JVM的逻辑内存模型如下:
我们现在来逐个的看下每个到底是做什么的!
1、程序计数器
程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看
做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,
各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变
这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、
线程恢复等基础功能都需要依赖这个计数器来完成。
由于Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现
的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行
一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要
有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内
存区域为“线程私有”的内存。
如果线程正在执行的是一个Java 方法,这个计数器记录的是正在执行的虚拟机字节
码指令的地址;如果正在执行的是Natvie 方法,这个计数器值则为空(Undefined)。此
内存区域是唯一一个在Java 虚拟机规范中没有规定任何OutOfMemoryError 情况的区域。
2、Java 虚拟机栈
与程序计数器一样,Java 虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,
它的生命周期与线程相同。虚拟机栈描述的是Java 方法执行的内存模型:每个方法被执
行的时候都会同时创建一个栈帧(Stack Frame ①)用于存储局部变量表、操作栈、动态
链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在
虚拟机栈中从入栈到出栈的过程。
经常有人把Java 内存区分为堆内存(Heap)和栈内存(Stack),这种分法比较粗
糙,Java 内存区域的划分实际上远比这复杂。这种划分方式的流行只能说明大多数程序
员最关注的、与对象内存分配关系最密切的内存区域是这两块。其中所指的“堆”在后
面会专门讲述,而所指的“栈”就是现在讲的虚拟机栈,或者说是虚拟机栈中的局部变
量表部分。
局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、
float、long、double)、对象引用(reference 类型,它不等同于对象本身,根据不同的虚拟
机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或
者其他与此对象相关的位置)和returnAddress 类型(指向了一条字节码指令的地址)。
其中64 位长度的long 和double 类型的数据会占用2 个局部变量空间(Slot),其余
的数据类型只占用1 个。局部变量表所需的内存空间在编译期间完成分配,当进入一个
方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间
不会改变局部变量表的大小。
在Java 虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大
于虚拟机所允许的深度,将抛出StackOverflowError 异常;如果虚拟机栈可以动态扩展
(当前大部分的Java 虚拟机都可动态扩展,只不过Java 虚拟机规范中也允许固定长度的
虚拟机栈),当扩展时无法申请到足够的内存时会抛出OutOfMemoryError 异常。
3、本地方法栈
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其
区别不过是虚拟机栈为虚拟机执行Java 方法(也就是字节码)服务,而本地方法栈则
是为虚拟机使用到的Native 方法服务。虚拟机规范中对本地方法栈中的方法使用的语
言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至
有的虚拟机(譬如Sun HotSpot 虚拟机)直接就把本地方法栈和虚拟机栈合二为一。
与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError 和OutOfMemoryError
4、Java 堆
对于大多数应用来说,Java 堆(Java Heap)是Java 虚拟机所管理的内存中最大的
一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的
唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在Java 虚
拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配①,但是随着JIT 编译器
的发展与逃逸分析技术的逐渐成熟,栈上分配、标量替换②优化技术将会导致一些微妙
的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。
Java 堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC 堆”(Garbage
Collected Heap,幸好国内没翻译成“垃圾堆”)。如果从内存回收的角度看,由于现在
收集器基本都是采用的分代收集算法,所以Java 堆中还可以细分为:新生代和老年代;
再细致一点的有Eden 空间、From Survivor 空间、To Survivor 空间等。如果从内存分配
的角度看,线程共享的Java 堆中可能划分出多个线程私有的分配缓冲区(Thread Local
Allocation Buffer,TLAB)。不过,无论如何划分,都与存放内容无关,无论哪个区域,
存储的都仍然是对象实例,进一步划分的目的是为了更好地回收内存,或者更快地分配
内存。在本章中,我们仅仅针对内存区域的作用进行讨论,Java 堆中的上述各个区域的
分配和回收等细节将会是下一章的主题。
根据Java 虚拟机规范的规定,Java 堆可以处于物理上不连续的内存空间中,只要
逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小
的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx
和-Xms 控制)。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出
OutOfMemoryError 异常。
方法区(Method Area)与Java 堆一样,是各个线程共享的内存区域,它用于存
储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽
然Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-
Heap(非堆),目的应该是与Java 堆区分开来。
对于习惯在HotSpot 虚拟机上开发和部署程序的开发者来说,很多人愿意把方法区
称为“永久代”(Permanent Generation),本质上两者并不等价,仅仅是因为HotSpot 虚
拟机的设计团队选择把GC 分代收集扩展至方法区,或者说使用永久代来实现方法区而
已。对于其他虚拟机(如BEA JRockit、IBM J9 等)来说是不存在永久代的概念的。即
使是HotSpot 虚拟机本身,根据官方发布的路线图信息,现在也有放弃永久代并“搬家”
至Native Memory 来实现方法区的规划了。
Java 虚拟机规范对这个区域的限制非常宽松,除了和Java 堆一样不需要连续的内
存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾
收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一
样“永久”存在了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸
载,一般来说这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件
相当苛刻,但是这部分区域的回收确实是有必要的。在Sun 公司的BUG 列表中,曾出
现过的若干个严重的BUG 就是由于低版本的HotSpot 虚拟机对此区域未完全回收而导
致内存泄漏。
根据Java 虚拟机规范的规定, 当方法区无法满足内存分配需求时, 将抛出
OutOfMemoryError 异常。
5、运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有
类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool
Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放
到方法区的运行时常量池中。
Java 虚拟机对Class 文件的每一部分(自然也包括常量池)的格式都有严格的规
定,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才会被虚拟机认可、
装载和执行。但对于运行时常量池,Java 虚拟机规范没有做任何细节的要求,不同的
提供商实现的虚拟机可以按照自己的需要来实现这个内存区域。不过,一般来说,除
了保存Class 文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常
量池中①。
运行时常量池相对于Class 文件常量池的另外一个重要特征是具备动态性,Java 语
言并不要求常量一定只能在编译期产生,也就是并非预置入Class 文件中常量池的内容
才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发
人员利用得比较多的便是String 类的intern() 方法。
既然运行时常量池是方法区的一部分,自然会受到方法区内存的限制,当常量池无
法再申请到内存时会抛出OutOfMemoryError 异常
6、直接内存
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java
虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致
OutOfMemoryError 异常出现,所以我们放到这里一起讲解。
在JDK 1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)
与缓冲区(Buffer)的I/O 方式,它可以使用Native 函数库直接分配堆外内存,然
后通过一个存储在Java 堆里面的DirectByteBuffer 对象作为这块内存的引用进行
操作。这样能在一些场景中显著提高性能,因为避免了在Java 堆和Native 堆中来
回复制数据。
显然,本机直接内存的分配不会受到Java 堆大小的限制,但是,既然是内存,则
肯定还是会受到本机总内存(包括RAM 及SWAP 区或者分页文件)的大小及处理器
寻址空间的限制。服务器管理员配置虚拟机参数时,一般会根据实际内存设置-Xmx
等参数信息,但经常会忽略掉直接内存,使得各个内存区域的总和大于物理内存限制
(包括物理上的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError
逻辑内存模型我们已经看到了,那当我们建立一个对象的时候是怎么进行访问的呢?
在Java 语言中,对象访问是如何进行的?对象访问在Java 语言中无处不在,是最普通的程序行为,但即使是最简单的访问,也会却涉及Java 栈、Java 堆、方法区这三个最重要内存区
域之间的关联关系,如下面的这句代码:
Object obj = new Object();
假设这句代码出现在方法体中,那“Object obj”这部分的语义将会反映到Java 栈
的本地变量表中,作为一个reference 类型数据出现。而“new Object()”这部分的语义
将会反映到Java 堆中,形成一块存储了Object 类型所有实例数据值(Instance Data,对
象中各个实例字段的数据)的结构化内存,根据具体类型以及虚拟机实现的对象内存布
局(Object Memory Layout)的不同,这块内存的长度是不固定的。另外,在Java 堆中
还必须包含能查找到此对象类型数据(如对象类型、父类、实现的接口、方法等)的地
址信息,这些类型数据则存储在方法区中。
由于reference 类型在Java 虚拟机规范里面只规定了一个指向对象的引用,并没有
定义这个引用应该通过哪种方式去定位,以及访问到Java 堆中的对象的具体位置,因此
不同虚拟机实现的对象访问方式会有所不同,主流的访问方式有两种:使用句柄和直接
如果使用句柄访问方式,Java 堆中将会划分出一块内存来作为句柄池,reference
中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的
具体地址信息,如下图所示。
如果使用直接指针访问方式,
Java 堆对象的布局中就必须考虑如何放置访问类型
数据的相关信息,reference 中直接存储的就是对象地址,如下图所示
这两种对象的访问方式各有优势,使用句柄访问方式的最大好处就是
reference 中存
储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只
会改变句柄中的实例数据指针,而reference 本身不需要被修改。
使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开
销,由于对象的访问在Java 中非常频繁,因此这类开销积少成多后也是一项非常可观的
执行成本。就本书讨论的主要虚拟机Sun HotSpot 而言,它是使用第二种方式进行对象访问的,但从整个软件开发的范围来看,各种语言和框架使用句柄来访问的情况也十分常见。
下面我们来看几个示例
1、Java 堆溢出
下面的程中我们限制Java 堆的大小为20MB,不可扩展(将堆的最小值-Xms 参
数与最大值-Xmx 参数设置为一样即可避免堆自动扩展),通过参数-XX:+HeapDump
OnOutOfMemoryError 可以让虚拟机在出现内存溢出异常时Dump 出当前的内存堆转储
快照以便事后进行分析。
参数设置如下
Java 堆内存的OutOfMemoryError异常是实际应用中最常见的内存溢出异常情况。出现Java 堆内
存溢出时,异常堆栈信息“java.lang.OutOfMemoryError”会跟着进一步提示“Java heap
要解决这个区域的异常,一般的手段是首先通过内存映像分析工具(如Eclipse
Memory Analyzer)对dump 出来的堆转储快照进行分析,重点是确认内存中的对象是
否是必要的,也就是要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢
出(Memory Overflow)。图2-5 显示了使用Eclipse Memory Analyzer 打开的堆转储快
如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots 的引用链。于是就
能找到泄漏对象是通过怎样的路径与GC Roots 相关联并导致垃圾收集器无法自动回收
它们的。掌握了泄漏对象的类型信息,以及GC Roots 引用链的信息,就可以比较准确
地定位出泄漏代码的位置。
如果不存在泄漏,换句话说就是内存中的对象确实都还必须存活着,那就应当检查
虚拟机的堆参数(-Xmx 与-Xms),与机器物理内存对比看是否还可以调大,从代码上
检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期
的内存消耗。
以上是处理Java 堆内存问题的简略思路,处理这些问题所需要的知识、工具与经验
在后面的几次分享中我会做一些额外的分析。
2、java栈溢出
package com.yhj.jvm.memory.
public class StackOverFlow {
private int
public void plus() {
public static void main(String[] args) {
StackOverFlow stackOverFlow = new StackOverFlow();
stackOverFlow.plus();
} catch (Exception e) {
System.out.println("Exception:stack length:"+stackOverFlow.i);
e.printStackTrace();
} catch (Error e) {
System.out.println("Error:stack length:"+stackOverFlow.i);
e.printStackTrace();
3、常量池溢出(常量池都有哪些信息,我们在后续的JVM类文件结构中详细描述)
import java.util.ArrayL
import java.util.L
public class ConstantOutOfMemory {
public static void main(String[] args) throws Exception {
List&String& strings = new ArrayList&String&();
int i = 0;
while(true){
strings.add(String.valueOf(i++).intern());
} catch (Exception e) {
e.printStackTrace();
4、方法去溢出
package com.yhj.jvm.memory.methodA
import java.lang.reflect.M
import net.sf.cglib.proxy.E
import net.sf.cglib.proxy.MethodI
import net.sf.cglib.proxy.MethodP
public class MethodAreaOutOfMemory {
public static void main(String[] args) {
while(true){
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(TestCase.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
public Object intercept(Object arg0, Method arg1, Object[] arg2,
MethodProxy arg3) throws Throwable {
return arg3.invokeSuper(arg0, arg2);
enhancer.create();
class TestCase{
5、直接内存溢出
package com.yhj.jvm.memory.directoryM
import java.lang.reflect.F
import sun.misc.U
public class DirectoryMemoryOutOfmemory {
private static final int ONE_MB = 1024*1024;
private static int count = 1;
public static void main(String[] args) {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
while (true) {
unsafe.allocateMemory(ONE_MB);
} catch (Exception e) {
System.out.println("Exception:instance created "+count);
e.printStackTrace();
} catch (Error e) {
System.out.println("Error:instance created "+count);
e.printStackTrace();
浏览 10673
浏览: 233558 次
来自: Alien
厉害了,,,请问可以转载到我们专栏吗?会注明来源和地址的~专栏 ...
看了三行就知道是我想要的!!
想找这样一篇文章真的不容易!!!!

我要回帖

更多关于 计算机什么时候用堆栈 的文章

 

随机推荐