知识屋:更实用的电脑技术知识网站
所在位置:首页 > 科技

jvm的内存分布,参数配置 和 GC处理机制

发表时间:2022-03-25来源:网络

   3.垃圾回收:   

 JVM中会在如下状况触发回收:对象没有被引用 , 做用域发生未捕捉异常 , 程序正常执行完毕 , 程序执行了System.exit() , 程序发生意外终止。  JVM中标记垃圾使用的算法是一种根搜索算法。简单的说,就是从一个叫GC Roots的对象开始 , 向下搜索 , 若是一个对象不能达到GC Roots对象的时候 , 说明它能够被回收了。这种算法比一种叫作引用计数法的垃圾标记算法要好,由于它避免了当两个对象啊互相引用时没法被回收的现象。

JVM中对于被标记为垃圾的对象进行回收时又分为了一下3种算法:

     1.标记清除算法 ,该算法是从根集合扫描整个空间,标记存活的对象,而后在扫描整个空间对没有被标记的对象进行回收,这种算法在存活对象较多时比较高效,但会产生内存碎片。

     2.复制算法 ,该算法是从根集合扫描,并将存活的对象复制到新的空间,这种算法在存活对象少时比较高效。

     3.标记整理算法 ,标记整理算法和标记清除算法同样都会扫描并标记存活对象,在回收未标记对象的同时会整理被标记的对象,解决了内存碎片的问题。

     JVM中,不一样的 内存区域做用和性质不同,使用的垃圾回收算法也不同,因此JVM中又定义了几种不一样的垃圾回收器(上图中连线表明两个回收器能够同时使用)

 

按照基本回收策略分

引用计数(Reference Counting):

比较古老的回收算法。原理是此对象有一个引用,即增长一个计数,删除一个引用则减小一个计数。垃圾回收时,只用收集计数为0的对象。此算法最致命的是没法处理循环引用的问题。 

标记-清除(Mark-Sweep): 

 

 

此算法执行分两阶段。第一阶段从引用根节点开始标记全部被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。此算法须要暂停整个应用,同时,会产生内存碎片。 

复制(Copying): 

 

此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另一个区域中。算法每次只处理正在使用中的对象,所以复制成本比较小,同时复制过去之后还能进行相应的内存整理,不会出现“碎片”问题。固然,此算法的缺点也是很明显的,就是须要两倍内存空间。

标记-整理(Mark-Compact):

 

 此算法结合了“标记-清除”和“复制”两个算法的优势。也是分两阶段,第一阶段从根节点开始标记全部被引用对象,第二阶段遍历整个堆,把清除未标记对象而且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。

 

按分区对待的方式分

增量收集(Incremental Collecting):实时垃圾回收算法,即:在应用进行的同时进行垃圾回收。不知道什么缘由JDK5.0中的收集器没有使用这种算法的。 

分代收集(Generational Collecting):基于对对象生命周期分析后得出的垃圾回收算法。把对象分为年青代、年老代、持久代,对不一样生命周期的对象使用不一样的算法(上述方式中的一个)进行回收。如今的垃圾回收器(从J2SE1.2开始)都是使用此算法的。 

 

按系统线程分

串行收集:串行收集使用单线程处理全部垃圾回收工做,由于无需多线程交互,实现容易,并且效率比较高。可是,其局限性也比较明显,即没法使用多处理器的优点,因此此收集适合单处理器机器。固然,此收集器也能够用在小数据量(100M左右)状况下的多处理器机器上。 

并行收集:并行收集使用多线程处理垃圾回收工做,于是速度快,效率高。并且理论上CPU数目越多,越能体现出并行收集器的优点。(串型收集的并发版本,须要暂停jvm) 并行paralise指的是多个任务在多个cpu中一块儿并行执行,最后将结果合并。效率是N倍。 

并发收集:相对于串行收集和并行收集而言,前面两个在进行垃圾回收工做时,须要暂停整个运行环境,而只有垃圾回收程序在运行,所以,系统在垃圾回收时会有明显的暂停,并且暂停时间会由于堆越大而越长。(和并行收集不一样,并发只有在开头和结尾会暂停jvm)并发concurrent指的是多个任务在一个cpu伪同步执行,但实际上是串行调度的,效率并不是直接是N倍。

 

分代垃圾回收

    分代的垃圾回收策略,是基于这样一个事实:不一样的对象的生命周期是不同的。所以,不一样生命周期的对象能够采起不一样的收集方式,以便提升回收效率。 

    在Java程序运行的过程当中,会产生大量的对象,其中有些对象是与业务信息相关,好比Http请求中的Session对象、线程、Socket链接,这类对象跟业务直接挂钩,所以生命周期比较长。可是还有一些对象,主要是程序运行过程当中生成的临时变量,这些对象生命周期会比较短,好比:String对象,因为其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次便可回收。 

    试想,在不进行对象存活时间区分的状况下,每次垃圾回收都是对整个堆空间进行回收,花费时间相对会长,同时,由于每次回收都须要遍历全部存活对象,但实际上,对于生命周期长的对象而言,这种遍历是没有效果的,由于可能进行了不少次遍历,可是他们依旧存在。所以,分代垃圾回收采用分治的思想,进行代的划分,把不一样生命周期的对象放在不一样代上,不一样代上采用最适合它的垃圾回收方式进行回收。 

 如图所示: 

    虚拟机中的共划分为三个代:年轻代(Young Generation)、年老点(Old Generation)和持久代(Permanent Generation)。其中持久代主要存放的是Java类的类信息,与垃圾收集要收集的Java对象关系不大。年轻代和年老代的划分是对垃圾收集影响比较大的。 

年轻代:

    全部新生成的对象首先都是放在年轻代的。年轻代的目标就是尽量快速的收集掉那些生命周期短的对象。年轻代分三个区。一个Eden区,两个Survivor区(通常而言)。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区满时,此区的存活对象将被复制到另一个Survivor区,当这个Survivor区也满了的时候,从第一个Survivor区复制过来的而且此时还存活的对象,将被复制“年老区(Tenured)”。须要注意,Survivor的两个区是对称的,没前后关系,因此同一个区中可能同时存在从Eden复制过来 对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor去过来的对象。并且,Survivor区总有一个是空的。同时,根据程序须要,Survivor区是能够配置为多个的(多于两个),这样能够增长对象在年轻代中的存在时间,减小被放到年老代的可能。 

年老代:

    在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。所以,能够认为年老代中存放的都是一些生命周期较长的对象。 

持久代:

    用于存放静态文件,现在Java类、方法等。持久代对垃圾回收没有显著影响,可是有些应用可能动态生成或者调用一些class,例如Hibernate等,在这种时候须要设置一个比较大的持久代空间来存放这些运行过程当中新增的类。持久代大小经过-XX:MaxPermSize=进行设置。 

 

什么状况下触发垃圾回收 

因为对象进行了分代处理,所以垃圾回收区域、时间也不同。GC有两种类型:Scavenge GCFull GC。 

Scavenge GC

    通常状况下,当新对象生成,而且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,而且把尚且存活的对象移动到Survivor区。而后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。由于大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,因此Eden区的GC会频繁进行。于是,通常在这里须要使用速度快、效率高的算法,使Eden去能尽快空闲出来。 

Full GC

    对整个堆进行整理,包括Young、Tenured和Perm。Full GC由于须要对整个对进行回收,因此比Scavenge GC要慢,所以应该尽量减小Full GC的次数。在对JVM调优的过程当中,很大一部分工做就是对于FullGC的调节。有以下缘由可能致使Full GC:

· 年老代(Tenured)被写满

· 持久代(Perm)被写满 

· System.gc()被显示调用 

·上一次GC以后Heap的各域分配策略动态变化

 图1 | --> 图2 | --> 图3 | --> 图4 | --> 

图1 --> 图2 --> 图3 --> 图4 -->  

 

1.Serial GC 。从名字上看,串行GC意味着是一种单线程的,因此它要求收集的时候全部的线程暂停。这对于高性能的应用是不合理的,因此串行GC通常用于Client模式的JVM中。2.ParNew GC 。是在SerialGC的基础上,增长了多线程机制。可是若是机器是单CPU的,这种收集器是比SerialGC效率低的。

3.Parrallel Scavenge GC 。这种收集器又叫吞吐量优先收集器,而吞吐量=程序运行时间/(JVM执行回收的时间+程序运行时间),假设程序运行了100分钟,JVM的垃圾回收占用 1分钟,那么吞吐量就是99%。Parallel Scavenge GC因为能够提供比较不错的吞吐量,因此被做为了server模式JVM的默认配置。

4.ParallelOld 是老生代并行收集器的一种,使用了标记整理算法,是JDK1.6中引进的,在以前 老生代 只能使用串行回收收集器。

5.Serial Old 是老生代client模式下的默认收集器,单线程执行,同时也做为CMS收集器失败后的备用收集器。

 6.CMS 又称响应时间优先回收器,使用标记清除算法。他的回收线程数为(CPU核心数+3)/4,因此当CPU核心数为2时比较高效些。CMS分为4个过程:初始标记、并发标记、从新标记、并发清除。

7.GarbageFirst(G1) 。比较特殊的是G1回收器既能够回收Young Generation,也能够回收Tenured Generation。它是在JDK6的某个版本中才引入的,性能比较高,同时注意了吞吐量和响应时间。

对于垃圾收集器的组合使用能够经过下表中的参数指定:

默认的GC种类能够经过jvm.cfg或者经过jmap dump出heap来查看,通常咱们经过jstat -gcutil [pid] 1000能够查看每秒gc的大致状况,或者能够在启动参数中加入:-verbose:gc -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:./gc.log来记录GC日志。

 

GC中有一种状况叫作Full GC,如下几种状况会触发Full GC:

1.Tenured Space空间不足以建立打的对象或者数组,会执行FullGC,而且当FullGC以后空间若是还不够,那么会OOM:java heap space。

2.Permanet Generation的大小不足,存放了太多的类信息,在非CMS状况下回触发FullGC。若是以后空间还不够,会OOM:PermGen space。

3.CMS GC时出现promotion failed和concurrent mode failure时,也会触发FullGC。

   promotion failed是在进行Minor GC时,survivor space放不下、对象只能放入旧生代,而此时旧生代也放不下形成的;

   concurrent mode failure是在执行CMS GC的过程当中同时有对象要放入旧生代,而此时旧生代空间不足形成的。

4.判断MinorGC后,要晋升到TenuredSpace的对象大小大于TenuredSpace的大小,也会触发FullGC。能够看出,当FullGC频繁发生时,必定是内存出问题了。

young generation有eden、2个survivor 区域组成。其中一个survivor区域一直是空的,是eden区域和另外一个survivor区域在下一次copy collection后活着的objecy的目的地。object在survivo区域被复制直到转移到tenured区。

咱们要尽可能减小 Full gc 的次数(tenured generation 通常比较大,收集的时间较长,频繁的Full gc会致使应用的性能收到严重的影响)。

堆内存GC
       JVM(采用分代回收的策略),用较高的频率对年轻的对象(young generation)进行YGC,而对老对象(tenured generation)较少(tenured generation 满了后才进行)进行Full GC。这样就不须要每次GC都将内存中全部对象都检查一遍。

非堆内存不GC

      GC不会在主程序运行期对PermGen Space进行清理,因此若是你的应用中有不少CLASS(特别是动态生成类,固然permgen space存放的内容不只限于类)的话,就极可能出现PermGen Space错误。

内存申请、对象衰老过程
1、内存申请过程

JVM会试图为相关Java对象在Eden中初始化一块内存区域; 当Eden空间足够时,内存申请结束。不然到下一步; JVM试图释放在Eden中全部不活跃的对象(minor collection),释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区; Survivor区被用来做为Eden及old的中间交换区域,当OLD区空间足够时,Survivor区的对象会被移到Old区,不然会被保留在Survivor区; 当old区空间不够时,JVM会在old区进行major collection; 彻底垃圾收集后,若Survivor及old区仍然没法存放从Eden复制过来的部分对象,致使JVM没法在Eden区为新对象建立内存区域,则出现"Out of memory错误";

 

GC性能方面的考虑

对于GC的性能主要有2个方面的指标:吞吐量throughput(工做时间不算gc的时间占总的时间比)和暂停pause(gc发生时app对外显示的没法响应)。

1. Total Heap

默认状况下,vm会增长/减小heap大小以维持free space在整个vm中占的比例,这个比例由MinHeapFreeRatio和MaxHeapFreeRatio指定。

通常而言,server端的app会有如下规则:

对vm分配尽量多的memory; 将Xms和Xmx设为同样的值。若是虚拟机启动时设置使用的内存比较小,这个时候又须要初始化不少对象,虚拟机就必须重复地增长内存。 处理器核数增长,内存也跟着增大。

2. The Young Generation

另一个对于app流畅性运行影响的因素是young generation的大小。young generation越大,minor collection越少;可是在固定heap size状况下,更大的young generation就意味着小的tenured generation,就意味着更多的major collection(major collection会引起minor collection)。

NewRatio反映的是young和tenured generation的大小比例。NewSize和MaxNewSize反映的是young generation大小的下限和上限,将这两个值设为同样就固定了young generation的大小(同Xms和Xmx设为同样)。

 

若是但愿,SurvivorRatio也能够优化survivor的大小,不过这对于性能的影响不是很大。SurvivorRatio是eden和survior大小比例。

通常而言,server端的app会有如下规则:

首先决定能分配给vm的最大的heap size,而后设定最佳的young generation的大小; 若是heap size固定后,增长young generation的大小意味着减少tenured generation大小。让tenured generation在任什么时候候够大,可以容纳全部live的data(留10%-20%的空余)。

经验&&规则

年轻代大小选择 响应时间优先的应用:尽量设大,直到接近系统的最低响应时间限制(根据实际状况选择).在此种状况下,年轻代收集发生的频率也是最小的.同时,减小到达年老代的对象. 吞吐量优先的应用:尽量的设置大,可能到达Gbit的程度.由于对响应时间没有要求,垃圾收集能够并行进行,通常适合8CPU以上的应用. 避免设置太小.当新生代设置太小时会致使:1.YGC次数更加频繁 2.可能致使YGC对象直接进入旧生代,若是此时旧生代满了,会触发FGC. 年老代大小选择 响应时间优先的应用:年老代使用并发收集器,因此其大小须要当心设置,通常要考虑并发会话率和会话持续时间等一些参数.若是堆设置小了,能够会形成内存碎 片,高回收频率以及应用暂停而使用传统的标记清除方式;若是堆大了,则须要较长的收集时间.最优化的方案,通常须要参考如下数据得到:
并发垃圾收集信息、持久代并发收集次数、传统GC信息、花在年轻代和年老代回收上的时间比例。 吞吐量优先的应用:通常吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代.缘由是,这样能够尽量回收掉大部分短时间对象,减小中期的对象,而年老代尽存放长期存活对象. 较小堆引发的碎片问题
由于年老代的并发收集器使用标记,清除算法,因此不会对堆进行压缩.当收集器回收时,他会把相邻的空间进行合并,这样能够分配给较大的对象.可是,当堆空间较小时,运行一段时间之后,就会出现"碎片",若是并发收集器找不到足够的空间,那么并发收集器将会中止,而后使用传统的标记,清除方式进行回收.若是出现"碎片",可能须要进行以下配置:
-XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩.
-XX:CMSFullGCsBeforeCompaction=0:上面配置开启的状况下,这里设置多少次Full GC后,对年老代进行压缩 用64位操做系统,Linux下64位的jdk比32位jdk要慢一些,可是吃得内存更多,吞吐量更大 XMX和XMS设置同样大,MaxPermSize和MinPermSize设置同样大,这样能够减轻伸缩堆大小带来的压力 使用CMS的好处是用尽可能少的新生代,经验值是128M-256M, 而后老生代利用CMS并行收集, 这样能保证系统低延迟的吞吐效率。 实际上cms的收集停顿时间很是的短,2G的内存, 大约20-80ms的应用程序停顿时间 系统停顿的时候多是GC的问题也多是程序的问题,多用jmap和jstack查看,或者killall -3 java,而后查看java控制台日志,能看出不少问题。(相关工具的使用方法将在后面的blog中介绍) 仔细了解本身的应用,若是用了缓存,那么年老代应该大一些,缓存的HashMap不该该无限制长,建议采用LRU算法的Map作缓存,LRUMap的最大长度也要根据实际状况设定。 采用并发回收时,年轻代小一点,年老代要大,由于年老大用的是并发回收,即便时间长点也不会影响其余程序继续运行,网站不会停顿 JVM参数的设置(特别是 –Xmx –Xms –Xmn -XX:SurvivorRatio  -XX:MaxTenuringThreshold等参数的设置没有一个固定的公式,须要根据PV old区实际数据 YGC次数等多方面来衡量。为了不promotion faild可能会致使xmn设置偏小,也意味着YGC的次数会增多,处理并发访问的能力降低等问题。每一个参数的调整都须要通过详细的性能测试,才能找到特定应用的最佳配置。

promotion failed:

垃圾回收时promotion failed是个很头痛的问题,通常多是两种缘由产生,第一个缘由是救助空间不够,救助空间里的对象还不该该被移动到年老代,但年轻代又有不少对象须要放入救助空间;第二个缘由是年老代没有足够的空间接纳来自年轻代的对象;这两种状况都会转向Full GC,网站停顿时间较长。

解决方方案一:

第一个缘由个人最终解决办法是去掉救助空间,设置-XX:SurvivorRatio=65536 -XX:MaxTenuringThreshold=0便可,第二个缘由个人解决办法是设置CMSInitiatingOccupancyFraction为某个值(假设70),这样年老代空间到70%时就开始执行CMS,年老代有足够的空间接纳来自年轻代的对象。

解决方案一的改进方案:

又有改进了,上面方法不太好,由于没有用到救助空间,因此年老代容易满,CMS执行会比较频繁。我改善了一下,仍是用救助空间,可是把救助空间加大,这样也不会有promotion failed。具体操做上,32位Linux和64位Linux好像不同,64位系统彷佛只要配置MaxTenuringThreshold参数,CMS仍是有暂停。为了解决暂停问题和promotion failed问题,最后我设置-XX:SurvivorRatio=1 ,并把MaxTenuringThreshold去掉,这样即没有暂停又不会有promotoin failed,并且更重要的是,年老代和永久代上升很是慢(由于好多对象到不了年老代就被回收了),因此CMS执行频率很是低,好几个小时才执行一次,这样,服务器都不用重启了。

 

JVM参数的含义 实例见实例分析

参数名称 含义 默认值   -Xms 初始堆大小 物理内存的1/64(
收藏
  • 人气文章
  • 最新文章
  • 下载排行榜
  • 热门排行榜