0%

JVM内存回收

1.判断对象是否存活

1.1引用计数法
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加2;当引用失效时,计数器值减1。但是这种计算方法很难解决对象互相循环引用的问题。

1.2 根搜索算法
通过一系列的GC Roots对象作为起始点,通过这个节点向下搜索,如果一个对象没有一条从GC Roots到该对象的路径,则不可达,即可被回收。

1.3 引用
强引用:永远不会被回收的引用对象 Object o = new Object()
软引用:在OOM之前会把这些对象作为垃圾对象进行回收,如果还没有足够的内存,则OOM,通过SoftReference类来实现
弱引用: 存活到下一次垃圾收集发生之前WeakReference实现
虚引用:最弱的,唯一目的是在对象被回收时收到一个系统通知。 PlantomReference实现。

当一个对象要被回收时需要进行两次标记,经历两次标记后的对象才会被回收,第一次就是GC roots不可达的的,第二次是调用finalize()后还没被拯救的。

2. 垃圾收集算法

2.1 标记-清除

标记—清除算法是最基础的收集算法,它分为“标记”和“清除”两个阶段:首先标记出所需回收的对象,在标记完成后统一回收掉所有被标记的对象,它的标记过程其实就是前面的根搜索算法中判定垃圾对象的标记过程。标记—清除算法的执行情况如下图所示:
图片加载流程图

该算法有如下缺点:
标记和清除过程的效率都不高。
标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不触发另一次垃圾收集动作。

2.2 复制算法

它将内存按容量分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活着的对象复制到另外一块内存上面,然后再把已使用过的内存空间一次清理掉。复制算法有如下优点:
每次只对一块内存进行回收,运行高效。
只需移动栈顶指针,按顺序分配内存即可,实现简单。
内存回收时不用考虑内存碎片的出现。
它的缺点是:可一次性分配的最大内存缩小了一半。
对新生代的回收采用此方法,一般不是采用1:1的比例,而是将其化为y一个 Eden和2个Survivor区,Eden:Survivor为8:1 ,每次只使用Eden区和1个Survivor,将活着的放到剩下的Survivor区中。
复制算法的执行情况如下图所示:
图片加载流程图

2.3 标记-整理

复制算法比较适合于新生代,在老年代中,对象存活率比较高,如果执行较多的复制操作,效率将会变低,所以老年代一般会选用其他算法,如标记—整理算法。该算法标记的过程与标记—清除算法中的标记过程一样,但对标记后出的垃圾对象的处理情况有所不同,它不是直接对可回收对象进行清理,而是让所有的对象都向一端移动,然后直接清理掉端边界以外的内存。
标记—整理算法的回收情况如下所示:
图片加载流程图

2.4 分代收集

当前商业虚拟机的垃圾收集 都采用分代收集,它根据对象的存活周期的不同将内存划分为几块,一般是把Java堆分为新生代和老年代。在新生代中,每次垃圾收集时都会发现有大量对象死去,只有少量存活,因此可选用复制算法来完成收集,而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记—清除算法或标记—整理算法来进行回收。

3 垃圾回收器

在java虚拟机中,没有最好的垃圾收集器,只有适合的垃圾收集,所以垃圾收集通过多种垃圾收集器合作完成。
图片加载流程图

3.1 Serial收集器

新生代垃圾收集器,单一线程,垃圾收集时Stop The World,复制算法。

3.2 Serial Old收集器

老年代垃圾收集器,单一线程,垃圾收集时Stop The World,标记整体算法。

两种Serial收集器工作流程图:
图片加载流程图

3.3 ParNew收集器

新生代垃圾收集器,多线程,垃圾收集时Stop The World,复制算法。

3.3 ParNew Old收集器

老年代垃圾收集器,多线程,垃圾收集时Stop The World,标记整体算法。

3.4 Parallel Scanvenge 收集器

新生代收集器,复制算法,多线程执行,垃圾收集时Stop The World,关注吞吐量(用户代码时间/虚拟机运行总时间)。
高吞吐量可以高效的利用Cpu时间,尽快的完成程序的运算任务,适合在后台运算而不需要太多交互的任务。

3.5 CMS收集算法

老年代收集器,标记-清除算法,关注最短收集停顿时间。
分为四个步骤:初始标记,并发标记,重新标记,并发清除。
初始标记、重新标记两个步骤Stop The World,其余步骤都是可以和用户线程并发进行。
图片加载流程图

缺点:
对CPU资源敏感。
无法处理浮动垃圾。
产生空间碎片。

3.6 G1收集器

标记整理算法,精确的控制停顿时间。G1强整个Java堆划分为多个固定的独立却与,并且跟踪这些区域里面的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域。

4. 内存分配和回收策略

4.1对象优先分配在Eden

大多数情况下,对象在新生代Eden区中分配,当Eden区没有足够的空间进行分配吗虚拟机则发起一次young gc。

4.2 大对象直接进入老年代

需要大量连续的内存空间的Java对象,比如长字符串和数组。目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝。

4.3 长期存活的对象进入老年代

每一个对象都有一个对象年龄计数器。如果对象在Eden出生并经过第一次young gc后仍然存活,并且能被Survivor容纳,将被移动到Survivor区,并将对象年龄设为1。对象在Survivor区没熬过一次young gc年龄增加一岁,当年龄增加到一定的程度将会移动到老年代中。

4.4 动态对象年龄判断

如果在Survivor空间中相同年龄所有对象大小总和大于Survivor空间的一半,年龄大于或等于该年龄的所有对象就可以直接进入老年代。

4.5 空间分配担保

在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。
新生代使用复制收集算法,但为了内存利用率,只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代。与生活中的贷款担保类似,老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,一共有多少对象会活下来在实际完成内存回收之前是无法明确知道的,所以只好取之前每一次回收晋升到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多空间。
取平均值进行比较其实仍然是一种动态概率的手段,也就是说,如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然会导致担保失败(Handle Promotion Failure)。如果出现了HandlePromotionFailure失败,那就只好在失败后重新发起一次Full GC。虽然担保失败时绕的圈子是最大的,但大部分情况下都还是会将HandlePromotionFailure开关打开,避免Full GC过于频繁。