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

1 运行时数据区域

  运行时数据区域主要包括:方法区、虚拟机栈、本地方法栈、堆、程序计数器

  程序计算器和Java虚拟机栈是线程私有的,生命周期与线程相同,程序计数器是唯一一个在java虚拟机规范中没有规定任务OutOfMemoryError的区域

  在java虚拟机规范中,对虚拟机栈规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展且扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。本地方法栈也会抛出这两种异常。

  java堆是所有线程共享的一大块内存区域,在虚拟机启动时创建。所有的对象实例和数组都要在堆上分配。

  java堆也叫GC堆,由于现在收集器都采用分代收集算法,所以java堆可以细分为:新生代和老年代;再细一点可以分为Eden、From Survivor、To Survivor空间。从内存分配的角度,线程共享的java堆中可能划分出多个线程私有的分配缓冲区TLAB。如果堆中没有内存完成实例分配,且无法扩展时,将会抛出OutOfMemoryError异常。(-Xmx 堆最大值和-Xms堆最小值)

  方法区也是各线程共享的内存区域。存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区习惯被称为永久代(-XX:MaxPermSize 最大方法区容量)。方法区的内存回收目标是针对常量池的回收和对类型的卸载。该区域也会抛出OutOfMemoryError异常。

  运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量和符号引用。

  直接内存虽然不是虚拟机运行时数据区的一部分,但这部分内存也被频繁使用,虽然不受堆大小限制,但也受限于整机内存。

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

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

  可达性分析算法:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。在java中,可作为GC Roots的对象包括下面几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象

3 再谈引用

  引用分为强引用、软引用、弱引用、虚引用四种。

  强引用,类似 Object obj = new Object()

  软引用,描述还有用但非必需的对象。对于软引用关联的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够内存,则会抛出内存溢出异常。提供了SoftReference类实现软引用。

  弱引用,描述非必需对象的,比软引用更弱,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象,提供了WeakReference类实现弱引用。

  虚引用,称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取的一个对象实例。为一个对象设置虚引用关联的唯一目的是能在这个对象被收集器回收时收到一个系统通知。提供了PhantomReference类实现虚引用。

3 生存还是死亡

  宣告一个对象死亡,至少经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots想连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize方法,或该方法已被虚拟机调用过,则视为没必要执行该方法。如果这个对象被判定为有必要执行finalize方法,那么这个对象将会放置在一个叫做F-Queue的队列制作,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它。对象只要在finalizer中成功拯救自己——重新与引用链上的任何一个对象建立关联即可,那在第二次标记时它将被移除出“即将回收”的集合,如果没有逃脱则会被回收。(任何一个对象的finalize方法都只会被系统自动调用一次)

 4 回收方法区(HotSpot虚拟机中的永久代)

  永久代的垃圾回收主要回收两部分内容:废弃常量、无用的类。无用的类:

  • 该类所有的实例都已经被回收
  • 加载该类的classloader已经被回收
  • 该类对应的java.lang.class对象没有在任何地方被引用,无法通过反射访问该类的方法

5 垃圾回收算法

  5.1 标记-清除算法(Mark-Sweep)

    不足:效率低,清除后产生大量的不连续内存碎片

  5.2 复制算法

    复制算法将内存划分为大小相等的两块,每次使用一块,用完了将存活的复制到另一块,然后清理用过的那一块。现在的商业虚拟机都采用这种算法回收新生代,将内存分为一块较大的Eden空间和两块较小的survivor空间,每次使用Eden和其中一块Survivor。回收时将Eden和Survivor存活的对象一次性复制到另一块Survivor上,清理掉Eden和用过的Survivor。Hotspot虚拟机默认Eden和Survivor大小比例是8:1。当Survivor空间不够时,需要依赖老年代进行分配担保。

  5.3 标记-整理算法

    针对老年代的特点,有人提出了另外一种标记-整理算法(Mark-Compact),标记后不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后清理掉边界外的内存。

  5.4 分代收集算法

    当前商业虚拟机的垃圾回收都采用分代收集算法(Generational Collection),该算法根据对象存活周期不同将内存分为几块,一般把java堆分为新生代和老年代。新生代采用复制算法,老年代采用标记-清理或者标记-整理算法。

 6 垃圾收集器

  hotspot 虚拟机的收集器如下:

  

  上面有7中收集器,分为两块,上面为新生代收集器,下面是老年代收集器。如果两个收集器之间存在连线,就说明它们可以搭配使用。

先解释下什么是垃圾收集器的上下文语境中的并行和并发:

  并行(Parallel):指多条垃圾收集器线程并行工作,但此时用户线程仍然处于等待。

  并发(Concurrent):指用户线程与垃圾收集器线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集器程序运行于另一个CPU之上。

  • Serial(串行GC)收集器

    Serial收集器是单线程执行,新生代使用复制算法。它在进行垃圾收集时,它不仅只会使用一个CPU或者一条收集线程去完成垃圾收集工作,而且必须暂停其他所有的工作线程(用户线程),直到它收集完成。

 
            
             
              Serial/Serial Old收集器运行示意图(表示Serial和Serial Old搭配使用)
    Serial收集器是Jvm client模式下默认的新生代收集器。对于限定单个CPU的环境来说,简单高效,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率,因此是运行在Client模式下的虚拟机的不错选择(比如桌面应用场景)。
  • ParNew收集器
  ParNew收集器其实就是serial收集器的多线程版本,使用复制算法。除了使用多条线程进行垃圾收集之外,其余行为与Serial收集器一样。
              
               ParNew/Serial Old收集器运行示意图(表示ParNew和Serial Old搭配使用)
  是运行在Service模式下虚拟机中首选的新生代收集器,其中一个与性能无关的原因就是除了Serial收集器外,目前只有ParNew收集器能与CMS收集器配合工作。
  PreNew收集器在单CPU环境中绝对没有Serial的效果好,由于存在线程交互的开销,该收集器在超线程技术实现的双CPU中都不能一定超过Serial收集器。默认开启的垃圾收集器线程数就是CPU数量,可通过-XX:parallelGCThreads参数来限制收集器线程数。
 
  • Parallel Scavenge 收集器

  parallel scavenge 收集器是一个新生代收集器,也是吞吐量优先收集器,它也是使用复制算法,也是并行的多线程收集器。它的关注点和其他收集器不同,这个收集器的目标是达到一个可控制的吞吐量。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis以及直接设置吞吐量大小的-XX:GCTimeRatio参数。它还有一个开关参数:-XX:+UseAdaptiveSizePolicy,打开后就不需要设置新生代大小,SurvivorRatio、老年代对象大小PretenureSizeThreshold等细节参数,实现GC自适应的调节策略。

  • Serial Old 收集器

  Serial Old收集器是一个老年代收集器,单线程执行,使用标记-整理算法。

  • Parallel Old 收集器

  Parallel Old收集器是Parallel Scavenge 的老年代收集器,多线程执行,使用标记-整理算法。在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge 加Parallel Old收集器。

  • CMS 收集器

  CMS收集器是一种以获取最短回收停顿时间为目标的收集器,使用标记-清除算法。整个过程分为4个步骤:

  • 初始标记  --需要stop the world
  • 并发标记
  • 重新标记 --需要stop the world
  • 并发清除
  由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以整体来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
      
                CMS收集器运行示意图
  • CMS收集器对CPU资源非常敏感。CMS默认启动的回收线程数是(CPU数量+3)/4,当cpu不足4个时,对用户程序的影响可能变得很大
  • CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。-XX:CMSInitiatingOccupancyFraction触发参数,JDK1.6中,该启动阈值已经提高到92%,要是CMS运行期间预留的内存无法满足程序需要,就会出现Concurrent Mode Failure失败,这时就会临时启用Serial OLd收集器来重新进行老年代的垃圾回收
  • 由于采用标记-清除算法,会有大量空间碎片产生,CMS提供了-XX:+UseCMSCompactAtFullCollection开关参数,默认开启,用于CMS收集时顶不住要进行Full Gc时开启内存碎片的合并整理过程。

 

  •  G1收集器   

      G1收集器是一款面向服务端应用的垃圾收集器。与其他GC收集器相比,有以下特点:

    1.并行与并发。部分其他收集器原本需要停顿java线程执行的GC动作,G1收集器能通过并发的方式让java程序继续执行。

    2.分代收集。不需要其他收集器配合就能独立管理整个堆。

    3.空间整合。G1从整体上看是基于标记-整理算法实现的收集器,从局部(两个Region)看是基于复制算法实现的。

               4.可预测的停顿。G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。

  G1时,java堆的内存布局与其他收集器有很大差别,它将整个java堆划分为多个大小相等的独立区域Region,虽然保留新生代、老年代概念,但他们不再是物理隔离的,都是一部分region的集合。G1有计划避免整个java堆中进行全区域的垃圾收集,G1跟踪各个Region里面的垃圾堆积的价值大小(空间时间经验值),在后台维护一个优先级列表,每次根据允许的回收时间,优先回收价值最大的region。在G1收集器中,Region之间的对象引用以及其他收集器中的新生代和老年代之间的对象引用,虚拟机都是使用Remembered Set来避免全堆扫描的。每个region有个对应的remembered set。G1的运作大致可划分为以下几个步骤:

   初始标记、并发标记、最终标记、筛选回收

如果现有的垃圾收集器没有出现任何问题,没有任何理由去选择G1,如果应用追求低停顿,G1可选择,如果追求吞吐量,和Parallel Scavenge/Parallel Old组合相比G1并没有特别的优势。

垃圾收集器参数总结

-XX:+<option> 启用选项
-XX:-<option> 不启用选项
-XX:<option>=<number> 
-XX:<option>=<string>
参数描述

-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:SurvivorRatio 新生代中Eden区域与Survivor区域的容量比值,默认为8,代表Eden:Subrvivor = 8:1
-XX:PretenureSizeThreshold 直接晋升到老年代对象的大小,设置这个参数后,大于这个参数的对象将直接在老年代分配
-XX:MaxTenuringThreshold 晋升到老年代的对象年龄,每次Minor GC之后,年龄就加1,当超过这个参数的值时进入老年代
-XX:UseAdaptiveSizePolicy 动态调整java堆中各个区域的大小以及进入老年代的年龄
-XX:+HandlePromotionFailure 是否允许新生代收集担保,进行一次minor gc后, 另一块Survivor空间不足时,将直接会在老年代中保留
-XX:ParallelGCThreads 设置并行GC进行内存回收的线程数
-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:+UseFastAccessorMethods
原始类型优化
-XX:+DisableExplicitGC
是否关闭手动System.gc
-XX:+CMSParallelRemarkEnabled
降低标记停顿
-XX:LargePageSizeInBytes
内存页的大小不可设置过大,会影响Perm的大小,-XX:LargePageSizeInBytes=128m

 

Client、Server模式默认GC

 新生代GC方式老年代和持久代GC方式

Client

Serial 串行GC Serial Old 串行GC
Server Parallel Scavenge  并行回收GC Parallel Old 并行GC

Sun/Oracle JDK GC组合方式

 新生代GC方式老年代和持久代GC方式

-XX:+UseSerialGC

Serial 串行GC Serial Old 串行GC
-XX:+UseParallelGC Parallel Scavenge  并行回收GC Serial Old  并行GC
-XX:+UseConcMarkSweepGC ParNew 并行GC CMS 并发GC 
当出现“Concurrent Mode Failure”时
采用Serial Old 串行GC
-XX:+UseParNewGC ParNew 并行GC Serial Old 串行GC
-XX:+UseParallelOldGC Parallel Scavenge  并行回收GC Parallel Old 并行GC
-XX:+UseConcMarkSweepGC
-XX:+UseParNewGC
Serial 串行GC CMS 并发GC 
当出现“Concurrent Mode Failure”时
采用Serial Old 串行GC

注:Minor GC和Full GC区别:

新生代GC:Minor GC,发生在新生代,非常频繁,也很快

老年代GC: Major GC/Full GC ,指发生在老年代的GC,经常会伴随至少一次的Minor GC(非绝对).Major GC的速度一般会比Minor GC慢10倍以上。 

posted @ 2018-09-25 16:25  旭日晨  阅读(136)  评论(0)    收藏  举报