【不失业计划】JVM Part3—垃圾回收

三、垃圾回收
1、判断算法
(1)引用计数算法

​ 给每个对象添加一个计数器,当有地方引用该对象时计数器加1,当引用失效时计数器减1。用对象计数器是否为0来判断对象是否可被回收。

​ 引用计数算法的实现简单,判断效率也很高,大部分情况下是一个不错的算法。但是Java并没有选用引用计数算法来管理内存,主要的原因是它很难解决对象之间相互循环引用的问题

public class ReferrenceCountingGC{
    public Object instance = null;
    public static void testGC(){
        ReferrenceCountingGC objA = new ReferrenceCountingGC();
        ReferrenceCountingGC objB = new ReferrenceCountingGC();
        objA.instance = objB;
        objB.instance = objA;
        objA = null;
        objB = null;
        System.GC();
    }
}
//两个对象无任何引用,实际上两个对象都不能再被访问,但是他们互相引用着对方,导致他们的引用计数都不为0,于是引用计数算法无法通知GC收集器回收他们。
(2)根搜索(可达性分析)算法

​ 通过一系列的名为GC ROOTS 的对象作为起始点,从这些节点开始向下搜索,搜所走过的路径称为引用链 ,当一个对象到GC ROOTS没有任何引用链相连(图论就是从GC ROOTS到该对象不可达)时,则证明此对象是不可用的。

​ 成功解决了引用计数所无法解决的循环依赖问题,只要你无法与GC Root建立直接或间接的连接,系统就会判定你为可回收对象。

  • Java内存区域中可以作为GC ROOTS的对象
    • 虚拟机栈中引用的对象
    • 方法区中类静态属性引用的对象
    • 方法区中的常量引用的对象
    • 本地方法栈中JNI(native方法)的引用的对象
(3)引用
  • jdk1.2之前

    Java中的引用定义很传统:如果reference类型的数据中存储的数值代表的是另一块内存的起始地址,就称这块内存代表一个引用。对象在此只有两种状态:被引用/没有被引用。

  • jdk1.2之后

    Java对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种,这四种引用依次逐渐减弱。

    • 强引用就是指在程序代码中普遍存在的类似Object obj = new Object() 这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象

    • 软引用用来描述一些还有用,但非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出之前,将会把这些对象列进回收范围内并进行第二次回收,如果这次回收还是没有足够的内存,才会抛出内存溢出的异常。SoftReference<Object> obj = new SoftReference<Object>(new Object());

    • 弱引用也是用来描述非必需对象的,但是它比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前,当垃圾收集器开始工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。WeakReference<Object> obj = new WeakReference<Object>(new Object());

    • 虚引用也称为幽灵引用或者幻影引用,是最弱的一种引用关系,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是希望能在这个对象被收集器回收时收到一个系统通知。PhantomReference<Object> obj = new PhantomReference<Object>(new Object());

(4)finalize()

​ 在根搜索算法中不可达的对象,也并非”非死不可“的,这时候他们暂时处于”缓刑“阶段,真正要宣告一个对象死亡,至少要经历两次标记过程:

第一次标记:如果对象在根搜索之后发现没有与GC Roots相连接的引用链,会被第一次标记并筛选,筛选条件是判断当前对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法 或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为”没有必要执行“。

第二次标记:当这个对象被判定为有必要执行finalize()方法,将会把这个对象放置到一个F-Queue队列之中,并在稍后由一条虚拟机自动创建的低优先级的Finalizer线程去执行。之后GC会对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只需要重新与引用链上的任何一个对象建立关联即可,则在第二次标记中则会被移出”即将回收“的集合。

​ finalize()能做的所有工作,使用try-finally或其他方式都能做得更好更及时。

在这里插入图片描述

(5)回收方法区

​ 很多人认为方法区(或者HotSpot虚拟机中的永久代)是没有垃圾回收的,在Java虚拟机规范中确实说过可以不要求虚拟机在方法区实现垃圾回收,而且在方法区进行的垃圾收集”性价比“比较低(一般在新生代进行一次垃圾回收可以回收70%-95%的空间,但是在永久代就远远小于新生代),但是永久代确实是存在垃圾回收的,并且主要回收两个部分内容:废弃常量和无用的类。

2、回收算法
(1)标记-清除算法

​ 分为两个部分,先把内存区域中的这些对象进行标记,哪些属于可回收标记出来,然后把这些垃圾拎出来清理掉。清理掉的垃圾就变成未使用的内存区域,等待被再次使用。

在这里插入图片描述

缺点:等我们回收完,内存就会切成了很多段。我们知道开辟内存空间时,需要的是连续的内存区域,这时候我们需要一个2M的内存区域,其中有2个1M是没法用的。这样就导致,其实我们本身还有这么多的内存的,但却用不了。

(2)复制算法

​ 在标记清除算法基础上演化而来,解决标记清除算法的内存碎片问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。保证了内存的连续可用,内存分配时也就不用考虑内存碎片等复杂情况。

在这里插入图片描述

缺点:内存代价比较大,将现内存缩小为原来的一半,造成很大的资源浪费。

​ 但是在现在的商业虚拟机都采用了这种收集算法来回收新生代,根据研究,新生代中的对象98%是朝生夕死的,所以并不需要按照1:1的比例来划分内存空间,而是将内存划分为一个较大的Eden区和两块较小的Survivor区,每次使用Eden区和一块Servivor区。当回收时,将Eden区和使用的一块Servivor区中还存活的对象一次性地拷贝到另一块Servivor区上,最后清理掉Eden区和使用过的Servivor区,HotSpot虚拟机默认Eden:Servivor是8:1的比例,所以每次只会有10%的内存区域被浪费掉。当Servivor空间不够用时,需要依赖其他内存(老年代)进行分配担保。

(3)标记整理算法

标记-整理算法标记过程仍然与标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,再清理掉端边界以外的内存区域。
在这里插入图片描述

缺点:标记整理算法对内存变动更频繁,需要整理所有存活对象的引用地址,在效率上比复制算法要差很多。

(4)分代收集算法

分代收集算法分代收集算法严格来说并不是一种新的思想或理论,而是融合上述3种基础的算法思想,而产生的针对不同情况所采用不同算法的一套组合拳,根据对象存活周期的不同将内存划分为几块。

  • 在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
  • 在老年代中,因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记-清理算法或者标记-整理算法来进行回收。
3、收集器

在这里插入图片描述

​ 如果两个收集器之间存在连线,就说明他们可以搭配使用。

在介绍各个收集器的特性之前,需要先明确一个点:虽然是在对各个收集器进行比较,但并非为了挑选一个最好的收集器出来。因为直到现在为止还没有最好的收集器出现,没有万能的收集器,所以只是选择对具体应用场景最合适的收集器。

新生代收集器:

(1)Serial
  • 特点

    单线程收集;简单而高效(与其他收集器的单线程相比),对于限定单个CPU环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程效率;使用标记-复制算法。

  • 缺点

    “Stop The World” 在进行垃圾回收时,必须暂停其他所有的工作线程,直到它收集结束。

  • 场景

    Client模式下默认新生代收集器;单核机器;

(2)ParNew
  • 特点

    多线程并行收集;标记-复制算法;1.5时期能与CMS配合使用;随着可以使用的CPU数量增加,对于GC时系统资源的利用有好处;其他有点与Serial相似。

  • 缺点

    在单CPU场景效果不突出

  • 场景

    用户交互;配合CMS垃圾收集器使用。

(3)Parallel Scavenge
  • 特点

    目标在于达到一个可控制的吞吐量(吞吐量=用户代码运行时间/(用户代码运行时间+垃圾收集时间));标记-复制算法。

  • 场景

    高效利用CPU、后台运算且不需要太多交互。

老年代收集器

(4)Serial Old
  • 特点

    Serial的老年代版本;单线程;标记-整理算法。

  • 场景

    1.5之前与ParallelScavenge配合使用;作为CMS的后备预案。

(5)Parallel Old
  • 特点

    标记-整理;多线程。

  • 场景

    为了替代Serial Old与Paeallel Scavenge配合使用

(6)CMS
  • 特点

    以获取最短回收停顿时间为目标;基于标记-清除算法;

  • 步骤

    • 初始标记

      需要“Stop The World”,标记GC Roots直接关联的对象,速度快。

    • 并发标记

      进行GC Roots Tracing 的过程,与用户进程并发工作,耗时长。

    • 重新标记

      也需要“Stop The World”,为了修正并发标记期间,用户程序继续运作导致标记产生变动的那一部分对象的标记记录,停顿时间比初始标记厂,但远小于并发标记。

    • 并发清除

      清除标记的对象。

  • 缺点

    • 对CPU资源非常敏感。面向并发设计的程序都有该通病。
    • 无法处理浮动垃圾,可能出现“Concurrent Mode Failure” 失败而导致另一次Full Gc。
    • 基于标记-清除算法,会产生大量空间碎片,可能提前触发Full GC,但是可以通过参数开启碎片的合并整理。
(7)G1
  • 特点

    当前收集器技术发展的最前沿成果。将整个Java堆划分为多个大小相等的独立区域Region,跟踪每个Region里面的垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。

  • 改进(相比于CMS)

    • G1收集器基于“标记-整理”算法实现,不会产生空间碎片。
    • 可以非常精确的控制停顿,既能让使用者明确知道那个在一个长度M毫秒的时间片段内,消耗垃圾收集器上的时间不超过N毫秒。
4、内存分配与回收策略
(1)对象优先在Eden分配

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

(2)大对象直接进入老年代

所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串及数组。大对象对于虚拟机的内存来说就是一个坏消息,经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来“安置”他们。

(3)长期存活的对象将进入老年代

虚拟机给每个对象定义了一个对象年龄计数器。如果对象在Eden出生并经过了第一次Minor GC后仍然存活且能被Survivor容纳的话,将被移动到Servivor空间中,并将其年龄设为1。对象在Servivor中每熬过一次Minor GC,年龄就加1岁,当他的年龄到达一定程度(默认为15)时,就会被晋升到老年代中。老年代的年龄阈值可以通过参数设置。

(4)动态对象年龄判定

为了更好地适应不同程序的内存状况,虚拟机并不总是要求对象的年龄必须达到MaxTenuringThreshold才能晋升到老年代,如果在Servivor空间中相同的年龄所有对象大小的总和大于Servivor空间的一半,年龄大于等于该年龄的对象就可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。

(5)空间分配担保

当发生Minor GC时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小,如果大于,则改为直接进行一次Full GC。如果小于,则查看HandlePromotionFailure设置是否允许担保失败;如果允许,则只会进行Minor GC;如果不允许,则需要改为进行一次Full GC。

(6)回收策略(MinorGC和FullGC)

新生代GC(Minor GC):指发生在新生代的垃圾回收动作,因为Java对象大多具有朝生夕死的特性,所以MinorGC非常频繁,一般回收速度也非常快。

老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对,在Parallel Scavenge就有直接Major GC策略的选择)。Major GC的速度一般会比Minor GC慢10倍以上。

参考资料:《深入理解Java虚拟机》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值