原博客地址:http://www.solr.cc/blog/?p=369
垃圾回收,一直是JVM帮我们干,看过几篇文章。总结一下,不求能优化回收机制,记录下来就好。
做点准备工作:GC工作就是回收死对象,腾出空间。
什么是死对象:没有任何引用的对象就是死对象.应该被回收。
怎么找死对象:通过根搜索方法找到死对象。
说的挺玄,其实就是通过GC ROOTS能不能找到,找不到的对象就被认为是死对象。
垃圾回收需要说明几个问题 :
- 内存分配
- 对象的生命周期
- 垃圾回收的算法
- 垃圾回收器(这个不打算写)
1 内存分配:堆存放对象实例,是垃圾回收的主要战场。
我们先将堆拆分,jvm 采取的是分代(新生代和老年代)管理:
采取分代管理和java的两种垃圾回收方式有关系:
- 新生代 GC(Minor GC):指发生在新生代的垃圾收集动作,因为 Java 对象大多都具
备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。 - 老年代 GC(Major GC / Full GC):指发生在老年代的 GC,出现了 Major GC,经常
会伴随至少一次的 Minor GC(但非绝对的,在 ParallelScavenge 收集器的收集策略里
就有直接进行 Major GC 的策略选择过程) 。MajorGC 的速度一般会比 Minor GC 慢 10
倍以上。
换句话说,新生代会被频繁回收牺牲的代价小(minor GC),老年代回收次数少牺牲的代价搞(full gc)。永久代一般不会被回收(Spring,Hibernate这类喜欢AOP动态生成类的框架需要更多的持久代内存。一般情况下,持久代是不会进行GC的,除非通过-XX:+CMSClassUnloadingEnabled -XX:+CMSPermGenSweepingEnabled进行强制设置。)
2 对象的生命周期
不同的垃圾回收器,对象的生命周期不同。我说说我认识到的通用的。
当我们启动一个本地线程的时候,会在堆中申请空间,因为堆是共享的所以肯定有锁定机制,jvm会优先在新生代创建一个TLAB(Thread local allocation buffer)区域,这时不需要加锁,减少了系统开销。这时新生代的对象经过一次GC以后,就会进入幸存代。在幸存代中,每经过一次GC该对象的年龄就会+1 ,当增加到15岁以后(少女的话都可以嫁人了,当然了这个年龄可以通过参数设置需要时再google),那么进入老年代。所以生命越长的对象将进入老年代,减少回收的次数。
上面只是一种生命周期,还有一中对象叫大对象(默认值我忘记了3或4个M吧。该值不同的虚拟机配置方式不同),一旦超过这个默认值对象将直接进入老年代。想想我们短暂使用一个10M的数组,那么它将长期站着茅坑不XX ,直到执行FULL GC。
3垃圾回收的算法:
- 复制(copying):将堆内分成两个相同空间,从根(ThreadLocal的对象,静态对象)开始访问每一个关联的活跃对象,将空间A的活跃对象全部复制到空间B,然后一次性回收整个空间A。
因为只访问活跃对象,将所有活动对象复制走之后就清空整个空间,不用去访问死对象,所以遍历空间的成本较小,但需要巨大的复制成本和较多的内存。可参考如下的示例图:
- 标记-清除(mark-sweep):收集器先从根开始访问所有活跃对象,标记为活跃对象。然后再遍历一次整个内存区域,把所有没有标记活跃的对象进行回收处理。该算法遍历整个空间的成本较大暂停时间随空间大小线性增大,而且整理后堆里的碎片很多。可参考如下的示例图:
- 标记整理(mark-sweep-compact):综合了上述两者的做法和优点,先标记活跃对象,然后将其合并成较大的内存块。可参考如下的示例图:
在分代的垃圾回收中新生代(量小,频繁)一般采用复制的算法进行清理。而老年代(量大,被回收的对象几率小)中一般采用标记整理。
4 垃圾回收器(有时间再整理)
垃圾回收器有7种:(JDK 6 有六种 没有G1)
太多了,每一个说一句
- Serial(串行GC)收集器 单线程 终止一切活动 专门回收内存。
- ParNew(并行GC)收集器 多线程 其他和串行GC相同。
- Parallel Scavenge(并行回收GC)收集器 复制算法回收内存。 关注点:控制吞吐量
吞吐量= 程序运行时间/(程序运行时间 + 垃圾收集时间),虚拟机总共运行了100分钟。其中垃圾收集花掉1分钟,那吞吐量就是99%。 - Serial Old(串行GC)收集器 单线程使用标记整理法回收内存。
- Parallel Old(并行GC)收集器 多线程使用标记整理法回收内存
- CMS(并发GC)收集器 标记清除算法。关注点:最少回收中断时间。这个太复杂,拷贝个别人的。
- G1收集器:JDK7产物,这个牛逼了 直接标记整理,收集整个堆。
CMS收集器的优点:并发收集、低停顿,但是CMS还远远达不到完美,器主要有三个显著缺点:
CMS收集器对CPU资源非常敏感。在并发阶段,虽然不会导致用户线程停顿,但是会占用CPU资源而导致引用程序变慢,总吞吐量下降。CMS默认启动的回收线程数是:(CPU数量+3) / 4。
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”失败,性能反而降低。
最后一个缺点,CMS是基于“标记-清除”算法实现的收集器,使用“标记-清除”算法收集后,会产生大量碎片。空间碎片太多时,将会给对象分配带来很多麻烦,比如说大对象,内存空间找不到连续的空间来分配不得不提前触发一次Full GC。为了解决这个问题,CMS收集器提供了一个-XX:UseCMSCompactAtFullCollection开关参数,用于在Full GC之后增加一个碎片整理过程,还可通过-XX:CMSFullGCBeforeCompaction参数设置执行多少次不压缩的Full GC之后,跟着来一次碎片整理过程。
以上 我们可知通过配置垃圾回收器的线程,吞吐量,对象成长方式等参数进行调整垃圾回收器。参数网上一大堆。用时再找。
个人观点:知道参数有什么用?知道找啥才能知道用啥参数。