天天看点

Jvm垃圾回收初探1、简介2、常用JVM参数配置3、垃圾收集算法3、垃圾收集器4、内存分配与回收

1、简介

a、意义

垃圾回收可以有效的防止内存泄露,有效的使用空闲的内存。 内存泄露指该内存空间使用完毕后未回收,在不涉及复杂数据结构的一般情况下,java的内存泄露表现为一个内存对象的生命周期超出了程序需要它的时间长度,也将其称为“对象游离”。

b、判断

判断废弃常量
  • 如果常量池中的某个常量没有被任何引用所引用,则该常量是废弃常量。
判断无用的类
  • 该类的所有实例都已经被回收,即java堆中不存在该类的实例对象。
  • 加载该类的类加载器已经被回收。
  • 该类所对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射机制访问该类的方法。

c、特性

  • 垃圾回收机制只负责回收内存中的对象,不会回收任何物理资源(例如数据库连接、网络IO等资源 );
  • 程序无法精确控制垃圾回收的运行,垃圾回收会在合适的时候进行。当对象永久地失去引用后,系统就会在合适的时候回收它所占有的内存 ;
  • 垃圾回收机制回收任何对象执之前,总会调用finalize () 方法,该方法可能是该对象重新复活(然一个引用变量重新引用该对象 ), 从而导致垃圾回收机制取消回收。

2、常用JVM参数配置

  • -server//服务器模式
  • -Xmx2g //JVM最大允许分配的堆内存,按需分配
  • -Xms2g //JVM初始分配的堆内存,一般和Xmx配置成一样以避免每次gc后JVM重新分配内存。
  • -Xmn256m //年轻代内存大小,整个JVM内存=年轻代 + 年老代 + 持久代
  • -XX:PermSize=128m //持久代内存大小
  • -Xss256k //设置每个线程的堆栈大小
  • -XX:+DisableExplicitGC //忽略手动调用GC, System.gc()的调用就会变成一个空调用,完全不触发GC
  • -XX:+UseConcMarkSweepGC //并发标记清除(CMS)收集器
  • -XX:+CMSParallelRemarkEnabled //降低标记停顿
  • -XX:+UseCMSCompactAtFullCollection //在FULL GC的时候对年老代的压缩
  • -XX:LargePageSizeInBytes=128m //内存页的大小
  • -XX:+UseFastAccessorMethods //原始类型的快速优化
  • -XX:+UseCMSInitiatingOccupancyOnly //使用手动定义初始化定义开始CMS收集
  • -XX:CMSInitiatingOccupancyFraction=70 //使用cms作为垃圾回收使用70%后开始CMS

对于CMS GC时出现promotion failed和concurrent mode failure的调优:

-server
-Xms6000M
-Xmx6000M
-Xmn500M
-XX:PermSize=M
-XX:MaxPermSize=M
-XX:SurvivorRatio=
-XX:MaxTenuringThreshold=
-Xnoclassgc
-XX:+DisableExplicitGC
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=
-XX:+CMSClassUnloadingEnabled
-XX:-CMSParallelRemarkEnabled
-XX:CMSInitiatingOccupancyFraction=
-XX:SoftRefLRUPolicyMSPerMB=
-XX:+PrintClassHistogram
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintHeapAtGC
-Xloggc:log/gc.log
           

产生原因:

promotion failed是在进行Minor GC时,survivor space放不下、对象只能放入旧生代,而此时旧生代也放不下造成的;concurrent mode failure是在执行CMS GC的过程中同时有对象要放入旧生代,而此时旧生代空间不足造成的。

3、垃圾收集算法

a、引用计数

每个对象计算指向它的指针的数量,当有一个指针指向自己时计数值加1;当删除一个指向自己的指针时,计数值减1,如果计数值减为0,说明已经不存在指向该对象的指针了,所以它可以被安全的销毁了。

  • 优点是它在进行垃圾回收的时候无需挂起程序,常用在实时系统中;空间上的引用局部性比较好。某个对象的引用计数变为0后,系统无需访问位于堆中其他页面的单元。废弃即回收。
  • 缺点:每次创建和销毁都要更新引用计数值,会引起额外的开销;引用计数占据了额外的空间;无法处理环形引用;

b、标记-清除

标记和清除两个阶段,标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象。

  • 缺点:效率问题,标记和清楚过程的效率都不高;空间问题,标记清楚后会产生大量不连续的内存碎片,空间碎片太多可能会导致当程序在以后的运行过程中需要分配较大对象时无法找到足够连续的内存空间而不得不提前出发另一次垃圾收集动作。

c、复制算法

为了解决效率问题,复制算法将可用内存按容量划分为大小相等的两块,每次只是用其中一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

  • 优点:这样使得每次都是对其中的一块进行内存回收,没存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
  • 缺点:可用内存缩小为原来的一半。

实际上新生代中的对象98%是朝生夕死的,并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中的一块Survivor。回收时,将Eden和Survivor中还存活着的兑现个一次性地拷贝到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor的空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%,只有10%的内存是会被浪费的。

d、标记整理

标记-整理(Mark-Compact)算法不直接对可回收对象进行清理,而是让所有可用的对象都向一端移动,然后直接清理掉边界意外的内存。即在清理无用对象完成后让所有存活的对象都向一端移动,并更新引用其对象的指针。缺点:在标记-清除的基础上还需进行对象的移动,成本相对较高,好处则是不会产生内存碎片。

e、分代收集

据内存中对象的存活周期不同,将内存划分为几块,java的虚拟机中一般把内存划分为新生代和年老代,当新创建对象时一般在新生代中分配内存空间,当新生代垃圾收集器回收几次之后仍然存活的对象会被移动到年老代内存中,当大对象在新生代中无法找到足够的连续内存时也直接在年老代中创建。

新生代使用复制和标记-清除垃圾收集算法。

年老代中的对象一般都是长生命周期对象,对象的存活率比较高,因此在年老代中使用标记-整理垃圾回收算法。

java虚拟机内存中的方法区在Sun HotSpot虚拟机中被称为永久代,是被各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。永久代垃圾回收比较少,效率也比较低,但是也必须进行垃圾回收,否则会永久代内存不够用时仍然会抛出OutOfMemoryError异常。

3、垃圾收集器

串行垃圾回收:

串行垃圾回收器通过持有应用程序所有的线程进行工作。它为单线程环境设计,只使用一个单独的线程进行垃圾回收,通过冻结所有应用程序线程进行工作,所以可能不适合服务器环境。它最适合的是简单的命令行程序。通过JVM参数-XX:+UseSerialGC可以使用串行垃圾回收器。

并行垃圾回收:

并行垃圾回收器也叫做 throughput collector 。它是JVM的默认垃圾回收器。与串行垃圾回收器不同,它使用多线程进行垃圾回收。不过同的是:当执行垃圾回收的时候它也会冻结所有的应用程序线程。

并发垃圾回收:

并发标记垃圾回收使用多线程扫描堆内存,标记需要清理的实例并且清理被标记过的实例。相比并行垃圾回收器,并发标记扫描垃圾回收器使用更多的CPU来确保程序的吞吐量。如果我们可以为了更好的程序性能分配更多的CPU,那么并发标记上扫描垃圾回收器是更好的选择相比并发垃圾回收器。通过JVM参数 XX:+USeParNewGC 打开并发标记扫描垃圾回收器。

^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^

Serial收集器

Serial收集器是最基本、发展历史最悠久的单线程收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作。重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。

ParNew收集器

ParNew收集器是Serial收集器的多线程版本。除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一样。这两种收集器在实现上也共用了相当多的代码。ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器。

Parallel Scavenge收集器

Parallel Scavenge收集器是新生代使用复制算法的并行多线程收集器。它的关注点与其它收集器不同,CMS等收集器的关注点尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput),也被称为吞吐量优先收集器。

所谓吞吐量就是CPU用于运行用户代码时间与CPU总消耗时间的比值。吞吐量=运行用户代码时间/运行用户代码时间+垃圾收集时间。其停顿时间短,适合需要与用户交互的程序,良好的响应速度能提升用户体验,高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务。其主要适合在后台运算而不需要太多交互的任务。

Parallel Scavenge提供两个参数精确控制吞吐量,-XX:MaxGCPauseMillis控制最大垃圾收集停顿时间和-XX:GCTimeRatio设置吞吐量大小 。MaxGCPauseMillis允许的值是一个大于零的毫秒数,收集器将尽力保证内存回收花费的时间不超过设定值。GC停顿时间缩小是以牺牲吞吐量和新生代空间来换取的,也就是要使停顿时间更短,需要使新生代的空间减小,这样垃圾回收的频率会增加,吞吐量也降下来了。

Serial Old收集器

Serial Old是Serial收集器的老年代版本,使用标记-整理算法的单线程收集器。对于Client模式,主要在于给Client模式下的虚拟机使用;而对于Server模式,在JDK 1.5以及之前的版本中与Parallel Scavenge收集器搭配使用,或作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。

Parallel Old收集器

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

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的基于“标记—清除”算法的收集器。主要的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间短,以给用户较好体验。

其运行整个过程分为4个步骤:

  • 初始标记(CMS initial mark)。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要“Stop The World”。
  • 并发标记(CMS concurrent mark)。并发标记阶段就是进行GC Roots Tracing的过程。
  • 重新标记(CMS remark)。重新标记阶段是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短,仍然需要“Stop The World”。
  • 并发清除(CMS concurrent sweep)。并发清除阶段会清除对象。

由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

优点:

  • CMS是一款优秀的收集器,它的主要优点在名字上已经体现出来了:并发收集、低停顿。

缺点:

  • CMS收集器对CPU资源非常敏感。其实,面向并发设计的程序都对CPU资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。

    CMS默认启动的回收线程数是(CPU数量+3)/ 4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。但是当CPU不足4个(譬如2个)时,CMS对用户程序的影响就可能变得很大。

  • CMS收集器无法处理浮动垃圾。可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。
  • CMS收集器会产生大量空间碎片。CMS是一款基于“标记—清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。

G1收集器

G1(Garbage-First)是一款面向服务端应用的垃圾收集器。

G1具备如下特点。

  • 并发

    G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。

  • 分代收集

    与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。

  • 空间整合

    与CMS的“标记—清理”算法不同,G1从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。

  • 可预测的停顿

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

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

G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的来由)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。

G1收集器的运作大致可划分为以下几个步骤:

  • 初始标记(Initial Marking)

    初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要停顿线程,但耗时很短。

  • 并发标记(Concurrent Marking)

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

  • 最终标记(Final Marking)

    最终标记阶段是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。

  • 筛选回收(Live Data Counting and Evacuation)

    筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。

4、内存分配与回收

  • 对象优先分配到Eden。HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to),默认比例为8:1。一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。由于年轻代中的对象基本都是朝生夕死的(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。
  • 大对象直接分配在年老代。大对象简单的理解就是可能会占用堆空间很大的对象实例,比如说一个很大的字符串数组,因为虚拟机默认是将对象实例分配在年轻代的Eden区。年轻代垃圾收集器主要采用复制算法,如果大对象存活周期比较长意味着其会被多次复制,最重要的是当年轻代没有足够的空间来存放大对象时,会引发年轻代垃圾回收。一旦大对象数量比较多,年轻代则会频繁垃圾回收。为了防止这种情况的发生,通过-XX:PretenureSizeThreshold参数来指定直接分配到老年代的实例大小。
  • 长期存活的对象进入年老代。当对象在Eden区中经过一次年轻代的回收成功转入Survivor之后,它的年龄就为1,后续每发生一次年轻代的垃圾回收,只要该对象还存活,则它的年龄就加1,直到其年龄达到设定的最大值(默认为15),然后将被移到年老代。为了可以灵活的控制这个年龄,通过 -XXMaxTenuringThreshold参数来控制。

参考文献:

周志明. 深入理解 Java 虚拟机: JVM 高级特性与最佳实践[J]. 2010.