Hello World

吞风吻雨葬落日 欺山赶海踏雪径

0%

java的垃圾回收总结

垃圾回收(Garbage Collection,GC)

可回收对象的判定

GC Roots和对象之间不可达(没有任何引用链接相连),被判定为可回收对象。
GC Roots的对象包括下面几种:

  1. 虚拟机栈中引用的对象;
  2. 方法区中类静态属性引用的对象;
  3. 方法区中常量引用的对象;
  4. 本地方法栈中Native方法引用的对象。

垃圾回收算法

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

标记-清除算法分为两个阶段:标记阶段清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。
标记-清除算法实现起来比较容易,当时容易产生内存碎片。碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。

Copying(复制)算法

复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。
这种算法虽然实现简单,运行高效且不容易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。
而且Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。

Mark-Compact(标记-整理)算法

为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。

Generational Collection(分代收集)算法

将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。

注意,在堆区之外还有一个代就是永久代(Permanet Generation),它用来存储class类、常量、方法描述等。对永久代的回收主要回收两部分内容:废弃常量和无用的类

目前大部分垃圾收集器对新生代都采取Copying算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,但是实际中并不是按照1:1的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间,默认比例为8:1:1(一个Survivor占年轻代的1/10),通过JVM参数-XX:SurvivorRatio=8设置比例,每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将Eden(经过一次Minor GC)和Survivor中还存活的对象复制到另一块Survivor空间中,然后清理掉Eden和刚才使用过的Survivor空间。(所以必然有一个Survivor区是空的)

而由于老年代的特点是每次回收都只回收少量对象,老年代一般使用Mark-Compact算法

相关的jvm参数

-Xmn

设置新生代的初始大小eden+ 2 survivor。(老版本1.3或1.4是-XX:NewSize)
整个堆大小=年轻代大小 + 年老代大小 + 持久代大小.

增大年轻代后,将会减小年老代大小.此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8

-XX:NewRatio

年轻代(包括Eden和两个Survivor区)与年老代的比值.(除去持久代)
即-XX:NewRatio=老年代/新生代。

-XX:SurvivorRatio

参数-XX:SurvivorRatio是用来设置新生代中eden空间和s0空间的比例,即-XX:SurvivorRatio=eden/s0=eden/s1。s0和s1空间又分别称为from空间和to空间,它们的大小是相同的,职能也是一样,并在Minor GC后,会互动角色。

-Xms

JVM初始分配的堆内存, 生产环境建议与Xmx相同, 设为1024m以上

1
-Xms256m
-Xmx

JVM最大允许分配的堆内存, 生产环境建议设为1024m以上

1
-Xmx1024m
-Xss

线程堆栈大小, JDK5以上一般设置为256k或以上

1
-Xss128k
-XX:PermSize

永久代的大小

1
2
-XX:PermSize=64m
-XX:MaxPermSize=64m
-XX:+PrintGCDetails

–打印GC详细信息,相关参数包含

1
2
3
4
5
6
-XX:+PrintGC 输出GC日志
-XX:+PrintGCDetails 输出GC的详细日志
-XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
-XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
-Xloggc:../logs/gc.log 日志文件的输出路径

比如-XX:+PrintGCDetails -Xloggc:../logs/gc.log -XX:+PrintGCTimeStamps结果为

1
2
3
4
5
6
0.756: [Full GC (System) 0.756: [CMS: 0K->1696K(204800K), 0.0347096 secs] 11488K->1696K(252608K), [CMS Perm : 10328K->10320K(131072K)], 0.0347949 secs] [Times: user=0.06 sys=0.00, real=0.05 secs]  
1.728: [GC 1.728: [ParNew: 38272K->2323K(47808K), 0.0092276 secs] 39968K->4019K(252608K), 0.0093169 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
2.642: [GC 2.643: [ParNew: 40595K->3685K(47808K), 0.0075343 secs] 42291K->5381K(252608K), 0.0075972 secs] [Times: user=0.03 sys=0.00, real=0.02 secs]
4.349: [GC 4.349: [ParNew: 41957K->5024K(47808K), 0.0106558 secs] 43653K->6720K(252608K), 0.0107390 secs] [Times: user=0.03 sys=0.00, real=0.02 secs]
5.617: [GC 5.617: [ParNew: 43296K->7006K(47808K), 0.0136826 secs] 44992K->8702K(252608K), 0.0137904 secs] [Times: user=0.03 sys=0.00, real=0.02 secs]
7.429: [GC 7.429: [ParNew: 45278K->6723K(47808K), 0.0251993 secs] 46974K->10551K(252608K), 0.0252421 secs]
-XX:+DisableExplicitGC

禁止显示调用System.gc().
java nio中的direct memory时存在一定风险 http://blog.csdn.net/aitangyong/article/details/39403031
另外
代码中显示调用System.gc(),只是建议JVM进行垃圾回收,但是到底会不会执行垃圾回收是不确定的,可能会进行垃圾回收,也可能不会。

-XX:MaxTenuringThreshold

这个参数用于控制对象能经历多少次Minor GC才晋升到旧生代,默认值是15。但并不意味对象一定要尽力15次minor GC才能进入老年代。
JVM会自动调整这个age的阈值。
http://blog.sina.com.cn/s/blog_a57761d6010107r9.html

其他参数参考链接:
http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html

垃圾回收器

Serial(新生代)/Serial Old(老年代)

Serial/Serial Old收集器是最基本最古老的收集器,它是一个单线程收集器,并且在它进行垃圾收集时,必须暂停所有用户线程。它的优点是实现简单高效,但是缺点是会给用户带来停顿。
Serial收集器是针对新生代的收集器,采用的是Copying算法。它的缺点是当Serial收集器想进行垃圾回收的时候,必须暂停用户的所有进程,即stop the world。但到现在为止,它依然是虚拟机运行在client模式下的默认新生代收集器,与其他收集器相比,对于限定在单个CPU的运行环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾回收自然可以获得最高的单线程收集效率。
Serial Old收集器是针对老年代的收集器,采用的是Mark-Compact算法。这个收集器的主要意义也是被Client模式下的虚拟机使用。在Server模式下,它主要还有两大用途:一个是在JDK1.5及以前的版本中与Parallel Scanvenge收集器搭配使用,另外一个就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure的时候使用。

通过指定-UseSerialGC参数,使用Serial + Serial Old的串行收集器组合进行内存回收。

ParNew(新生代)

ParNew收集器是Serial收集器新生代的多线程实现,注意在进行垃圾回收的时候依然会stop the world,只是相比较Serial收集器而言它会运行多条进程进行垃圾回收。

ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百的保证能超越Serial收集器。当然,随着可以使用的CPU的数量增加,它对于GC时系统资源的利用还是很有好处的。它默认开启的收集线程数与CPU的数量相同,在CPU非常多(譬如32个,现在CPU动辄4核加超线程,服务器超过32个逻辑CPU的情况越来越多了)的环境下,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。

-UseParNewGC: 打开此开关后,使用ParNew + Serial Old的收集器组合进行内存回收,这样新生代使用并行收集器,老年代使用串行收集器。

Parallel Scavenge(新生代)/Parallel Old(老年代)

Parallel是采用复制算法的多线程新生代垃圾回收器,似乎和ParNew收集器有很多的相似的地方。但是Parallel Scanvenge收集器的一个特点是它所关注的目标是吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)。停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能够提升用户的体验;而高吞吐量则可以最高效率地利用CPU时间,尽快地完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,采用多线程和”标记-整理”算法。这个收集器是在jdk1.6中才开始提供的,在此之前,新生代的Parallel Scavenge收集器一直处于比较尴尬的状态。原因是如果新生代Parallel Scavenge收集器,那么老年代除了Serial Old(PS MarkSweep)收集器外别无选择。由于单线程的老年代Serial Old收集器在服务端应用性能上的”拖累“,即使使用了Parallel Scavenge收集器也未必能在整体应用上获得吞吐量最大化的效果,又因为老年代收集中无法充分利用服务器多CPU的处理能力,在老年代很大而且硬件比较高级的环境中,这种组合的吞吐量甚至还不一定有ParNew加CMS的组合”给力“。直到Parallel Old收集器出现后,”吞吐量优先“收集器终于有了比较名副其实的应用祝贺,在注重吞吐量及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。

-UseParallelGC: 虚拟机运行在Server模式下的默认值,打开此开关后,使用Parallel Scavenge + Serial Old的收集器组合进行内存回收。-UseParallelOldGC: 打开此开关后,使用Parallel Scavenge + Parallel Old的收集器组合进行垃圾回收

CMS(老年代)

CMS(Concurrent Mark Swep)收集器是一个比较重要的回收器,应用非常广泛,CMS一种获取最短回收停顿时间为目标的收集器,这使得它很适合用于和用户交互的业务。从名字(Mark Swep)就可以看出,CMS收集器是基于标记清除算法实现的。它的收集过程分为四个步骤:

初始标记(initial mark) -stop the world
并发标记(concurrent mark)
重新标记(remark) -stop the world
并发清除(concurrent sweep)
注意初始标记和重新标记还是会stop the world,但是在耗费时间更长的并发标记和并发清除两个阶段都可以和用户进程同时工作。

不过由于CMS收集器是基于标记清除算法实现的,会导致有大量的空间碎片产生,在为大对象分配内存的时候,往往会出现老年代还有很大的空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前开启一次Full GC。为了解决这个问题,CMS收集器默认提供了一个-XX:+UseCMSCompactAtFullCollection收集开关参数(默认就是开启的),用于在CMS收集器进行FullGC完开启内存碎片的合并整理过程,内存整理的过程是无法并发的,这样内存碎片问题倒是没有了,不过停顿时间不得不变长。虚拟机设计者还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction参数用于设置执行多少次不压缩的FULL GC后跟着来一次带压缩的(默认值为0,表示每次进入Full GC时都进行碎片整理)。

不幸的是,它作为老年代的收集器,却无法与jdk1.4中已经存在的新生代收集器Parallel Scavenge配合工作,所以在jdk1.5中使用cms来收集老年代的时候,新生代只能选择ParNew或Serial收集器中的一个。ParNew收集器是使用-XX:+UseConcMarkSweepGC选项启用CMS收集器之后的默认新生代收集器,也可以使用-XX:+UseParNewGC选项来强制指定它。

CMS算法的几个常用参数:
UseCMSInitatingOccupancyOnly:表示只在到达阈值的时候,才进行 CMS 回收。
为了减少第二次暂停的时间,通过-XX:+CMSParallelRemarkEnabled开启并行remark。如果ramark时间还是过长的话,可以开启-XX:+CMSScavengeBeforeRemark选项,强制remark之前开启一次minor gc,减少remark的暂停时间,但是在remark之后也立即开始一次minor gc。
CMS默认启动的回收线程数目是(ParallelGCThreads + 3)/4,如果你需要明确设定,可以通过-XX:+ParallelCMSThreads来设定,其中-XX:+ParallelGCThreads代表的年轻代的并发收集线程数目。
CMSClassUnloadingEnabled: 允许对类元数据进行回收。
CMSInitatingPermOccupancyFraction:当永久区占用率达到这一百分比后,启动 CMS 回收 (前提是-XX:+CMSClassUnloadingEnabled 激活了)。
CMSIncrementalMode:使用增量模式,比较适合单 CPU。
UseCMSCompactAtFullCollection参数可以使 CMS 在垃圾收集完成后,进行一次内存碎片整理。内存碎片的整理并不是并发进行的。
UseFullGCsBeforeCompaction:设定进行多少次 CMS 垃圾回收后,进行一次内存压缩。

一些建议

对于Native Memory:

1
2
3
使用了NIO或者NIO框架(Mina/Netty)
使用了DirectByteBuffer分配字节缓冲区
使用了MappedByteBuffer做内存映射

由于Native Memory只能通过FullGC回收,所以除非你非常清楚这时真的有必要,否则**不要轻易调用System.gc()**。

另外为了防止某些框架中的System.gc调用(例如NIO框架、Java RMI),建议在启动参数中加上-XX:+DisableExplicitGC来禁用显式GC。这个参数有个巨大的坑,如果你禁用了System.gc(),那么上面的3种场景下的内存就无法回收,可能造成OOM,如果你使用了CMS GC,那么可以用这个参数替代:-XX:+ExplicitGCInvokesConcurrent

此外除了CMS的GC,其实其他针对old gen的回收器都会在对old gen回收的同时回收young gen。

G1

G1收集器是一款面向服务端应用的垃圾收集器。HotSpot团队赋予它的使命是在未来替换掉JDK1.5中发布的CMS收集器。与其他GC收集器相比,G1具备如下特点:

并行与并发:G1能更充分的利用CPU,多核环境下的硬件优势来缩短stop the world的停顿时间。
分代收集:和其他收集器一样,分代的概念在G1中依然存在,不过G1不需要其他的垃圾回收器的配合就可以独自管理整个GC堆
空间整合:G1收集器有利于程序长时间运行,分配大对象时不会无法得到连续的空间而提前触发一次GC。
可预测的非停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
在使用G1收集器时,Java堆的内存布局和其他收集器有很大的差别,它将这个Java堆分为多个大小相等的独立区域,虽然还保留新生代和老年代的概念,但是新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。

虽然G1看起来有很多优点,实际上CMS还是主流。

图解

CMS的补充

目前CMS作为低延迟的垃圾回收器被广泛运用于生产环境,所以这里在深入下。

CMS详细的步骤 STW=STOP THE WORD

  • trigger CMS
  • STW initial mark
  • Concurrent marking
  • Concurrent precleaning
  • STW remark
  • Concurrent sweeping
  • Concurrent reset

CMS GC log

1
2
3
4
5
6
7
8
9
10
11
12
13
4391.322: [GC [1 CMS-initial-mark: 655374K(1310720K)] 662197K(1546688K), 0.0303050 secs] [Times: user=0.02 sys=0.02, real=0.03 secs]
4391.352: [CMS-concurrent-mark-start]4391.779: [CMS-concurrent-mark: 0.427/0.427 secs] [Times: user=1.24 sys=0.31, real=0.42 secs]
4391.779: [CMS-concurrent-preclean-start]
4391.821: [CMS-concurrent-preclean: 0.040/0.042 secs] [Times: user=0.13 sys=0.03, real=0.05 secs]
4391.821: [CMS-concurrent-abortable-preclean-start]
4392.511: [CMS-concurrent-abortable-preclean: 0.349/0.690 secs] [Times: user=2.02 sys=0.51, real=0.69 secs]
4392.516: [GC[YG occupancy: 111001 K (235968 K)]
4392.516: [Rescan (parallel) , 0.0309960 secs]
4392.547: [weak refs processing, 0.0417710 secs] [1 CMS-remark: 655734K(1310720K)] 766736K(1546688K), 0.0932010 secs] [Times: user=0.17 sys=0.00, real=0.09 secs]
4392.609: [CMS-concurrent-sweep-start]
4394.310: [CMS-concurrent-sweep: 1.595/1.701 secs] [Times: user=4.78 sys=1.05, real=1.70 secs]
4394.310: [CMS-concurrent-reset-start]
4394.364: [CMS-concurrent-reset: 0.054/0.054 secs] [Times: user=0.14 sys=0.06, real=0.06 secs]

触发

通常情况下JVM会根据运行时数据来决定是否启动CMS,但是可以通过CMSInitiatingOccupancyFractionUseCMSInitiatingOccupancyOnly来按手动配置的条件启动CMS

1
2
-XX:CMSInitiatingOccupancyFraction=80   老年代到达此比例触发fullgc
-XX:+UseCMSInitiatingOccupancyOnly 使用自定义的条件触发CMS

初始标记

STW阶段,从垃圾回收的root对象开始扫描和root有直接关联的对象,并标记。

1
4391.322: [GC [1 CMS-initial-mark: 655374K(1310720K)] 662197K(1546688K), 0.0303050 secs] [Times: user=0.02 sys=0.02, real=0.03 secs]

655374K(1310720K) 代表永久代的已用空间和总空间

并发标记

GC线程和应用线程并发执行的,从初始化标记阶段标记的对象基础上继续向下追溯标记,标记可达的对象。这个阶段会遍历整个老年代并且标记所有存活的对象。

1
CMS-concurrent-mark-start,CMS-concurrent-mark

并发预清理

在并发标记时候,一些对象引用可能已经发生变化,当这些引用发生变化的时候,JVM会标记堆的这个区域为Dirty Card(包含被标记但是改变了的对象,被认为”dirty”),这就是 Card Marking。
http://psy-lob-saw.blogspot.com/2014/10/the-jvm-write-barrier-card-marking.html


那些能够从dirty card对象可达的对象也会被标记,这个标记做完之后,dirty card标记会被清除。

1
CMS-concurrent-preclean
终止预清理

这个阶段尝试着去承担STW的Final Remark阶段足够多的工作。这个阶段持续的时间依赖好多的因素,由于这个阶段是重复的做相同的事情直到发生aboart的条件(比如:重复的次数、多少量的工作、持续的时间等等)之一才会停止。
有多个参数可以控制此阶段

  • -XX:CMSMaxAbortablePrecleanTime abortable-preclean阶段执行达到这个时间时才会结束,默认是5s
  • -XX:CMSScheduleRemarkEdenSizeThreshold 控制abortable-preclean什么时候开始执行,即当eden使用达到此值时,才会开始abortable-preclean阶段,默认2m
  • -XX:CMSScheduleRemarkEdenPenetratio 控制abortable-preclean阶段什么时候结束执行
1
2
CMS-concurrent-abortable-preclean-start
CMS-concurrent-abortable-preclean

重新标记

CMS第二个stop the world阶段,此阶段暂停应用线程,对对象进行重新扫描并标记。由于之前的预处理是并发的,它可能跟不上应用程序改变的速度,这个时候,STW是非常必要的。
YG occupancy:964861K(2403008K),指执行时young代的情况
CMS remark:961330K(1572864K),指执行时old代的情况

并发清理

这个阶段的目的就是移除那些不用的对象,回收他们占用的空间并且为将来使用。

CMS回收器采用的基础算法是Mark-Sweep。所有CMS不会整理、压缩堆空间。这样就会有一个问题:经过CMS收集的堆会产生空间碎片
CMS收集器提供了一个-XX:UseCMSCompactAtFullCollection开关参数,用于在Full GC之后增加一个碎片整理过程。还可通过-XX:CMSFullGCBeforeCompaction参数设置执行多少次不压缩的Full GC之后,跟着来一次碎片整理过程。

并发重置

这个阶段并发执行,重新设置CMS算法内部的数据结构,准备下一个CMS生命周期的使用。

比较典型的CMS配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-server
-Xms4g 初始堆内存
-Xmx4g 最大对内存
-Xmn2g 设置年轻代大小为2G 内存大小=年轻代大小 + 年老代大小 + 持久代大小
-XX:MetaspaceSize=256m Metaspace初始大小
-XX:MaxMetaspaceSize=512m Metaspace最大大小
-XX:MaxDirectMemorySize=1g 堆外直接内存
-XX:SurvivorRatio=10 eden:survivor=10,eden+2*survivor=Xmn
-XX:+UseConcMarkSweepGC 使用CMS
-XX:CMSMaxAbortablePrecleanTime=5000 preClean超过此时间AbortablePreclean直接触发remark
-XX:+CMSClassUnloadingEnabled 清理持久代移除不使用的class
-XX:CMSInitiatingOccupancyFraction=80 老年代到达此比例触发fullgc
-XX:+UseCMSInitiatingOccupancyOnly 使用自定义的条件触发CMS
-XX:+CMSParallelRemarkEnabled 降低标记停顿
-XX:+UseFastAccessorMethods, ##原始类型的快速优化

参考

CMS-maxabortableprecleantime
http://blog.parwy.com/2009/09/cmsmaxabortableprecleantime.html

CMS浅析
https://www.jianshu.com/p/226093b08362

Card Marking
http://psy-lob-saw.blogspot.com/2014/10/the-jvm-write-barrier-card-marking.html

Java VM Options You Should Always Use in Production
https://blog.sokolenko.me/2014/11/javavm-options-production.html