Sun JVM 内存模型及垃圾回收策略

Sun JVM 内存模型

Java栈  
  Java 栈是与每一个线程关联的,JVM 在创建每一个线程的时候,会分配一定的栈空间给线程。它主要用来存储线程执行过程中的局部变量,方法的返回值,以及方法调用上下文。栈空间随着线程的终止而释放。 StackOverflowError:如果在线程执行的过程中,栈空间不够用,那么 JVM就会抛出此异常,这种情况一般是死递归造成的。
 
  Java 中堆是由所有的线程共享的一块内存区域,堆用来保存各种JAVA对象,比如数组,线程对象等。
Generation  
JVM 堆一般又可以分为以下三部分:  


Perm
  Perm代主要保存class,method,filed对象,这部门的空间一般不会溢出,除非一次性加载了很多的类,不过在涉及到热部署的应用服务器的时候,有时候会遇到java.lang.OutOfMemoryError : PermGen space 的错误,造成这个错误的很大原因就有可能是每次都重新部署,但是重新部署后,类的 class没有被卸载掉,这样就造成了大量的class对象保存在了perm中,这种情况下,一般重新启动应用服务器可以解决问题。
Tenured
  Tenured 区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在Young复制转移一定的次数以后,对象就会被转移到Tenured区,一般如果系统中用了application 级别的缓存,缓存中的对象往往会被转移到这一区间。
Young
  Young 区被划分为三部分,Eden 区和两个大小严格相同的 Survivor区,其中Survivor区间中,某一时刻只有其中一个是被使用的,另外一个留做垃圾收集时复制对象用,在Young 区间变满的时候,minor GC 就会将存活的对象移到空闲的Survivor 区间中,根据 JVM的策略,在经过几次垃圾收集后,任然存活于Survivor的对象将被移动到 Tenured 区间。

jvmstat

  可用jvmstat查看Java虚拟机信息,可从http://java.sun.com/performance/jvmstat/#Download下载jvmstat,解压后用visualgc 进程id命令即可打开jvmstat.

内存大小配置

  JVM 提供了相应的参数来对内存大小进行配置。

  正如上面描述,JVM中堆被分为了3个大的区间,同时 JVM也提供了一些选项对Young,Tenured 的大小进行控制。
Total Heap  
  -Xms :指定了 JVM初始堆内存大小
  -Xmx:指定JVM 堆得最大内存,在JVM 启动以后,会分配-Xmx 参数指定大小的内存给JVM,但是不一定全部使用,JVM会根据-Xms 参数来调节真正用于JVM的内存
  -Xmx -Xms之差就是三个 Virtual 空间的大小
Young Generation
  -XX:NewRatio=8 意味着 tenured  和  young 的比值 8 : 1 ,这样eden+2*survivor=1/9 堆内存

  -XX:SurvivorRatio=32意味着eden和一个survivor的比值是32:1,这样一个Survivor 就占Young 区的1/34.
  -Xmn 参数设置了年轻代的大小
Perm Generation
  -XX:PermSize=16M -XX:MaxPermSize=64M
Thread Stack

  -XX:Xss=128K

Sun JVM 垃圾回收策略

常见的垃圾收集策略

Reference Counting(引用计数)

  引用计数是最简单直接的一种方式,这种方式在每一个对象中增加一个引用的计数,这个计数代表当前程序有多少个引用引用了此对象,如果此对象的引用计数变为0,那么此对象就可以作为垃圾收集器的目标对象来收集。
  优点:简单,直接,不需要暂停整个应用
  缺点: 1.需要编译器的配合,编译器要生成特殊的指令来进行引用计数的操作,比如每次将对象赋值给新的引用,或者者对象的引用超出了作用域等。 2.不能处理循环引用的问题
跟踪收集器

  跟踪收集器首先要暂停整个应用程序,然后开始从根对象扫描整个堆,判断扫描的对象是否有对象引用,这里面有两个个问题需要搞清楚:
  1.如果每次扫描整个堆,那么势必让 GC 的时间变长,从而影响了应用本身的执行。因此在 JVM 里面采用了分代收集,在新生代收集的时候 minor gc只需要扫描新生代,而不需要扫描老生代。
  2.JVM 采用了分代收集以后,minor gc只扫描新生代,但是 minor gc 怎么判断是否有老生代的对象引用了新生代的对象,JVM采用了卡片标记的策略,卡片标记将老生代分成了一块一块的,划分以后的每一个块就叫做一个卡片,JVM采用卡表维护了每一个块的状态,当 JAVA 程序运行的时候,如果发现老生代对象引用或者释放了新生代对象的引用,那么就JVM就将卡表的状态设置为脏状态,这样每次minor gc 的时候就会只扫描被标记为脏状态的卡片,而不需要扫描整个堆。

Mark-Sweep Collector(标记-清除收集器)

  标记清除收集器最早由 Lisp 的发明人于 1960 年提出,标记清除收集器停止所有的工作,从根扫描每个活跃的对象,然后标记扫描过的对象,标记完成以后,清除那些没有被标记的对象。
  优点:
  1 解决循环引用的问题
  2 不需要编译器的配合,从而就不执行额外的指令
  缺点:
  1.每个活跃的对象都要进行扫描,收集暂停的时间比较长。

Copying Collector(复制收集器)   
  复制收集器将内存分为两块一样大小空间,某一个时刻,只有一个空间处于活跃的状态,当活跃的空间满的时候,GC 就会将活跃的对象复制到未使用的空间中去,原来不活跃的空间就变为了活跃的空间。

  优点:
  1 只扫描可以到达的对象,不需要扫描所有的对象,从而减少了应用暂停的时间
  缺点:
  1.需要额外的空间消耗,某一个时刻,总是有一块内存处于未使用状态
  2.复制对象需要一定的开销

Mark-Compact Collector(标记-整理收集器)  

  标记整理收集器汲取了标记清除和复制收集器的优点,它分两个阶段执行,在第一个阶段,首先扫描所有活跃的对象,并标记所有活跃的对象,第二个阶段首先清除未标记的对象,然后将活跃的的对象复制到堆得底部。

  Mark-compact策略极大的减少了内存碎片,并且不需要像Copy Collector一样需要两倍的空间。

Sun JVM的垃圾收集策略

    GC 的执行时要耗费一定的 CPU 资源和时间的,因此在 JDK1.2以后,JVM引入了分代收集的策略,其中对新生代采用"Mark-Compact"策略,而对老生代采用了“Mark-Sweep"的策略。其中新生代的垃圾收集器命名为“minor gc”,老生代的 GC 命名为"Full Gc 或者Major GC".其中用 System.gc()强制执行的是Full Gc.

Serial Collector

  Serial Collector 是指任何时刻都只有一个线程进行垃圾收集,这种策略有一个名字“stop the whole world",它需要停止整个应用的执行。这种类型的收
集器适合于单CPU 的机器。
  Serial Copying Collector
  此种GC 用-XX:UseSerialGC 选项配置,它只用于新生代对象的收集。
  1.5.0 以后. -XX:MaxTenuringThreshold 来设置对象复制的次数。当 eden 空间不够的时候,GC会将eden的活跃对象和一个名叫From survivor空间中尚不够资格放入 Old代的对象复制到另外一个名字叫 To Survivor的空间。而此参数就是用来说明到底 From survivor中的哪些对象不够资格,假如这个参数设置为31,那么也就是说只有对象复制31次以后才算是有资格的对象。 这里需要注意几个个问题:
  1.From Survivor 和To survivor的角色是不断的变化的,同一时间只有一块空间处于使用状态,这个空间就叫做 From Survivor区,当复制一次后角色就发生了变化。
  2. 如果复制的过程中发现 To survivor空间已经满了,那么就直接复制到old generation.
  3.比较大的对象也会直接复制到 Old generation,在开发中,我们应该尽量避免这种情况的发生。
  Serial  Mark-Compact Collector
  串行的标记-整理收集器是 JDK5 update6之前默认的老生代的垃圾收集器,此收集使得内存碎片最少化,但是它需要暂停的时间比较长

Parallel Collector   

  对年轻代进行并行垃圾回收,因此可以减少垃圾回收时间。一般在多线程多处理器机器上使用。使用-XX:+UseParallelGC.打开。并行收集器在J2SE5.0第六更新上引入,在Java SE6.0中进行了增强,可以对年老代进行并行收集。如果年老代不使用并发收集的话,是使用单线程进行垃圾回收,因此会制约扩展能力。使用-XX:+UseParallelOldGC打开。使用-XX:ParallelGCThreads=<N>设置并行垃圾回收的线程数。此值可以设置与机器处理器数量相等。

  此收集器可以进行如下配置:

  最大垃圾回收暂停:指定垃圾回收时的最长暂停时间,通过-XX:MaxGCPauseMillis=<N>指定。<N>为毫秒.如果指定了此值的话,堆大小和垃圾回收相关参数会进行调整以达到指定值。设定此值可能会减少应用的吞吐量。

  吞吐量:吞吐量为垃圾回收时间与非垃圾回收时间的比值,通过-XX:GCTimeRatio=<N>来设定,公式为1/(1+N)。例如,-XX:GCTimeRatio=19时,表示5%的时间用于垃圾回收。默认情况为99,即1%的时间用于垃圾回收。

Concurrent Collector  

  可以保证大部分工作都并发进行(应用不停止),垃圾回收只暂停很少的时间,此收集器适合对响应时间要求比较高的中、大规模应用。使用-XX:+UseConcMarkSweepGC打开。

  并发收集器主要减少年老代的暂停时间,他在应用不停止的情况下使用独立的垃圾回收线程,跟踪可达对象。在每个年老代垃圾回收周期中,在收集初期并发收集器会对整个应用进行简短的暂停,在收集中还会再暂停一次。第二次暂停会比第一次稍长,在此过程中多个线程同时进行垃圾回收工作。

   并发收集器使用处理器换来短暂的停顿时间。在一个N个处理器的系统上,并发收集部分使用K/N个可用处理器进行回收,一般情况下1<=K<=N/4。

   在只有一个处理器的主机上使用并发收集器,设置为incremental mode模式也可获得较短的停顿时间。

 浮动垃圾:由于在应用运行的同时进行垃圾回收,所以有些垃圾可能在垃圾回收进行完成时产生,这样就造成了“Floating Garbage”,这些垃圾需要在下次垃圾回收周期时才能回收掉。所以,并发收集器一般需要20%的预留空间用于这些浮动垃圾。

   Concurrent Mode Failure:并发收集器在应用运行时进行收集,所以需要保证堆在垃圾回收的这段时间有足够的空间供程序使用,否则,垃圾回收还未完成,堆空间先满了。这种情况下将会发生“并发模式失败”,此时整个应用将会暂停,进行垃圾回收。

   启动并发收集器:因为并发收集在应用运行时进行收集,所以必须保证收集完成之前有足够的内存空间供程序使用,否则会出现“Concurrent Mode Failure”。通过设置-XX:CMSInitiatingOccupancyFraction=<N>指定还有多少剩余堆时开始执行并发收集


小结:

  串行处理器:

    --适用情况:数据量比较小(100M左右);单处理器下并且对响应时间无要求的应用。

    --缺点:只能用于小型应用

  并行处理器:

    --适用情况:“对吞吐量有高要求”,多CPU、对应用响应时间无要求的中、大型应用。举例:后台处理、科学计算。

    --缺点:应用响应时间可能较长

  并发处理器:

    --适用情况:“对响应时间有高要求”,多CPU、对应用响应时间有较高要求的中、大型应用。

    举例:Web服务器/应用服务器、电信交换、集成开发环境。

常见配置举例

堆大小设置

  JVM中最大堆大小有三方面限制:相关操作系统的数据模型(32-bt还是64-bit)限制;系统的可用虚拟内存限制;系统的可用物理内存限制。32位系统下,一般限制在1.5G~2G;64为操作系统对内存无限制。如在Windows Server 2003 系统,3.5G物理内存,JDK5.0下测试,最大可设置为1478m。

  典型设置:java -Xmx3550m -Xms3550m -Xmn2g -Xss128k

  -Xmx3550m:设置JVM最大可用内存为3550M。

  -Xms3550m:设置JVM促使内存为3550m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。如调用本地方法,则不能设置成相同。

  -Xmn2g:设置年轻代大小为2G。整个堆大小=年轻代大小 + 年老代大小。增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。

  -Xss128k:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。

  java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4

  -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0

  -XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆的1/5

  -XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6

  -XX:MaxPermSize=16m:设置持久代大小为16m。

  -XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概率。

回收器选择

  JVM给了三种选择:串行收集器、并行收集器、并发收集器,但是串行收集器只适用于小数据量的情况,所以这里的选择主要针对并行收集器和并发收集器。默认情况下,JDK5.0以前都是使用串行收集器,如果想使用其他收集器需要在启动时加入相应参数。JDK5.0以后,JVM会根据当前系统配置进行判断。

吞吐量优先的并行收集器

如上文所述,并行收集器主要以到达一定的吞吐量为目标,适用于科学技术和后台处理等。

典型配置: java -Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC

-XX:ParallelGCThreads=20

-XX:+UseParallelGC:选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集。

-XX:ParallelGCThreads=20:配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。

java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC

-XX:ParallelGCThreads=20 -XX:+UseParallelOldGC

-XX:+UseParallelOldGC:配置年老代垃圾收集方式为并行收集。JDK6.0支持对年老代并行收集。

java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100

-XX:MaxGCPauseMillis=100:设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。

java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100 -XX:+UseAdaptiveSizePolicy

-XX:+UseAdaptiveSizePolicy:设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低响应时间或者收集频率等,此值建议使用并行收集器时,一直打开。

响应时间优先的并发收集器

如上文所述,并发收集器主要是保证系统的响应时间,减少垃圾收集时的停顿时间。适用于应用服务器、电信领域等。

典型配置: java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC

-XX:+UseConcMarkSweepGC:设置年老代为并发收集。测试中配置这个以后,-XX:NewRatio=4的配置失效了,原因不明。所以,此时年轻代大小最好用-Xmn设置。

-XX:+UseParNewGC:设置年轻代为并行收集。可与CMS收集同时使用。JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值。

java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection

-XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。

-XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是可以消除碎片.

调优总结

年轻代大小选择

  响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。

  吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。

年老代大小选择

响应时间优先的应用:

年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得:

l  并发垃圾收集信息

l  持久代并发收集次数

l  传统GC信息

l  花在年轻代和年老代回收上的时间比例

l  减少年轻代和年老代花费的时间,一般会提高应用的效率

吞吐量优先的应用:

  一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。

较小堆引起的碎片问题

  因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行如下配置:

-XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。

-XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩

posted @ 2012-09-13 23:35  cprime  阅读(299)  评论(0编辑  收藏  举报