jvm垃圾收集器与内存分配策略

一、垃圾回收

  1、对象是否已经变为垃圾

    1.1、引用计数法:给对象添加一个引用计数器,每当有地方引用它时,计数器就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。

            这个方法有个很大的缺陷,无法解决循环引用的问题。所以主流的实现中,基本没有使用。

    1.2、可达性分析法:通过一系列被称为GC Roots的对象作为起点,从这些节点向下搜索,搜索走过的路径被称为引用链,当一个对象不再在任意引用链上时(不可达),则证明这个对象是不可用的。

      1.2.1、GC Roots对象包括下面几种

          虚拟机栈中引用的对象。

          方法区中静态属性应用的对象。

          方法区中常量引用的对象。

          Native方法引用的对象。

    1.3、在上面两种方法中对象是否不可用关键在于该对象是否被引用。在JDK1.2之后,java对引用的概念进行的扩充。将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)

      强引用:类似Object o = new Object(); 只要强引用还在,垃圾回收器永远不会回收该对象。

      软引用:用来描述一些还有用但非必须的对象。对于软银用关联的对象,在系统将要内存溢出之前,会将这些对象列入回收范围之中,进行第二次回收。如果这次回收还没有足够的内存,才会抛出异常。JDK1.2之后,提供了SoftRefernce类来实现弱引用。

      弱引用:用来描述非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。

          JDK1.2之后,提供了WeakRefernce类来实现弱引用。

      虚引用:它是最弱的一种引用关系,一个对象是否有虚引用存在,完全不会影响其生存时间,也无法通过虚引用来获取对象的实例。为一个对象设置虚引用的目的是能在这个对象被回收的时候收到一个系统通知。

          JDK1.2之后,提供了PhantomReference类来实现弱引用。

    1.4、当一个对象不可达时,需要经历两次标记才能判断该对象是否需要被回收。

      第一次标记后对对象进行筛选,判断对象是否需要执行finalize()方法。如果该对象没有覆盖finalize()方法,或者该对象已经执行过finalize()方法,则该对象无需执行finalize()方法。

      如果在对象在finalize()方法中对该对象添加了新的引用,与引用链上的对象关联起来,则该对象在第二次标记的时候会被移出即将回收的集合,将不会被收集。

      如果如果两次标记后回收被标记两次的对象。

    

    1.5、方法区垃圾回收:该区域的垃圾回收效率非常低,该区域主要是收集废弃的常量和无用的类。

      1.5.1、废弃的常量:如果当前系统中没有任何地方引用常量池中的常量,如果这时发生内存回收,如果有必要的话,该常量会被清楚出常量池。

      1.5.2、无用的类:判断一个类是否可卸载需要同时满足下面三个条件就说明该类可以被回收:

        该类的所有实例都已经被回收,java堆中不存在该类的任何实例;

        加载该类的ClassLoader已经被回收;

        该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

  

  2、垃圾清除的算法

    2.1、标记-清除算法:这个方法是最基础的收集算法,该算法分为“标记”和“清除”两个阶段:首先标记出需要回收的对象,在标记完成后统一回收被标记的对象。之所以说它是最基础的算法是因为后续的收集算法都是基于这种算法的不足进行改进的。

      标记-清除算法主要有两个不足:一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清除后会产生大量不连续的内存碎片,内存碎片较多可能会导致在后续的运行中创建较大对象时,无法找到足够的连续内存而触发另一次垃圾回收。

    jvm垃圾收集器与内存分配策略

      

    2.2、复制算法:为了解决效率问题,一种称为“复制”的收集算法出现了,它将可用内存按容量大小划分为两块,每次只使用其中一块。当一块内存用完了,就将还存活的对象复制到另外一块,然后将以使用过的内存空间清理掉。这种算法的代价是将内存缩小为原来的一半。

  jvm垃圾收集器与内存分配策略

    2.3、标记-整理算法:复制算法在对象存活率较高时,就需要进行较多的复制操作,效率将会降低。如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况。

      标记-整理算法的标记阶段和标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。

    jvm垃圾收集器与内存分配策略

    2.4、分代收集算法:当前商业虚拟机的垃圾收集都采用“分代收集”算法,这种算法根据对象存活周期的不同划分为几块,一般把java堆分为新生代和老年代,这样就可以根据各个年代的特点采用不同的收集算法。在新生代采用复制算法,老年代采用“标记-清理”或者“标记-整理”算法。

      分代算法将新生代分为三个部分,Eden区和两个Survivor区(Survivor0和Survivor1,也叫From Space 和To Space),默认比例是8:1:1。

  jvm垃圾收集器与内存分配策略

    对象首先都放在Eden Space 如果Eden Space 内存空间不够了,就将存活的对象复制到Survivor0区,然后清空Eden区。这被称为Minor GC也叫YoungGC。

    如果Survivor0区也存放满了,则将Eden Space和Survivor0区存活的对象复制到Survivor1区,然后清空Eden区和Survivor0区。此时Survivor1区和Survivor0区身份对调。

    当Survivor1区的内存空间无法存放来自Eden Space和Survivor0区的对象时,Survivor0中的对象就会被放入老年代。

    当老年代的内存空间使用达到某个阈值时(不一定是老年代内存全部使用完)就会出发Full GC。     

  3、垃圾收集器

    jvm垃圾收集器与内存分配策略

    上图是基于JDK1.7 update 14之后的HotSopt虚拟机中的垃圾收集器。如果两个收集器之间存在连线,就说明他们可以搭配使用。

    3.1、Serial 收集器(串行收集器会STW)

        Serial 收集器是最基本的、发展历史最悠久的垃圾收集器。该收集器是一个单线程的收集器。它只会使用一条垃圾回收线程来收集垃圾,并且在收集的时候会暂停其他所有的工作线程(Stop The world)。

        Serial 收集器采用的是复制算法

      jvm垃圾收集器与内存分配策略

    3.2、ParNew 收集器(并行多线程收集器)

        该收集器是Serial 收集器的多线程版本,可以同时用多个线程来收集垃圾。在单CPU环境中,这个垃圾回收器相比Serial 收集器没有优势,但是在多核环境中,ParNew就有很大的优势。

        ParNew 收集器采用的是复制算法

        jvm垃圾收集器与内存分配策略

    3.3、Parallel Scavenge 收集器(并行多线程收集器)

      Parallel Scavenge 收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。该收集器的目的是达到一个可控制的吞吐量,即代码运行时间/(运行用户代码时间+垃圾收集时间)。

      停顿的时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则高效的利用了CPU的时间,尽快的完成程序的运算任务,主要适合后台运算而不需要太多的交互的任务。

      -XX:MaxGCMillis   : 控制最大垃圾收集停顿时间,值为大于0 的毫秒数。

      -XX:GCTimeRatio : 设置吞吐量大小,值为大于0小于100的整数,默认值为99 就是允许最大1%的垃圾收集时间。

      -XX:+UseAdaptiveSizePolicy  : 这是一个开关参数,当这个参数打开后,就不需要手动指定新生代的大小、Eden和Survivor的比例和晋升老年代年龄等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态的调整这些参数以提供最适合的停顿时间或者最大的吞吐量,这种调节方式被称为GC自适应的调节策略。

    3.4、Serial Old收集器(串行收集器)

      Serial Old 收集器是Serial 收集器的老年版本,它同样是一个单线程收集器,使用“标记-整理算法”。

    jvm垃圾收集器与内存分配策略

    3.5、Parallel Old 收集器(并行多线程收集器)

      Parallel Old 收集器是Parallel 收集器的老年代版本,使用多线程和“标记整理算法”。在JDK1.6才开始提供。

    jvm垃圾收集器与内存分配策略

    3.6、CMS收集器(并发多线程收集器)

      CMS(Concurrent Mark Sweep) 收集器是一种以获取最短回收停顿时间为目标的收集器。采用的“标记-清除“算法,它的运作过程相对于前面几种收集器来说更复杂一些,整个过程分为四个步骤:初始标记、并发标记、重新标记、并发清除

      其中初始标记和重新标记这两个步骤仍需要Stop The Word。

      初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快;

      并发标记是进行GC Roots tracing的过程;

      重新标记是为了修正并发标记期间因用户程序继续运作而导致标记产生的变动的那一部分对象的标记记录;

      并发标记和并发清除过程垃圾收集线程是和工作线程一起运作,所以总体来说CMS收集器内存回收过程是与用户线程一起并发执行的。

    jvm垃圾收集器与内存分配策略

     CMS的缺点:

        ①对CPU资源敏感,默认启动的线程数为(CPU数量+3)/4;

        ②CMS无法处理浮动垃圾,因为是一边收集垃圾一边运行程序所以产生了浮动垃圾无法清除,同时需要预留内存空间给用户线程使用,所以在老年代没有满的时候就得开始垃圾回收。JDK1.5默认是老年代使用了68%开始回收,JDK1.6是92%。

          如果没有足够的内存满足用户线程需要,就会出现“Concurrent Mode Failure”失败,这时将启动Serial Old 收集器来进行垃圾回收。通过-XX:CMSInitiatingOccupancyFraction的值来提高触发百分比。

        ③由于采用“标记-清除”算法,所以垃圾回收后会有内存碎片。

  3.7、G1 收集器(并发多线程收集器)

    G1是一款面向服务端应用的垃圾收集器,并且不需要与其他收集器配合就能独立管理整个GC堆。

    G1的新生代收集跟ParNew类似,当新生代占用达到一定比例的时候,开始出发收集。和CMS类似,G1收集器收集老年代对象会有短暂停顿。

    G1收集器有如下特点:

      并发和并行:G1能充分利用多核多CPU的运行环境,使用多个CPU来缩短“Stop-The-word”的停顿时间,并且在垃圾清理阶段回收线程和java程序线程能够同时运行。

      分代收集:使用G1收集器时,Java堆的内存布局有了很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。

      空间整合:与CMS的“标记-清除”算法不同,G1整体来看是基于“标记-整理”算法,从局部(两个Region之间)来看是基于复制算法,所以G1收集器不会产生内存碎片。

      可预测停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。

    

    收集步骤:
      ①初始标记(Initial-Mark):这个阶段会(Stop the World Event),但是时间停顿时间很短,该阶段仅仅是标记一下GC Roots能直接关联到的对象。

      ②并发标记(Concurrent Marking):开始从GC Root对堆中的对象进行可达性分析,找出存活的对象,这个阶段耗时很长,但可与用户程序并发执行。

      ③最终标记(Final Marking): 会有短暂停顿(STW),该阶段是用来收集 并发标记阶段 产生新的垃圾(并发阶段和应用程序一同运行);

      ④筛选回收(Live Data Counting and Evacuation):这个阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。

  4、理解GC 日志

jvm垃圾收集器与内存分配策略

jvm垃圾收集器与内存分配策略

jvm垃圾收集器与内存分配策略

jvm垃圾收集器与内存分配策略

  5.垃圾收集器参数总结

     “+” 号的意思是ture,开启,反之,如果是 “-”号,则是关闭。

    -XX:+UseSerialGC : jvm运行在Client模式下的默认值,打开此开关后,使用Serial + Serial Old的收集器组合进行内存回收

    -XX:+UseParNewGC : 打开此开关后,使用ParNew + Serial Old的收集器进行垃圾回收 

    -XX:+UseConcMarkSweepGC : 使用ParNew + CMS +  Serial Old的收集器组合进行内存回收,Serial Old作为CMS出现“Concurrent Mode Failure”失败后的后备收集器使用

    -XX:+UseParallelGC : Jvm运行在Server模式下的默认值,打开此开关后,使用Parallel Scavenge +  Serial Old的收集器组合进行回收

    -XX:+UseParallelOldGC : 使用Parallel Scavenge +  Parallel Old的收集器组合进行回收

    –XX:+UseG1GC : 打开此开关后,使用G1垃圾收集器

    -XX:PretenureSizeThreshold : 直接晋升到老年代对象的大小,设置这个参数后,大于这个参数的对象将直接在老年代分配

    -XX:MaxTenuringThreshold : 晋升到老年代的对象年龄,每次Minor GC之后,年龄就加1,当超过这个参数的值时进入老年代,默认是15

    -XX:UseAdaptiveSizePolicy : 动态调整java堆中各个区域的大小以及进入老年代的年龄

    -XX:+HandlePromotionFailure : 是否允许新生代收集担保,进行一次minor gc后, 另一块Survivor空间不足时,将直接会在老年代中保留

    -XX:ParallelGCThreads : 设置并行GC进行内存回收的线程数,一般最好和 CPU 核心数量相当。

               默认情况下,当 CPU 数量小于8, ParallelGCThreads 的值等于 CPU 数量,当 CPU 数量大于 8 时,则使用公式:3+((5*CPU)/ 8);

               同时这个参数只要是并行 GC 都可以使用,不只是 ParNew。

    -XX:GCTimeRatio : GC时间占总时间的比列,默认值为99,即允许1%的GC时间,仅在使用Parallel Scavenge 收集器时有效

    -XX:MaxGCPauseMillis : 设置GC的最大停顿时间,在Parallel Scavenge 收集器下有效

    -XX:CMSInitiatingOccupancyFraction : 设置CMS收集器在老年代空间被使用多少后出发垃圾收集,默认值为68%,仅在CMS收集器时有效,-XX:CMSInitiatingOccupancyFraction=70

    -XX:+UseCMSCompactAtFullCollection : 由于CMS收集器会产生碎片,此参数设置在垃圾收集器后是否需要一次内存碎片整理过程,仅在CMS收集器时有效

    -XX:+CMSFullGCBeforeCompaction : 设置CMS收集器在进行若干次垃圾收集后再进行一次内存碎片整理过程,通常与UseCMSCompactAtFullCollection参数一起使用

    -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况

    

    -XX:+PrintGC : 打印GC日志

    -verbose:gc  : 打印 GC 日志

    -XX:+PrintGCDetails :  打印 GC 详情

    -XX:+PrintGCTimeStamps :  打印此次垃圾回收距离jvm开始运行的所耗时间。

    -XX:+PrintGCDateStamps : 打印 GC 日志时间戳。

    -Xloggc:filename : 将垃圾回收信息输出到指定文件

    -XX:+HeapDumpOnOutOfMemoryError :  内存溢出时输出 dump 文件。

    -Xms:初始堆大小;如-Xms20M

    -Xmx:最大堆大小;如-Xmx1g    

    -Xmn:新生代大小;(-Xmn 是将NewSize与MaxNewSize设为一致。256m),同下面两个参数-XX:NewSize=256m和-XX:MaxNewSize=256m

    -XX:NewSize=n:设置年轻代大小

    -XX:MaxNewSize=256m:设置年轻代最大大小

    -XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4

    -XX:SurvivorRatio : 新生代中Eden区域与Survivor区域的容量比值,默认为8,代表Eden:Survivor0:survivor1 = 8:1:1

    -XX:MaxPermSize=n:设置持久代最大大小

    -XX:PermSize=n :设置持久代初始大小

    -Xss1m  Stack(栈)内存大小设置

    -XX:MetaspaceSize=128m  设置元空间初始大小

    -XX:MaxMetaspaceSize=512m  设置元空间最大大小

    

二、内存分配和回收策略

   1、两种GC方式:

      新生代GC(Minor GC):发生在新生代的垃圾收集动作,因为对象大多具备朝生夕灭的特性,所以Minor GC非常频繁,速度也非常快。

      老年代GC(Major GC):发生在老年代的GC,在Major GC之前一般至少会伴随着至少一次Minor GC。Major GC 比Minor GC 慢十倍以上。

  

   2、对象的内存分配

      2.1、对象优先分配在Eden中

        在大多数情况下对象优先分配在新生代的Eden区中,当Eden区没有足够的空间时,虚拟机将会发起一次Minor GC。

      2.2、大对象直接进入老年代

        可以通过-XX:PretenureSizeThreshold 来设置大于这个值的对象直接在老年代分配,单位是byte。这个参数只对Serial和ParNew两个垃圾回收器有效。

      2.3、长期存活的对象将进入老年代 

        可以通过-XX:MaxTenuringThreshold 来设置对象经历了多少次Minor GC  就需要将该对象放入老年代中

      2.4、动态对象年龄判定

      为了适应不同程序的内存状况,虚拟机并不是永远的必须要求对象年龄达到-XX:MaxTenuringThreshold 设置的值才能晋升老年代。如果Survivor空间中相同年龄大小的对象总和大于Survivor空间的一半,年龄大于和等于该年龄的对象就可以直接进入老年代。

      2.5、空间分配担保

        在发生Minor GC之前,虚拟机会检查老年代的最大可用连续内存空间是否大于新生代所有对象总空间,如果这个条件成立,则Minor GC 能确保安全(防止出现新生代的对象都存活,老年代连续内存不够用的情况)。

        如果不成立,则虚拟机会查看-XX:+HandlePromotionFailure 设置的是否允许担保失败,如果允许担保失败则会继续检查老年代最大可用连续内存空间是否大于历次晋级到老年代的对象的平均总大小,

        如果大于则进行Minor GC,如果小于或者不允许担保失败,则会直接进行Full GC。

上一篇:在线帮助文档编辑器gitbook


下一篇:为什么Lisp语言如此先进?(译文) - 阮一峰的网络日志