Java GC垃圾回收

Java的内存分配和回收也主要在Java的堆上进行的,Java的堆中存储了大量的对象实例,所以Java的堆也叫GC堆。

Java在垃圾收集的过程中,主要用到了分代收集算法,具体有复制、标记清除、标记压缩三种实现算法

1. 标记 - 清除算法
标记清除算法是最基础的收集算法,其他收集算法都是基于这种思想。标记清除算法分为“标记”和“清除”两个阶段:首先标记出需要回收的对象,标记完成之后统一清除对象。
它的主要缺点:
①.标记和清除过程效率不高
②.标记清除之后会产生大量不连续的内存碎片。

2.复制算法
它将可用内存容量划分为大小相等的两块,每次只使用其中的一块。当这一块用完之后,就将还存活的对象复制到另外一块上面,然后在把已使用过的内存空间一次清理掉。

这样使得每次都是对其中的一块进行内存回收,不会产生碎片等情况,只要移动堆订的指针,按顺序分配内存即可,实现简单,运行高效。
主要缺点:
内存缩小为原来的一半。

3.标记 - 整理算法(标记压缩算法)
标记操作和“标记-清除”算法一致,后续操作不只是直接清理对象,而是在清理无用对象完成后让所有存活的对象都向一端移动,并更新引用其对象的指针。
主要缺点:
在标记-清除的基础上还需进行对象的移动,成本相对较高,好处则是不会产生内存碎片。

分代收集算法具体应用:

Java主要采用了分代收集算法。分代收集算法主要根据对象存活期的长短将内存进行划分。

Java主要将内存划分为两部分:新生代老生代

Java的新生代中,对象的存活率低,存活期期会相对会比较短一些,所以可以选用复制算法来进行内存回收。

Java的老生代中,对象的存活率比较高,并且相对存活期比较长一些,可以采用标记-清除-压缩的算法来进行内存回收。

Java内存分配策略:

使用的ParNew+Serial Old收集器组合

1.对象优先在Eden上分配
大多数情况下,对象优先在新生代Eden区域中分配。当Eden内存区域没有足够的空间进行分配时,虚拟机将触发一次 Minor GC(新生代GC)。Minor GC期间虚拟机将Eden区域的对象移动到其中一块Survivor区域。

2.大对象直接进入老年代
所谓大对象是指需要大量连续空间的对象。虚拟机提供了一个XX:PretenureSizeThreshold参数,令大于这个值的对象直接在老年代中分配。

3.长期存活的对象将进入老年代
虚拟机采用分代收集的思想管理内存,那内存回收时就必须能识别那些对象该放到新生代,那些该到老年代中。为了做到这点,虚拟机为每个对象定义了一个对象年龄Age,每经过一次新生代GC后任然存活,将对象的年龄Age增加1岁,当年龄到一定程度(默认为15)时,将会被晋升到老年代中,对象晋升老年代的年龄限定值,可通过-XX:MaxTenuringThreshold来设置。

4.动态对象年龄判定
为了使内存分配更加灵活,虚拟机并不要求对象年龄达到MaxTenuringThreshold才晋升老年代
如果Survivor区中相同年龄所有对象大小的总和大于Survivor区空间的一半,年龄大于或等于该年龄的对象在Minor GC时将复制至老年代

5.空间分配担保
新生代使用复制算法,当Minor GC时如果存活对象过多,无法完全放入Survivor区,就会向老年代借用内存存放对象,以完成Minor GC。
在触发Minor GC时,虚拟机会先检测之前GC时租借的老年代内存的平均大小是否大于老年代的剩余内存,如果大于,则将Minor GC变为一次Full GC,如果小于,则查看虚拟机是否允许担保失败,如果允许担保失败,则只执行一次Minor GC,否则也要将Minor GC变为一次Full GC。
说白了,新生代放不下就会借用老年代的空间来进行GC

Minor GC 和Full GC区别
新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为对象大多都具备朝生夕灭特性,所以Minor GC非常频繁,回收速度也比较快。
老年代GC(Major GC / Full GC):指发生在老年代中的GC,出现Major GC后,经常会伴随至少一次的 Minor GC。Major GC的速度一般会比Minor GC慢10倍以上。

Eden与2个Survivor

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”区被填满之后,会将所有对象移动到年老代中。

Java垃圾收集器:

 

1. Serial 收集器 串行
单线程的串行收集器。它在垃圾收集的时候会暂停其它所有工作线程。直到收集结束。一般在客户端模式下使用。

2. ParNew收集器 并行
ParNew收集器是Serial的多线程版本。一般运行在Server模式下首先的新生代收集器。如果老年代使用CMS收集器,基本也只能和它进行合作。参数:-XX:+UseConcMarkSweepGC,比较适合web服务的收集器。
一般ParNew和CMS组合

3. Parallel Scavenge收集器 并行
它使用复制算法的收集器,并且是多线程的。该收集器主要目的就是达到一个可控制的吞吐量,说白了就是CPU的利用率。于是该收集器比较适合后端运算比较多的服务。
-XX:MaxGCPauseMillis每次年轻代垃圾回收的最长时间(最大暂停时间),收集器尽量保证内存回收时间不大于这个值,应该设置一个合理的值。
-XX:GCTimeRatio设置垃圾回收时间占程序运行时间的百分比
-XX:+UseAdaptiveSizePolicy 设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开.

4.Serial Old收集器 串行
单线程串行的老生代收集器。

5. Parallel Old 收集器 并行
使用“标记-整理”的算法。该收集器比较适合和Parallel Scavenge收集器进行组合。-XX:+UseParallelOldGC

6. CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,目前大部分的B/S系统都使用CMS的收集器。一般CMS是老生代收集器,新生代就和ParNew进行组合。
CMS收集器基于“标记-清除”的算法。分四个阶段:初始标记,并发标记,重新标记,并发清除
CMS收集器的优点:并发收集、低停顿
CMS缺点:
1. CMS收集器对CPU资源非常敏感。在并发阶段,虽然不会导致用户线程停顿,但是会占用CPU资源而导致引用程序变慢,总吞吐量下降。CMS默认启动的回收线程数是:(CPU数量+3) / 4。
2. CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure“,失败后而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行,伴随程序的运行自热会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在本次收集中处理它们,只好留待下一次GC时将其清理掉。这一部分垃圾称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,即需要预留足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分内存空间提供并发收集时的程序运作使用。在默认设置下,CMS收集器在老年代使用了68%的空间时就会被激活,也可以通过参数-XX:CMSInitiatingOccupancyFraction的值来提供触发百分比,以降低内存回收次数提高性能。要是CMS运行期间预留的内存无法满足程序其他线程需要,就会出现“Concurrent Mode Failure”失败,这时候虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数-XX:CMSInitiatingOccupancyFraction设置的过高将会很容易导致“Concurrent Mode Failure”失败,性能反而降低。
3. CMS是基于“标记-清除”算法实现的收集器,使用“标记-清除”算法收集后,会产生大量碎片。空间碎片太多时,将会给对象分配带来很多麻烦,比如说大对象,内存空间找不到连续的空间来分配不得不提前触发一次Full GC。为了解决这个问题,CMS收集器提供了一个-XX:UseCMSCompactAtFullCollection开关参数,用于在Full GC之后增加一个碎片整理过程,还可通过-XX:CMSFullGCBeforeCompaction参数设置执行多少次不压缩的Full GC之后,跟着来一次碎片整理过程。

总结:

GC又分为 minor GC 和 Full GC (也称为 Major GC )。Java 堆内存分为新生代和老年代,新生代中又分为1个 Eden 区域 和两个 Survivor 区域。

那么对于 Minor GC 的触发条件:大多数情况下,直接在 Eden 区中进行分配。如果 Eden区域没有足够的空间,那么就会发起一次 Minor GC;对于 Full GC(Major GC)的触发条件:也是如果老年代没有足够空间的话,那么就会进行一次 Full GC。

GC主要做了清理对象,整理内存的工作。Java堆分为新生代和老年代,采用了不同的回收方式。

例如新生代采用了复制算法,老年代采用了标记整理法。在新生代中,分为一个Eden 区域和两个Survivor区域,真正使用的是一个Eden区域和一个Survivor区域,GC的时候,会把存活的对象放入到另外一个Survivor区域中,然后再把这个Eden区域和Survivor区域清除。那么对于老年代,采用的是标记整理法,首先标记出存活的对象,然后再移动到一端。这样也有利于减少内存碎片。

在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。如果大于则进行Minor GC,如果小于则看HandlePromotionFailure设置是否允许担保失败(不允许则直接Full GC)。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于则尝试Minor GC(如果尝试失败也会触发Full GC),如果小于则进行Full GC。

http://blog.csdn.net/java2000_wl/article/details/8009362 

http://blog.csdn.net/lojze_ly/article/details/49456255

http://blog.csdn.net/java2000_wl/article/details/8022293

http://www.idouba.net/a-simple-example-demo-jvm-allocation-and-gc/

http://blog.csdn.net/java2000_wl/article/details/8030172

http://blog.163.com/jekyll_zhou@126/blog/static/182047382015743410747/

http://www.cnblogs.com/dolphin0520/p/3783345.html

http://www.cnblogs.com/zhguang/p/3257367.html

http://jet-han.oschina.io/2017/08/05/jvm%E8%B0%83%E4%BC%98/

posted @ 2016-11-13 22:25  hongdada  阅读(1325)  评论(0编辑  收藏  举报