java 垃圾回收策略

尝试介绍几种常用的垃圾回收算法,根据年轻代、年老的的特征分别使用不同的收集算法。以及考虑到垃圾收集的吞吐量、暂停时间等采用不同的收集选项。

垃圾回收算法

收集算法主要有引用计数(reference counting)和引用追踪(tracing collector),从名字上不难理解一个就是数对象引用数,没用了则清楚,引用追踪则是产看对象是否从活的“跟对象”(“跟对象”见本文下面介绍)关联出来。收集器从根开始访问每一个活跃的节点,标记它所访问的每一个节点。走过所有引用后,收集就完成了,然后就对堆进行清除(即对堆中的每一个对象进行检查),所有没有标记的对象都作为垃圾回收并返回空闲列表。

根据算法的细节引用追踪又分为标记清除、复制、标记整理三类。

tracing-collector

引用计数(Reference?Counting

比较简单直接。最好理解最容易想到的一种策略。原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收时,只用收集计数为0的对象。若干个该回收的对象间互相引用在用该方式是检测不出来,也没办法处理的。

优点:简单,直接,不需要暂停整个应用

缺点:

  • 需要编译器的配合,编译器要生成特殊的指令来进行引用计数的操作,比如每次将对象赋值给新的引用,或者者对象的引用超出了作用域等。
  • 不能处理循环引用的问题

标记-清除(Mark-Sweep

gc1-mark-sweep

 

这个算法执行分两阶段,分别是标记Mark阶段和清理Sweep阶段。第一阶段从引用根节点开始标记所有被引用的对象;第二阶段遍历整个堆,把未标记的对象清除。此算法需要暂停整个应用。遍历整个空间的成本较大暂停时间随空间大小线性增大,而且整理后堆里的碎片很多。

优点:

  • 解决循环引用的问题
  • 不需要编译器的配合,从而就不执行额外的指令

缺点:每个活跃的对象都要进行扫描,收集暂停的时间比较长

复制(Copying

gc2-copying

 

核心原理是借用一个空的分区,在回收的时候,把数据分区中存活的对象复制到这个空分区上,然后一次性清空原分区。年轻代中eden区和两survivor区就是这样配合工作的。

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

优点:?只扫描可以到达的对象,不需要扫描所有的对象,从而减少了应用暂停的时间

缺点:

  • 需要额外的空间消耗,某一个时刻,总是有一块内存处于未使用状态
  • 复制对象需要一定的开销

标记-整理(Mark-Compact

gc3-mark-compact

 

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

??默认收集器

默认情况下,年轻代使用复制收集器。年老代使用标记-整理收集器。

对象不可达说明

root-object-reference

 

上面的垃圾收集算法都要解决一个问题,就是识别程序中不可达的内存块,进而才能用各种策略或者算法将其释放。

那就是找出应用程序不可到达的内存块,将其释放,这里面得不可到达主要是指应用程序已经没有内存块的引用了。在JAVA中,某个对象对应用程序是可到达的是指:这个对象被根(根主要是指类的静态变量,或者活跃在所有线程栈的对象的引用)引用或者对象被另一个可到达的对象引用。

因为堆是存数据的地方,栈是程序执行的地方。要获取堆上哪些对象正在被使用,就是从栈上为根节点的对象树。在这些对象树上的对象,都是当前系统运行所需要的对象,不能被垃圾回收。而其他剩余对象,则可以视为无法被引用到的对象,可以被当做垃圾进行回收。一个线程对应一个栈,在多线程环境下,需要对对应的所有栈进行检查。

最好理解的栈,就是程序的main函数。

垃圾收集器(收集选项)

垃圾收集器就是收集算法的具体实现,不同的虚拟机会提供不同的垃圾收集器。并且提供参数供用户根据自己的应用特点和要求组合各个年代所使用的收集器。

gc collectors-compare

1. 串行收集器?Serial?Collector

Serial?Collector是指任何时刻都只有一个线程进行垃圾收集,这种策略有一个名字“stop?the?whole?world”,它需要停止整个应用的执行。在整个扫描和复制过程采用单线程的方式来进行,适用于单CPU、新生代空间较小及对暂停时间要求不是非常高的应用上,是client级别默认的GC方式,可以通过-XX:+UseSerialGC来强制指定。

1)Serial?Copying?Collector

此种GC-XX:UseSerialGC选项配置,它只用于新生代对象的收集。1.5.0以后。-XX:MaxTenuringThreshold来设置对象复制的次数。当eden空间不够的时候,GC会将eden的活跃对象和一个名叫From?survivor空间中尚不够资格放入Old代的对象复制到另外一个名字叫To?Survivor的空间。而此参数就是用来说明到底From?survivor中的哪些对象不够资格,假如这个参数设置为20,那么也就是说只有对象复制20次以后才算是有资格的对象。

2)Serial??Mark-Compact?Collector

串行的标记-整理收集器是JDK5?update6之前默认的老生代的垃圾收集器,此收集使得内存碎片最少化,但是它需要暂停的时间比较长。

2.并行收集器?Parallel?Collector

并行收集器又被称为吞吐量优先的收集器。垃圾回收是多线程并行完成的。大多数商业实现的并行垃圾回收器都是stop-the-world式的垃圾回收器,即在整个垃圾回收周期结束前,所有应用程序线程都会被挂起。挂起所有应用程序线程使垃圾回收器可以以并行的方式,更有效的完成标记和清理工作。Parallel?Collector主要是为了应对多CPU,大数据量的环境。

Parallel Collector

1) Parallel?Copying?Collector

此种GC用-XX:UseParNewGC参数配置,它主要用于新生代的收集,此GC可以配合CMS一起使用。

2) Parallel?Mark-Compact?Collector

此种GC用-XX:UseParallelOldGC参数配置,此GC主要用于老生代对象的收集。

3) Parallel?scavenging?Collector

此种GC用-XX:UseParallelGC参数配置,它是对新生代对象的垃圾收集器,但是它不能和CMS配合使用,起始于jdk?1.4.0。也是一个多线程收集器,使用复制算法,但它的对象分配规则与回收策略都与ParNew收集器有所不同,它是以吞吐量最大化(即GC时间占总运行时间最小)为目标的收集器实现,它允许较长时间的STW换取总吞吐量最大化。

3. 并发收集器?Concurrent?Collector

并发收集器是暂停时间优先的?Concurrent?Low?Pause?Collector。又称为CMS(Concurrent Mark Sweep)

Concurrent?Collector通过并行的方式进行垃圾收集,这样就减少了垃圾收集器收集一次的时间,这种GC在实时性要求高于吞吐量的时候比较有用。此种GC可以用参数-XX:UseConcMarkSweepGC配置,此GC主要用于老生代和Perm代的收集。

CMS是一种以最短停顿时间为目标的收集器,使用CMS并不能达到GC效率最高(总体GC时间最小),但它能尽可能降低GC时服务的停顿时间,这一点对于实时或者高交互性应用(譬如证券交易)来说至关重要,这类应用对于长时间STW一般是不可容忍的。CMS收集器使用的是标记-清除算法,也就是说它在运行期间会产生空间碎片,所以虚拟机提供了参数开启CMS收集结束后再进行一次内存压缩。

Concurrent Collector

 

gc中并行Parallel?与并发Concurrent的差别

  • 并行:指多条垃圾收集线程并行,此时用户线程是没有运行的;
  • 并发:指用户线程与垃圾收集线程并发执行,程序在继续运行,而垃圾收集程序运行于另一个个CPU

并发收集一开始会很短暂的停止一次所有线程来开始初始标记根对象,然后标记线程与应用线程一起并发运行。更详细的参照:JVM性能优化, Part 3 垃圾回收

下图展示了1.6中提供的6种作用于不同年代的收集器,两个收集器之间存在连线的话就说明它们可以搭配使用。

collector-compare-2

下表则列举出不同的jvm配置对新生代、老年代gc的设置。

gc-policy-options

摘录来自HotSpot VM GC 的种类一文对串行、并行、并发三种收集器的比较如下。

类别 serial collector parallel collector
(?throughput collector?)
concurrent collector
(concurrent low pause collector)
介绍 单线程收集器
使用单线程去完成所有的gc工作,没有线程间的通信,这种方式会相对高效
并行收集器
使用多线程的方式,利用多CUP来提高GC的效率
主要以到达一定的吞吐量为目标
并发收集器
使用多线程的方式,利用多CUP来提高GC的效率
并发完成大部分工作,使得gc pause短
试用场景 单处理器机器且没有pause time的要求 适用于科学技术和后台处理
有中规模/大规模数据集大小的应用且运行在多处理器上,关注吞吐量(throughput)
适合中规模/大规模数据集大小的应用,应用服务器,电信领域
关注response time,而不是throughput
使用 Client模式下默认
可使用
可用-XX:+UseSerialGC强制使用
优点:对server应用没什么优点
缺点:慢,不能充分发挥硬件资源
Server模式下默认–YGC:PS FGC:Parallel MSC

可用-XX:+UseParallelGC或-XX:+UseParallelOldGC强制指定

–ParallelGC代表FGC为Parallel MSC

–ParallelOldGC代表FGC为Parallel Compacting

优点:高效

缺点:当heap变大后,造成的暂停时间会变得比较长

可用-XX:+UseConcMarkSweepGC强制指定
优点:
对old进行回收时,对应用造成的暂停时间非常端,适合对latency要求比较高的应用
缺点:
1.内存碎片和浮动垃圾
2.old去的内存分配效率低
3.回收的整个耗时比较长
4.和应用争抢CPU
内存回收触发 YGC
eden空间不足
FGC
old空间不足
perm空间不足
显示调用System.gc() ,包括RMI等的定时触发
YGC时的悲观策略
dump live的内存信息时(jmap –dump:live)
YGC
eden空间不足
FGC
old空间不足
perm空间不足
显示调用System.gc() ,包括RMI等的定时触发
YGC时的悲观策略–YGC前&YGC后
dump live的内存信息时(jmap –dump:live)
YGC
eden空间不足
CMS GC
1.old Gen的使用率大的一定的比率 默认为92%
2.配置了CMSClassUnloadingEnabled,且Perm Gen的使用达到一定的比率 默认为92%
3.Hotspot自己根据估计决定是否要触法
4.在配置了ExplictGCInvokesConcurrent的情况下显示调用了System.gc.
Full GC(Serial MSC)
promotion failed 或 concurrent Mode Failure时;
内存回收触发时发生了什么 YGC
清空eden+from中所有no ref的对象占用的内存
将eden+from中的所有存活的对象copy到to中
在这个过程中一些对象将晋升到old中:
–to放不下的
–存活次数超过tenuring threshold的
重新计算Tenuring Threshold;
单线程做以上动作
全程暂停应用
FGC
如果配置了CollectGen0First,则先触发YGC
清空heap中no ref的对象,permgen中已经被卸载的classloader中加载的class的信息
单线程做以上动作
全程暂停应用
YGC
同serial动作基本相同,不同点:
1.多线程处理
2.YGC的最后不仅重新计算Tenuring Threshold,还会重新调整Eden和From的大小
FGC
1.如配置了ScavengeBeforeFullGC(默认),则先触发YGC(??)
2.MSC:清空heap中的no ref对象,permgen中已经被卸载的classloader中加载的class信息,并进行压缩
3.Compacting:清空heap中部分no ref的对象,permgen中已经被卸载的classloader中加载的class信息,并进行部分压缩
多线程做以上动作.
YGC
同serial动作基本相同,不同点:
1.多线程处理
CMSGC:
1.old gen到达比率时只清除old gen中no ref的对象所占用的空间
2.perm gen到达比率时只清除已被清除的classloader加载的class信息
FGC
同serial
细节参数 可用-XX:+UseSerialGC强制使用
-XX:SurvivorRatio=x,控制eden/s0/s1的大小
-XX:MaxTenuringThreshold,用于控制对象在新生代存活的最大次数
-XX:PretenureSizeThreshold=x,控制超过多大的字节的对象就在old分配.
-XX:SurvivorRatio=x,控制eden/s0/s1的大小
-XX:MaxTenuringThreshold,用于控制对象在新生代存活的最大次数
-XX:UseAdaptiveSizePolicy 去掉YGC后动态调整eden from已经tenuringthreshold的动作-XX:ParallelGCThreads 设置并行的线程数
-XX:CMSInitiatingOccupancyFraction 设置old gen使用到达多少比率时触发
-XX:CMSInitiatingPermOccupancyFraction,设置Perm Gen使用到达多少比率时触发
-XX:+UseCMSInitiatingOccupancyOnly禁止hostspot自行触发CMS GC

注:

参考:

原创文章。为了维护文章的版本一致、最新、可追溯,转载请注明: 转载自idouba

本文链接地址: java 垃圾回收策略


,

Trackbacks/Pingbacks

  1. 最简单例子图解JVM内存分配和回收 – Matrix工作室博客 - 2017年4月24日

    […] 还是尝试在eden上分配,但是eden空间只剩下28M,不能容纳alloc3要求的40M空间。于是触发在新生代上的一次gc,将Eden区的存活对象转移到Survivor区。在这个里先将2M的alloc1对象存放(其实是copy,参见java 垃圾回收策略的描述)到from区,然后copy 50M的alloc2对象,显然survivor区不能容纳下alloc2对象,该对象被直接copy到年老代。需要说明的是复制到Survivor区的对象在经历一次gc后期对象年龄会被加一。 […]

  2. Java 分代垃圾回收 | idouba - 2017年1月10日

    […] 年老代使用标记整理Mark-Compact算法。不同于年轻代,能添加到年老代的对象都是生命期比较长,都是些老不死的对象,即一般情况下,每次都年老代进行回收时,大部分对象都是存活的。因此不可能大量的复制(浪费空间又浪费效率),因此一般采用标记清理的方式是。详细参照GC策略介绍。 […]

发表评论