程序员登高之路——JAVA篇——2.JVM的垃圾回收

如何判断对象死亡?

目前主流的判断对象死亡的方法有两种:

1.引用计数法:

      每个对象对象包含一个引用计数器,每当对象被引用,引用计数器便加一,引用失效就减一。当对象的引用计数器为0时,则表示对象可被回收。此方法无法解决解决对象循环引用的情况,如:

// 产生循环引用的代码
 A objectA = new A();
 B objectB = new B();
 A.b = objectB;
 B.a = objectA;

      若采用引用计数法,对象A和B的引用计数器值永远不会小于1,那么就产生了内存泄漏。(据说Python使用的就是引用计数法,关于如何解决的循环引用问题,感兴趣的朋友可以去查一查)。

2.可达性分析算法:

      通过一系列可以被看做GCRoots的根节点出发,向下搜索,构成引用链,未在引用链之内的则会被视为可回收对象。

GCRoots:

(1)虚拟机栈中的引用
(2)方法区中的静态变量
(3)方法区中的常量对象
(4)本地虚拟机栈中的引用
除此之外还有synchronized持有的对象,minjorGC时存在跨代引用的老年代对象等。

垃圾回收算法

      上面说了如何判断对象可否回收,接下来说一说JVM如何回收这些对象。

1.标记清除法:

      首先扫描并标记出对象是否需要清除,扫描完成后一次清除需要清除的对象。
在这里插入图片描述

      上图红色方块表示垃圾,绿色方块表示存活对象,左图为垃圾清除之前的内存,右边为垃圾清除之后的内存。标记清除算法的缺点就是会产生大量的内存碎片,比如在垃圾回收之后,系统需要创建一个占4个小格的对象,此时内存内剩余空间明明大于4,系统却只会报内存溢出的异常。

2.复制算法:

      复制算法将内存分为了两部分,每次仅使用一部分,当使用那部分满了的时候,就会将所有存活的对象移到另一个区域。
在这里插入图片描述      上图可以看出,在经过复制算法之后,所有存活的对象都被移到了另一半内存中,之后清空了之前使用的内存区。复制算法的弊端也很容易看出来,就是虚拟机每次仅能使用一半的内存,对于"寸土寸金"的RAM来说,这真是用着肉疼。

3.标记整理算法:

      标记整理算法会在判断完垃圾之后,将存活的对象向一侧移动,之后清除掉剩余的内存。
在这里插入图片描述

      在上图中,存活对象都像左侧移动,移动后需要占用7格内存,最后将边界外(7格之后)的内存全部清除。与标记清除算法相比,它不会产生内存碎片,与复制算法相比,它不会浪费内存。不过移动对象的花费仍然无法避免。

JVM中的垃圾收集器

上面的三种垃圾回收算法,并没有最优解,只是各自适用于不同场景,为此JVM也实现了各种垃圾收集器。

1.Serial

新生代垃圾收集器,采用复制算法。
特点:单线程垃圾收集器,在垃圾收集的时候会停止所有其他的用户线程。

2.SerialOld

老年代垃圾收集器,采用标记整理算法。可以看作老年代版本的Serial,垃圾收集的时候也会停止所有其他用户线程

3.ParNew

新生代垃圾收集器可以看作多线程版的Serial收集器,垃圾回收的时候也会停止其他所有用户线程。默认线程数与CPU数相等,所以在单核CPU的情况下甚至可以看成Serial。

4.Parallel Scavenge

新生代垃圾收集器,采用复制算法,垃圾清除时会停止其他用户线程,存在的目的是为了控制垃圾收集的吞吐量

5.Parallel Old

老年代垃圾收集器,采用标记整理算法,垃圾清除时会停止其他用户线程,存在的目的是为了控制垃圾收集的吞吐量。

6.CMS *******

老年代垃圾收集器,采用标记清除。减少了垃圾回收时停止用户线程的时间。CMS将标记分为了三步:
1.初始标记:单线程标记所有GCRoots直接指向的对象,因为不会涉及到可达树的遍历,所以非常快,此时会停止其他用户线程。
2.并发标记:与其他用户线程一起执行,根据步骤一获取的结果遍历可达树并标记。
3.重新标记:步骤2时,系统可能会产生新的对象,这一步的目的就是标记这些新产生的对象,此时会停止其他线程。
4.清除垃圾:与其他用户线程一起执行。

可以看出CMS就是将最费时间的全局可达树遍历与用户线程一起执行,从而减少用户线程暂停时间,不过缺点也显而易见:CPU敏感,会产生浮动垃圾,有内存碎片。

7.G1 *******

G1收集器与以往的垃圾收集器不同,他并没有直接将堆区分成了新生代老年代,而是将堆划分成了一个一个的内存块,内存块可能是新生代,也可能是老年代。并且额外增加了humongous用来保存大对象。这样做的好处是新生代与老年代的大小不再固定,并且若某一内存块很多对象需要进入老年代,直接将内存块标记为老年代即可,减少了对象复制的开销。
算法:新生代采用复制算法,老年代采用标记整理算法。
特点:可以设置最大暂停时间,每次GC会选择暂停时间左右效率最高的内存区域。

步骤:
1.初始标记:单线程标记所有GCRoots直接指向的对象,因为不会涉及到可达树的遍历,所以非常快,此时会停止其他用户线程。
2.并发标记:与其他用户线程一起执行,标记1可达的对象。同时标记并发标记中产生的对象(认为不是垃圾),划一块内存区域,用来保存并发标记产生对象的指针。
3.多线程最终标记:停止其他用户线程
4.多线程并发清除:停止其他用户线程

CMS与G1的选择

G1优点较多,但CMS不一定不G1差,例如G1为了存放并发标记产生的对象需要占用内存进行存放。

并发标记中的三色标记

黑色:根对象,以及当前对象和子对象都标记完成,或者没有子对象,则当前对象为黑色,表示扫描完成且不会被GC
灰色:扫描完当前对象,但仍有子对象没有进行标记,
白色:所有对象初始为白色,扫描后仍为白色表示对象没有被可达,可以回收。

如何解决并发标记的漏标问题?

场景:再并发标记时,线程A已经完成了标记,线程B仍在标记。此时用户线程将B正在标记的可达树下B未标记的对象置为null,并又将此对象的指针交给了A扫描过的对象。此时因线程A已经扫描结束,线程B扫描不到这个对象,那这个不应该回收的对象就会被回收。

CMS解决办法:增量更新,若发现一个白色对象被黑色对象引用,则将黑色对象置为灰,垃圾回收器发现节点为灰,则会从头再次扫描。

G1解决办法:STAB(快照),在并发标记前拍一个快照信息,若在标记的时候发先有一个引用消失了,则将快照信息推送到GC的堆栈内,则快照内的引用还会存在。

特点:G1的解决方式会产生更多的浮动垃圾,不过不需要像CMS一样重新扫描。.

安全点和安全区域

无论什么垃圾收集器,都会出现需要暂停用户线程的时间段,为了让程序正确运行,用户线程只有运行到安全点,才会被暂停。安全点包括:
1.方法调用前
2.方法返回后
3.循环末尾
4.抛出异常未知。
安全区域:若线程进入了sleep或blocked,此时线程无法进入安全点,则认为此时线程处于安全区域,处于安全区域的线程,只有再用户线程的暂停结束后,才能继续执行。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值