JVM内存分配与垃圾回收浅析

想做architect,就必须对JVM的性能有所了解。JVM的内存管理是性能的一大瓶颈。JVM的性能调优,必须建立在对内存管理策略理解的基础之上。内容太多,简单的写写。

Agenda是: JVM内存划分 ->垃圾回收算法->内存分配策略->JVM垃圾收集器分类

JVM内存划分

1.方法区:存放Class定义,常量,全局变量
2.堆:存放对象实例
3.栈:(线程私有)对正在运行方法指令,变量值,实例引用进行压栈
4.PC计数器:(线程私有)记录当前指令位置
5.本地方法区:(线程私有)native code占用的内存。


堆中大量的实例死去以后,需要进行垃圾回收。否则就out of memory.方法区里面也需要进行垃圾回收,尤其是osgi和jsp的应用。大量的class动态生成,也会导致方法区out of memory.

满足下面条件,方法区的class定义所占内存将被回收

  1. class所有的实例均被回收。
  2. class的classloader被回收。
  3. class没有再被引用,以免使用reflect被初始化。

JVM启动参数 -Xnoclassgc 表示不对方法区进行垃圾回收。请谨慎使用。方法区垃圾回收到此结束。

后面所有的讨论都是关于堆中的垃圾回收。

回收算法

简单的说,如果一个实例没有再被使用,则可以当作垃圾被回收。如何判断实例是否被引用?通过根搜索,从GC roots开始,如果能找到实例,则实例还在被使用,如果找不到,则实例就已经死去。

GC roots可以是:

  • 栈中reference
  • 方法区中的reference

Java中对reference又分4类:

  • 强引用,不能回收
  • 软引用,第一次GC发生时,不回收,第二次回收
  • 弱引用,回收
  • 虚引用,回收 (虚引用我们接触不到,它是JVM用于接收垃圾回收后发送事件)。

新生代与老生代

堆中实例又被分为新生代和老生代。简单的理解,新生代是生命周期短,朝生夕死的实例,GC发生时新生代的成活率不足10%。老生代是一旦生成,就很难死去的实例。它们的成活率能占到近2/3.

新生代向老生代转变的几个条件:

  1. 新生代超过N次GC都没被回收,将变为老生代。N可以通过JVM参数指定,-XX:MaxTenuringThreshold=N. Parallel收集器默认是15.
  2. 如果实例需要的内存大小超过M,则直接分配到老生代。 M可以通过JVM参数指定。-XX:PretenureSizeThreshold=M。
  3. 如果suvivor中 同一代的实例在占据了survivor区(稍后讲)的一半以上,则年龄大于等于此代的一同进入老生代。
  4. Minor GC时,Survivor无法容纳存活新生代,由老生代空间担保,新生代被挪入老生代。

回收算法大体3类

  1. 标记-清除  顾名思义,就是将还在用的实例进行标记,然后将没标记的内存全部释放掉。 缺点是这将导致不连续的内存碎片,使得后面无法进行大块内存分配,从而导致再次出发GC。 老生代收集器CMS使用的算法。碎片也是CMS收集器的巨大缺点。
  2. 复制    新生代所使用的算法。新生代的内存分成3块,Eden和2个survivor. 因为大部分新生代成活率不到10%,所以,使用复制算法,将存活的实例复制到其中一个空白的survivor上,然后释放eden和另一个survivor的内存。两个survivor交替使用。上次GC用你,下次GC用我。默认情况下,Eden的大小是一个survivor大小的8倍。两个survivor相等。所以一个survivor占新生代内存的10%。 使用-XX:SurvivorRatio=8来改变eden与survivor的比例。
  3. 标记-整理  老生代经常使用的算法。和清除不同,将标记存活的实例向前移动,使得它们在内存上是存放连续的。

堆内存分配策略与GC触发

堆的大小由下面参数决定。
-Xms 堆最小值, -Xmx 堆最大值。

堆的大小动态调整
-XX:MaxHeapFreeRatio=70 当堆剩余空间达到70%,堆将变为Xms最小值。
-XX:MinHeapFreeRatio=40 当堆剩余空间低于到40%,堆将变成Xmx最大值。

堆分新生代和老生代。
-Xmn 为新生代大小。也可以用-XX:NewRatio=2来分配New/Old的比例。 2为默认值。

新生代又划分Eden和2*survivor。
-XX:survivorRatio=8来规定Eden/survivor的比例。8为默认值。

实例首先尝试分配在新生代eden上。如果实例所占内存超出限制XX:PretenureSizeThreshold,则直接分配到老生代。新生代分配内存时,如果eden空间不足,则触发新生代的GC, minor GC。老生代分配内存时,如果空间不足,则触发老生代的GC, full GC。老生代的GC比新生代GC要慢很多倍。老生代GC频率也比新生代低。

新生代minor GC时,如果存活的空间survivor容纳不下,则需要用老生代空间担保。如果老生代也容纳不下,就需要老生代做一次Full GC。 JVM会计算以往minor GC时新生代晋升老生代的平均大小,如果平均大小大于老生代剩余空间,则有很大可能发生担保失败,所以在minor GC前,先触发一次Full GC. 如果小于,则查看是否允许担保失败,如果允许,就直接进行minor GC。不允许,则先Full GC,再minor GC。

-XX:+HandlePromotionFailure 是允许老生代担保失败的发生。每次minor GC前,都会检查老生代剩余空间。JVM会计算以往minor GC时新生代晋升老生代的平均大小。此参数用于当平均大小小于老生代剩余空间时。以往小于不代表这次也小于,风险还是存在。如果允许担保失败,则直接进行minor GC, minor GC失败后,再Full GC. 如果不允许担保失败,则必须先运行Full GC,再minor GC。

这样子新生代GC时,如果survivor无法容纳存活实例,则将部分复制到老生代担保中。

垃圾回收器

Serial : 新生代收集器,使用copy算法。特点是单线程,当GC运行时,stop the world,所有的用户线程都必须暂停,等GC完成。缺点是停顿时间太长。

ParNew: 新生代收集器,使用copy,特点是多线程,也是停止用户线程,GC以后再运行用户线程。

Parallel: 新生代收集器,使用copy,但它注重JVM的吞吐量(JVM运行用户线程时间/GC线程时间),在提高吞吐量上有非常好的性能。它提供了很多可控参数,帮助调节JVM的吞吐量和停顿时间。

Serial Old: 老生代收集器,使用标记整理算法,特点跟serial 一样。

Parallel Old:老生代收集器,使用标记整理,产生于JDK 1.6,和Parallel搭配。在之前,Parallel只能和Serial old搭配,总体性能不是特别理想,现在双P。

CMS(Concurrent Mark Sweep):老生代收集器,使用标记清除。它的特点是可以和用户线程并发,可以减小停顿时间,但牺牲了吞吐量。CMS不能和Parallel搭配,一半和ParNew搭配。CMS的并发特性导致它3大缺点,1)对CPU要求高,2)无法清除因并发产生的浮动垃圾,3)容易产生碎片,为了清理碎片,每几次CMS之后,可以使用参数控制是否进行空间压缩。由于CMS是并发的,CMS需要预留空间给用户线程,一半老生代空间达到68%,就启动CMS. CMS过程中一旦程序失败,则发生CMS error。然后将使用Serial old作为它的备选,重新收集。CMS三个参数:

  1. XX:CMSInitiatingOccupancyFaction  设置老年代空间使用多少后出发CMS垃圾收集。一般是68%
  2. XX:UseCMSCompactAtFullCollection 每次CMS Full GC以后,做一次碎片整理。
  3. XX:CMSFullGCsBeforeCompaction 规定在多少次CMS Full GC以后,做一次碎片整理。

G1,传说中的新收集器,可以指定停顿时间,更高的效率。

Serial 和 serial old适合Client端的JVM。
Server端常用的是Parallel + Parallel Old, ParNew + CMS.
Parallel + Parallel Old 的吞吐量高
ParNew + CMS可减小停顿时间。

使用哪一个收集器,可以在JVM启动时用VM参数来指定。具体参考官方JVM options。

 完。

 

你可能感兴趣的:(jvm,GC)