`
red_xie
  • 浏览: 25852 次
  • 性别: Icon_minigender_1
  • 来自: 成都
社区版块
存档分类
最新评论

JVM和Memory

    博客分类:
  • java
阅读更多
转载之前:本篇较长~内容不错~

转载地址:http://spaces.msn.com/songsun/

JVM & Memory (1) paging
近期看了一些有关JVM和内存的资料,为了避免遗忘,特在这里作一笔记。今天说说操作系统的虚拟内存先。

虚拟内存管理(VMM)是现在操作系统广泛采用的内存管理方式,为什么出现VMM了呢,当初主要是为了解决物理内存不足的问题,既然是不足,设计师们当然就想起来用硬盘空间来协力,使用诸如LRU(最近最少使用)之类的算法,把物理内存(Main memory)中不怎么使用的内容,转储到硬盘上以节省空间(给急需内存的程序使用),而转储到硬盘上的内容又被访问时,再转载回来(如果总是发生这样的事,系统地效能就大大折扣了,这也是好的算法应该避免的)。

题外话:虽说VMM的实现主要是操作系统的事,但早期没有可支持VMM管理的CPU诞生时,如Intel系列,在386以前,要纯粹依靠OS自身做VMM,不仅麻烦而且效率也很差。要知道VMM是1960年以前就有这概念了,早期的实现者(如果有的话)肯定很郁闷。

继续正题,怎么做到VMM的呢,首先要定义一个概念,叫做虚拟地址空间(virtual memory spaces),这个空间就是进程是装载和运行的容器,以win32为例,32位的虚拟地址空间(virtual memory spaces)可以达到4GB那么大,这对于一般应用程序来说,几乎是富裕的太多了。这么大的空间,为了便于管理,首先按照特定粒度划分一个最小的基本单元,叫做页(Page),比如等于64kb吧,页有三类:Free,Reserved,Committed,Free的页对于进程来说,等于是不可用的,任何读写free页的行为都是极其无耻卑劣和下流的,要被OS严惩不贷(可以放心的是,你的Java程序不会发生这类行为,只有那些可以直接内存寻址的语言才可能发生);而Reserved的页是进程保留将来要使用的,这部分暂时还没有实际对应(物理内存或硬盘);Committed是OS已经提交给进程供其使用的,这部分虚拟内存可以由虚地址转换到实际的物理地址。

为什么要先说VMM呢,这和JVM有啥联系呢?这是因为JVM的堆(指Java heap)内存是动态扩张的,但它又总是连续的(JVM为了便于垃圾清理),怎么做到这样呢?JVM会首先申请一大块内存作堆,而其中只有开始使用的部分是Committed的,剩余的都是Reserved的,随着扩张过程,逐步把Reserved的变成Committed。而当JVM发觉堆过大值得减小时,又会把尾部的一部分堆释放回Reserved状态。

访问虚拟空间的每个地址的内容时,首先会把这个虚拟地址转换成物理地址,转换过程一般由cpu自己来完成,很快&很准,所以你不用担心速度问题或者转换出错。每一页的基址转换好了,页内的地址用过偏移地址访问即可,几乎没有转换的开销。所以把这个转换的过程叫做Paging,如果页是Free的, CPU会发生access violation,二话不说就直接转去执行寻址违例的处理过程,所以windows蓝屏会来的那么突然啊!如果页是Resreved或者页被交换到硬盘上,这时不能直接在物理内存中找到对应,那么发生Page fault,CPU转去执行相应例程做补救,把页交换回来,过程虽然费事,但进程自己什么也感觉不到(用户可就不能幸免了)。这些关键功能都是现代CPU直接具备的,OS只要把相应处理例程准备好就OK。

附一张切来的paging示意图,点击可看大图。

到此,一个庞大的虚拟地址空间就完全为进程准备好了,但别以为应用程序能够使用它的全部,因为操作系统及接口也是应用程序赖以生存的必不可少的,它们往往也预先落户在进程的虚拟地址空间里。

VMM还促使动态链接库(dll)技术(这个和jni有点联系)大行其道,这是别话了。

注:棕色部分为后补内容。







二:

JVM & Memory (2) java.exe
上篇说到了关于java heap的一些特征:连续的内存区域,逐步扩张的特征和如何做的这点的。but在讨论JVM的堆内存的细节以及垃圾回收等事宜之前,我们先讨论一下java进程的内存结构。

郁闷,昨日这一篇本来写完了,但是提交失败了。。。

只好再写了,简略些吧,继续正题:

通常我们所关注JVM的内存区域是java heap(堆),比如有时发生内存泄露(memory leak),或者内存溢出(out of memory)的问题,让我们不得不把堆的问题弄透彻。先提一个入门的小问题,而为什么c/c++就不用关注这个呢(它也有堆内存的申请)?那是因为, java语言为了让开发者省事,在语言设计上就没有提供内存释放的语句(比如delete/free那样的)并强制大家使用,而是采用了程序只管创建(new)对象即可,由JVM的Garbage collector全权负责回收无用的对象的方式。与c/c++那样的构造/析构方式相比,两者各有利弊,此处不再废话。

然而堆内存只是java进程所占内存空间上的一部分,JVM还有哪些主要的内存区域呢?首先是JVM native code segment,这部分内存空间被jvm中各部件的二进制执行代码所占据,属于固定开销,我们无法更改或影响其所占的大小,具体都有哪些部件呢,如执行引擎即解释器(exec engine/interpreter)、class loader、内存管理器、gc器、jit编译器、与OS的接口(主要有6大类:System/Memory/Library/Thread/File/Socket)等(这些部件都是老虎屁股,摸不得的);其次,是Java stack(栈),是解释器用于解释执行字节码指令的工作区域,也是本地变量的藏身之处(不明白什么是本地变量请自己翻书),一般每线程拥有独立的一段,如果线程很多,函数调用深度很深,本地变量繁多,那么,java stack自然庞大,另外,为了防止java stack开销过大,一个线程的调用深度是有限制(一般jvm命令行可指定)的,超过了就会发生stack overflow,要说明的是,java stack是jvm native heap的一部分,当然,java heap也是jvm native heap的一部分;第三,与java stack相仿的是native stack,不过它可不包含在jvm native heap之中,这是来自于jvm自身组件和native函数的普通code(非字节码的哦)在函数调用时必须要用到的栈(学过汇编语言的,这些都不难理解),这部分内存我们也无能为力;还有一部分重要的区域是method area,这部分内存存放了所有已加载class的字节码(注意class loader加载某类时,还会产生该类唯一的一个java.lang.Class实例,它存在于java heap中),这部分内存大小和系统运行时需要运行的类和代码的多寡有关,而它也不是一成不变的,当class gc不被禁止时,如果某类的Class对象实例被gc时,其method data也将被清除,而jit编译器或者hotspot编译器在运行时也会改动它们(以后再探讨这部分),显而易见,method area是jvm native heap中的一块;加上java heap以及class manifest等,jvm的内存基本上可以按以上划分区域,切个图表示一下吧。言多必失,各类jvm实现毕竟有所不同,而jvm规范又总是比较abstract。

再来说一下你的程序运行时,数据都存储在哪里(此时你也该明白了吧)。不考虑有native方法的情形(以后再细说它),那么:

class中的byte code会放在method area中(不用担心实例方法的重合,实例方法并不特殊于静态方法,只不过第一个参数隐含是this,而jvm用数字给本地变量命名,this的终身编号总是1);
本地变量(local variable)的值(对于原生类型,就是数据本身,若是引用类型则只是它的引用指针值,下同)存在于java stack中;
而所有的成员变量(instance field和static field)的值和所有的对象实例(Object,注意也包含一个class的Class对象)本身都存在于java heap中。
总的看来,大批量的数据(尤其是生命周期不长的动态的数据)还是存放在java heap上的,对于java heap,一个看似矛盾的事实是,原生数据(primitive)无法直接存在于java heap上,而java heap却主要地存放着它们。后面将专门写一篇关于java heap的。

编末:重写一遍,和原来的内容大相径庭啊,不过神没散就成。

从以上,我们基本可以先得出一部分best practices:

缩减你的系统中class的总个数。jvm随着版本升级,越来越庞大了,单是一个xml parser,类的个数已经成百上千。多一个类,意味着在java heap之外,仍然多一份内存开销。当你看到一个系统一开始运行就装载上万个class时,你的心情一定不轻松。
降低类之间的关联度,包括继承、域引用和方法调用,静态域也要注意释放(赋值为null),不仅在类加载和初始化获得效率,对于服务器程序来说,也使得class gc能够顺利进行。在多线程、热部署、长时间运行的大型环境下,稍不留神,JVM里就会发生极其龌龊的事情。
再次修改注:静态变量也是在java heap上的,原稿误解了。





三:


JVM & Memory (3) thread
以下继续探讨,说说jvm的线程(thread)及其执行中和内存相关的问题。but今天是写不完了,请保持关注,先写个预告栏咯

记得当年学java之初,总是很鄙夷它,因为那时候对c/c++很痴迷,结果呢,第一个多线程程序还是拿java写出来的。而线程(thread),本是操作系统的所提供/支持的,所以当初有一段时间,我总在怀疑我那java程序创建的线程到底是不是真正的操作系统线程,现在看来很可笑。

题外,再继续质疑一下:在有些平台上,比如Linux或者大部分的unix,只有进程而没有线程的概念的,但这些平台上,进程间通讯的手段极其发达,所以jvm会用进程模拟线程,效果也是一样的。不过,也有一些平台如sorlaris,在java线程和os内核线程间,存在着不是1对1的关系,实在让我挠头,容以后再去把它搞明白。

简单的说,jvm如何实现多线程呢,首先要有os的支持包括变相的支持,那么jvm通过os提供线程接口创建新线程,这些新线程当然还不能直接执行你的 thread对象的run方法,但它会拥有并执行属于该线程的一个java interpreter实例或者上下文,java解释器再去执行你的run方法,当run方法结束(自然结束时),java interpreter发现没有字节码可以解释了,也宣告结束,OS线程也就自然终结了。至于要实现线程的终止,挂起,恢复,也都可以从OS那里找到相应的接口,这里不再细说,而主要关注对内存的影响,主要是java stack和native call等问题。

待续


当执行引擎(即java interpreter,对应一个ExecEnv)开始解释执行一个java method以前,它首先初始化自身,即分配自己的initial java stack,栈开始时尺寸很小,随着使用的需要而扩展,存在一个400k的默认长度限制( -Xss<size>可更改该值);ExecEnv有两个重要的标志(或指针),一个是pc,即程序计数器,另一个是optop,即栈顶,也是最上面一个操作数(即本地变量)的位置。pc总是指向要执行的byte code(随着每一条byte code的执行,pc自动改变),而optop指向java stack的顶端(也随某些byte code的执行而改动)。当方法执行时,方法的invoker将pc指向method block的code位,同时,在java stack里为该方法新建java frame(每一个方法在编译期就确定了运行它所需的最大stack size,所以,新建java freame时,会按照这个size去检查java stack是否满足,若不足,则扩展java stack,而java stack已经达到最大长度限制时,扩展失败,发生stack overflow),准备工作完成后,还给ExecEnv继续解释和执行;当方法执行完毕时,一条ret字节指令将pc和optop送回前一个java frame所记录的位置。所以,对于某个线程来说,其java stack完整的记录了所深入的每一层java method(每层一个java frame),除了native method,本质上是因为c的堆栈不能记录。

以上是执行java method,若是native method呢?很简单,native method在类加载时,即被jvm安排了一个native invoker在其method block中,继而在ExecEnv执行一条call该native method的字节指令时,native invoker被执行,如果是首次执行,那么这个native mothod的method block的code段尚未挂接,native invoker检查到这个信息,将根据method name和signature搜索系统中已加载的dll的合适挂接点,将找到的挂接点挂在code上,而后调用该code段即可(找不到挂接点,会发生什么异常你应该清楚)。过程中,除非你的native method中又反调了java method,那么,java stack将毫无变化。

再来关注一下native method的内存模型:native mthod自身的机器码code存在于dll镜像(image)中;nm的本地变量存在于线程堆栈内(指native thread,和java thread是绑定着的);nm用到的静态变量存在于dll的共享变量区中;nm制造的java对象(通过jni手段new来的)依然在java heap;而nm声明的global ref以及local ref存放在jvm的native c heap上。本地方法也会带来内存泄露(c的内存泄露,也可能是java heap的global ref的未释放),特别是c heap的泄露,运行期是极难探测和定位的。

最后,说说调用深度过深(即java stack会很长)带来的负面影响。在前一节,我们知道本地变量都寄存在java stack中。如果调用深度过深,特别是jvm长时间运行在较高深度调用的情形下,意味着在栈上的各frame中,存在着很多变量,它们中的引用类型将一直保持对java heap上对应对象的引用而导致gc器始终不能释放它们;其次,较长的java stack也给gc器造成了较多的扫描时间(为何要扫描后面再说);第三,method call本身也是一种时间开销,对于很短小的method,call它比它本身代码执行的时间还要多,好比机关枪打蚊子,这也是现代编译器非常讲究inline优化的原因。


四:

JVM & Memory (4) gc

不同的JVM实现对堆结构的设计有所不同,这里先说说共性的,然后再比较classic vm和hotspot vm在gc方面的差异。

先大致说说gc的过程,通常有两种情形会导致gc发生,一种是显式的System.gc()调用而java进程未禁止显示gc,第二种是隐式的,即内存管理器(MM)在alloc内存时发生failure,MM进而作gc以便释放出空间用于分配(当然,假如gc后还是没有空间可满足分配数值,OutOfMemory就发生了)。gc过程分3步走,第1步,mark阶段,标示出allocbits和markbits,allocbits 代表当前java heap中存在的对象所占内存的bit映射,markbits代表所有reachable对象所占内存的bit映射,是allocbits的子集。这一步,一般是要锁定java heap的,但有些gc器能做到并发/并行mark(后面有解释)。第2步,sweep阶段,即将markbits和allocbits的补集所代表的内存区域作回收,这也包括对class的回收,假如classgc未被禁用,而jvm确实找到了未被使用的class,那么除了java heap中的Class对象被回收,method area里该class的method data也被释放。第3步,compact阶段(可选的,jvm会自行判断是否进行该步),整理碎片,两种技巧,一种是移动琐碎的对象区域,使之连续,一种是移动琐碎的free区域,也使之连续。这步也必须要锁定java heap。compact时还要做一个工作,即将所移动的对象的原始引用值作同步更新。这时存在一个问题,假如某thread stack里的一个浮点数碰巧看起来像是一个对象引用值(内存泄露也会因此情形而发生),那么jvm因为无法断定而放弃移动该对象,等待以后再移动(据称,hotspot vm对精确判定引用值的问题解决的较好)。

再解释一下mark阶段,为了得到markbits,首先要收集root references,也就是java thread stacks refs, jni global refs,thread moniters,interned strings,and soft/weak refs等,它们是对象引用的源头,从root referent object顺藤摸瓜,即可把所有reachable object标示出来。再解释一下bits映射,每一个bit代表每一个内存GRAIN(是分配的最小粒度),假设GRAIN是8 byte,那么对于64M的堆,需要额外的1M的bits来映射,这部分额外开支需要gc器向c heap申请,当堆扩展和收缩时,markbits也要随之变动。mark阶段工作量比较大,因为要扫描和遍历,所以后期的jvm都采用多种新的方式如 concurrent(普通线程也参与mark)或parallel(多cpu有效)进行mark。

再说堆的扩张(Expansion)和收缩(Shrinkage)。当内存不足时,gc清理一遍后还发现free空间不能满足分配的尺寸要求,那么它有两种手段继续推进,如果最大堆(-Xmx)未达到,那么扩展堆,如果已达到,那么强制回收soft/weak referents,如果此时还不能满足,那么OutOfMemory就发生了。而当经过一段运行时间后,jvm判断free区域较多(不同的jvm判定依据有所不同),那么就会收缩java heap。收缩的好处有两个,1是减轻OS的负担,2又减少了jvm内存管理器的维护成本。注意,不论再怎么收缩,也不可能小于最小堆(-Xms)。默认的ms值和mx值各vm提供商有所不同,请参阅其文档。

个人认为主流的jvm主要有3种,sun classic, ibm classsic,sun hotspot。sun classsic用于1.3.0以前,之后sun的jvm都是hotspot。而ibm的jdk一直是classsic,不知其5.0的vm是否会采用 hotspot。sun的是主流无可置疑啦,为什么扯上ibm的呢,因为看过一个民间的性能评测结果,ibm 1.42vm比sun的1.42vm甚至略好。sun classic使用间接句柄分配对象,所以堆分为两部分,一块放object,一块放handle。这个好处是compact方便得很,缺点也是很严重的缺点,是对象访问不直接,要多一层指针转换,对性能大有影响。ibm classic的具体细节的资料比较缺乏,但根据其white paper,似乎使用的是直接句柄(否则性能无法比sun的好),另外它将java heap划分成system heap和一般heap,仅将jvm的系统class和对象分配到system heap里,它还支持并行和并发的gc。sun hotspot使用直接句柄,似乎又采用了一些高级手段,做到了精确辨别,因此快而且回收准确,另外提供了多种的gc器,适用于不同场合。值得一提的是它的代生gc器(Generational Copying Collection),根据对象生命周期模型(小对象居多,生命周期短),使用类似stack的方式,将同一时期产生的对象放在同一容器 (nursery)中,这批对象基本上会同时消亡,这样,回收时可直接将容器释放,避免了繁琐的逐个对象释放的过程,因此可以减少gc的花费时间(duration)和频度(frequency),并且容器是现成的,所以分配时,寻找free空间很方便,所以提高了分配的效率。

本篇再加上前面的几篇,已经把JVM和Memory相关的主要机理和过程都已经覆盖了,下一篇主要是关于tuning和trouble shooting,请关注。

补:再说一下内存泄露(memory leak),与c/c++的内存泄露的概念不同,java的memory leak是指无用(unused)的对象因为仍然是reachable的,所以不能被gc,因而造成内存的被白白占用。造成memory leak的原因是多样的,比如程序未及时给变量赋null,再比如上文提到的浮点数被碰巧当成对象引用,或者jni程序里声明的global ref忘了做撤销。对于服务器程序来说,memory leak的逐渐积累,只要运行时间足够,必然造成OutOfMemory。如何确定jvm是否有内存泄露呢,假使你没有监测工具也不要紧,打开- verbose:gc开关,运行系统足够长的时间(约收集20到50次gc的数据),然后将verbosegc的信息作收集,察看java heap的谷值(gc后java heap的大小,峰值则表示gc前java heap的大小)是否有递增的倾向,如果倾向明显,那么很有可能是memory leak问题。


五:

2005/11/24
JVM & Memory (5) final
先说tuning,gc有两个指标,一个是frequentcy(以下称F),一个是duration(以下称D)。前者和程序产生garbage的速率和java heap大小有关,后者和java heap大小及gc thread数有关。依据应用程序的差异,F可能为30秒/次或者数小时/次不等,都是可接受的,而D则越短越好,因为它jvm让人感到pause的时刻。在应用程序和gc thread数给定的条件下,F和D是矛盾的,一个的减小意味着另一个的增大,因为F低了,说明heap size很大,而大的heap意味着gc要花费较多的时间,即D要增加,反之亦然。下面是几条准则:

你应该设置gc thread数与系统的cpu个数一致,道理很简单,多了是无用的,少了是浪费cpu。
min heap size应该设置在java heap的谷值或略大的水平,谷值和峰值如果没有profiling工具,打开-verbose:gc做计算。
max heap size约设置为java heap峰值的10/7倍,也就是max heap size的70%约等于峰值。也可以更大些,2倍左右。一定要注意物理内存的多寡,不要大得被paging到硬盘上去了,那就得不偿失了。
最后,稳定压倒一切,继续用-verbose:gc观测一段时间(有时甚者要多天),看看F和D是否可以保持在稳定的水平上。如果排出额外因素的影响还是波动太大,说明还需要进一步调节。
以上是一般过程,当然,你还应该参考所使用的jvm的document,注意其特征。

续trouble shooting
内存方面的trouble shooting主要就是解决OutOfMemory(以下简称OOM),导致OOM的原因总的来说有2种:第一是内存泄露,随着运行时间增加,内存逐渐吃紧,终于不治;第二是实际需要大于最大heap size,也就是说-Xmx太小了,需要调高(如果你上面的tuning做好了,就不会这样了)。直接原因就五花八门了,比如有的jvm,当在80%的cpu时间内,不能gc出2%的heap时,也发生OOM,还有一种可能是,你的heap实际使用并不大,free空间总数是充足的,但不巧的是free空间都是碎片而heap又不能compact,那么内存分配请求失败,发生OOM。

一般发生OOM时,jvm会导出heapdump,即使你对系统一无所知,只拥有一个heapdump就足以了。IBM alphaWorks上有一个HeapRoots工具可以用来分析heapdump(最好使用ibm的jre运行该工具,对于有些phd格式的堆,还用当使用svcdump.jar)。
假如你没有heapdump可用,那么需要检查系统是否禁止了heapdump,甚至有时需要手工使jvm产生heapdump(unix环境下,kill -3 pid,win32下,ctrl+break)。
一切就绪后,使用命令行
> java -Xbootclasspath/p:svcdump.jar -Xmx768M -jar HR207.jar -i heapdump.xxxx.phd
注意:如果heapdump文件本身很大,-Xmx768M需要增大;分析时尽量使用高配置的计算机,否则太慢,你会比较郁闷。

启动heaproots作分析,load完毕后:

输入p命令(help可以列出所有命令的注释)作预处理。
然后使用ot和ts分析对象和类所占空间的排序,默认列出前25个。
过程中过滤器很有用,com/foo为过滤则仅输出com.foo为前缀类型。
所占堆排名靠前的那些类别或对象,需要检查是否是内存泄露的结果或者是否有代码优化的余地。
如果对某对象感兴趣,使用i 0x<addr>命令深入追踪之。


注:一般发生OOM时,有的jvm在heapdump之前,会作一次gc,以利对堆的分析。

再注:以上的手段在实际中多次应用,有效率达99.9999%以上!



  • 大小: 18.6 KB
  • 大小: 30.8 KB
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics