垃圾收集GC Garbage Collection
垃圾收集器主要关注
堆内存分配与回收,因为我们只有在程序运行期间才知道会创建哪些对象,这部分内存分配和回收是动态的。
JVM堆内存分为2块 :
Permanent Space和Heap Space
Permanent即持久代,主要存放java类定义信息,
与垃圾回收器要收集的java对象关系不大。用于存放静态类型数据,如 Java Class, Method 等。
但是有些应用可能动态生成或者调用一些Class,这时候需要设置一个比较大的持久代空间来存放这些运行中动态增加的类型。
1、垃圾收集器在对堆进行回收前,先确定有哪些对象还“活着”,哪些已经“死去”
如何确定对象是否还“活着”或者“死去”?
引用计数算法
给对象添加一个引用计数器,每当有一个地方引用它时,计数器就加1;当引用失效时,计数器值减1;任何时刻计数器都为0的对象不可能再被使用。
根搜索算法
通过一系列的名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连,则证明此对象不可用。
其中,可以作为GC Roots的对象包括以下几点:
虚拟机栈中的引用的对象
方法区中的静态属性引用的对象
方法区中的常量引用的对象
本地方法栈中JNI的引用的对象
引用的分类(由强到弱):强引用,软引用,弱引用,虚引用
强引用:类似于Object obj = new Object()
软引用:描述一些还有用,但非必须的对象
弱引用:描述非必须对象,但比软引用更弱一点
虚引用:一个对象是否由虚引用,完全不会对其生存时间产生影响,设置虚引用的唯一目的在于在这个对象被GC回收时设置一个系统通知。
2、在根搜索算法中不可达的对象,这时处于“缓刑”阶段,若要真正标记该对象死亡,必须经历两次标记过程:
(1)若发现没有与GC Roots相连接的引用链,则该对象会第一次被标记,并进行筛选,看其是否有必要执行finalize()方法。若对象没有覆盖finalize()方法,或finalize()方法已经被虚拟机调用过,则虚拟机将这两种情况视为“没有必要执行”
(2)若该对象被判定为有必要执行finalize(),则该对象会被放置在一个名为F-Queue的队列中,若对象在finalize()重新与引用链上的任何一个对象建立关联,则该对象将被移除“即将回收的集合”,否则该对象会被回收。
3、回收方法区
4、垃圾收集算法
标记-清除算法
首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。
缺点:效率问题:标记和清除过程的效率不高
空间问题:标记清除后会产生大量不连续的内存碎片,如果空间碎片太多,可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够连续内存而不得不提前触发另一次垃圾收集动作。
复制算法
为了解决效率问题,提出了复制算法。
它将可用内存按容量大小分为相等的两块,每次只使用其中一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上,再将已经使用过得内存空间一次清理掉。
缺点:在存活率较高时需要执行较多的复制操作,效率会遍地。
会浪费50%的空间
将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中的一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性拷贝到另一块Survivor空间上。最后清理掉Eden和刚刚使用过的Survivor空间。
标记-整理算法
前面的步骤和标记-清除算法一致,但是不直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。
分代收集算法
根据对象的存活周期不同将内存划分为几块,一般将Java堆分为
新生代和老年代。
在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用
复制算法,只要复制少量存活的对象可以减少复制成本。
在老年代中,由于对象的存活率高,没有额外空间对它进行分配担保,就必须使用
“标记-清理”或者“标记-整理”算法来进行回收。
5、垃圾收集器
Serial收集器
单线程的收集器,但是它的单线程并不仅仅说明它只会使用一个CPU或者一条收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停其他所有的工作线程(称之为“stop the world”)。
优点:简单而高效
ParNew收集器
是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其他行为和Serial一样。
概念介绍:
并行:指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
并发:指用户线程和垃圾收集线程同时执行(但是不一定是并行,可能会交替执行),用户程序会继续运行,而垃圾收集程序运行于另一个CPU上。
Parallel Scavenge收集器
使用
复制算法的收集器,又是并行的多线程收集器。
特点:Parallel Scavenge收集器的目的在于达到一个可控制的
吞吐量。
自适应调节策略:参数-XX:+UseAdaptiveSizePolicy打开后,虚拟机会根据当前系统的运行情况,动态调节新生代大小(-Xmm),Eden,Survivor的比例,以获得最合适的停顿时间或最大吞吐量。
吞吐量:CPU用于运行用户代码的时间与CPU总消耗时间的比值,
即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
Serial Old收集器
同样是单线程收集器,使用“
标记-整理”算法。
Parallel Old收集器
是Parallel Scavenge收集器的老年代版本。
在注重吞吐量及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。
CMS收集器
是一种以获取最短回收停顿时间为目标的收集器。
基于
标记-清除算法实现
运作过程主要分为以下四个步骤
初始标记
并发标记
重新标记
并发清除
优点:并发收集,低停顿。
G1收集器
当前收集器技术发展的最前沿成果。
基于“标记-整理”算法实现(不会产生空间碎片),可以精确的控制停顿
可以实现在基本不牺牲吞吐量的前提下完成地停顿的内存回收
G1将整个Java堆(包括新生代,老年代)划分为多个大小固定的独立区域,并且跟踪这些区域里的垃圾堆积程度,在后台维护一个优先列表。
6、内存分配与回收策略
新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多数具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也较快。
老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的 Minor GC。MajorGC的速度一般会比Minor GC慢10倍以上。
对象
优先在新生代Eden区中分配,当Eden中没有足够的空间进行分配时,虚拟机将发起一次Minor GC。
大对象直接进入老年代,所谓大对象,即指需要大量连续内存空间的Java对象,例如很长的字符串及数组,-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代中分配。(这样做的目的在于能够避免Eden区和两个Survivor区之间发生大量的内存拷贝)。
长期存活的对象将进入老年代,虚拟机给每个对象定义一个对象年龄计数器,若对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳,将其移动到Survivor空间中,并将其年龄设为1,对象在Survivor区中每熬过一次Minor GC,年龄就加1,当它的年龄增加到一定程度(默认为15岁),就会进入老年代。通过-XX:MaxTenuringThreshold来设置。
Minor GC的触发条件
当Eden区空间满时,将会触发一次Minor GC
Full GC的触发条件
1、调用System.gc()
即建议JVM进行Full GC
2、老年代空间不足
大对象直接进入老年代,长期存活的对象进入老年代
3、空间分配担保失败
使用复制算法的Minor GC需要老年代的内存空间作担保,如果出现了HandlePromotionFailure担保失败,则会触发Full GC
4、JDK1.7及以前的永久带空间不足
在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 class 的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么 JVM 会抛出 java.lang.OutOfMemoryError,为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。
5、Concurrent Mode Failure
执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(有时候“空间不足”是 CMS GC 时当前的浮动垃圾过多导致暂时性的空间不足触发 Full GC),便会报 Concurrent Mode Failure 错误,并触发 Full GC。