深入了解JVM之垃圾回收(二)

一、前言

    为了深化知识体系的建立,笔者将采用提问的方式展开论述,欲通过一个个不断深入的问题强化知识点之间的联系。

二、问题
1、哪些内存需要回收?

    引用计数算法:
    为每一个对象创建一个引用计数器,当其他对象引用该对象,引用计数器的值就加一,当引用失效时,引用计数器的值就减一。如果引用计数器的值为0,则说明该对象可以被回收。
    这种算法的弊端是无法解决两个对象互相引用的问题。
    根搜索算法(目前使用):
    通过一个叫做“GC Roots”的对象作为起始点,向下开始搜索,经过的路径称为引用链。如果一个对象与GC Roots之间没有任何引用链相连,则认为该对象不可达,可以回收。

2、在根搜索算法中,什么对象可以成为GC Roots?

    虚拟机栈局部变量表引用的对象
    本地方法栈native方法引用的对象
    方法区中的类静态变量引用的对象
    方法区中的常量引用的对象

3、在根搜索算法中,如何确定对象死亡?

    确定对象死亡至少需要经历两次标记。
    当对象与GC Roots之间没有任何引用链(也就是对象不可达)时,会进行第一次标记。标记完会进行一次筛选,如果该对象重写了finalize方法或者没有执行过finalize方法,那么就会将该对象加入F-Queue队列,稍后JVM会分配一个低优先级的Finalizer线程去异步执行队列中所有对象的finalize方法。如果是同步执行,只要其中一个对象的finalize方法阻塞了,就会影响其他对象的finalize方法的执行,以至于其他对象不能进行回收。
    稍后,JVM会在F-Queue队列中进行小规模的第二次标记

4、方法区需要回收对象吗?回收什么对象?这个对象需要具备什么条件?

    需要。虽然方法区回收对象的效率低,但是不回收对象可能会引起OOM问题。
    方法区回收的对象是无用的常量和类。无用常量指的是如果一个常量不被任何地方引用,那么这个常量就可以回收。无用类的判断就相对复杂一点,需要满足下列3个条件:
    1)该类的所有实例均已回收。
    2)加载该类的ClassLoader已回收。
    3)该类对应Class对象没有任何地方被引用。
    达到了这三个条件,说明类可以回收了。是否回收需要根据JVM是否设置了-Xnoclassgc参数来判断。

5、什么时候触发垃圾回收?

    创建一个新对象,优先在Eden区分配内存。如果Eden区没有足够大的空间分配内存,这个时候会触发Minor GC。Minor GC将Eden区和From Survivor区的内存进行垃圾回收,将存活的对象复制到To Survivor区,清空Eden和From Survivor区,这就完成了一次Minor GC。From Survivor区和To Survivor区是相对的关系,哪个区中有对象,哪个区就是From Survivor区。
    当Survior区的剩余空间不足以存放新对象时,JVM会进行分配担保,新对象直接晋升到老年代。当老年代的剩余空间不足以存放晋升到老年代的对象时,会进行一次Major GC(Full GC)。这个时候有人就会问了,JVM怎么知道这次晋升到老年代的对象需要多少空间?是的,它不知道,所以它只能取之前每一次晋升到老年代的对象容量的平均大小值作为参考。
    Major GC的效率要比Minor GC慢十倍以上,所以要尽力避免Major GC。

6、什么情况下新生代的对象会晋升到老年代?

    1)Survivor区不足以存放新对象时,新对象会通过分配担保晋升到老年代。
    2)大对象直接晋升老年代。 为了避免大对象在Eden区和Survivor区的低效的内存拷贝,JVM设置了一个对象大小阈值(可通过JVM参数设置),只要超过这个阈值的对象,就直接晋升到老年代。
    3)长期存活的对象晋升老年代。 因为新生代和老生代的内存使用情况是不同的,所以虚拟机使用分代收集的思想来回收内存,即针对不同区域使用不同的垃圾回收算法。因为需要知道在内存回收后存活的对象应该放在哪里区域,所以JVM为每个对象设置一个对象年龄计数器。每经过一次Minor GC并且被Survior区容纳,年龄就增加1,当年龄达到一定程度(默认是15岁),该对象就会晋升到老年代。
    4)动态对象年龄判定。 如果Survivor空间中相同年龄的对象大小总和大于总空间的一半,那么大于或者等于这个年龄的对象直接进入老年代,无需达到年龄阈值。

7、为了减少Major GC的触发,JVM做哪些优化?

    默认开启担保失败。在发生Minor GC时,JVM会检测当老年代的剩余空间是否小于之前每一次晋升到老年代的对象容量的平均大小。如果小于,则进行一次Major GC。如果大于,则要看是否开启了担保失败,如果开启了,就只会进行Minor GC。否则,还是会进行Full GC。

8、垃圾收集算法有哪些?

    标记-清除算法:
    采用之前提到的根搜索算法进行标记,之后统一回收被标记的对象。这个垃圾收集算法是最基础的一个,往后的垃圾收集算法很多是基于这个进行改进的。
    这个算法有两个缺点:一是效率问题,标记和回收的效率都不高;二是空间问题,清除后产生大量的不连续的空间碎片,空间碎片的增多会使得可分配的连续的空间减少,这样就加快下一次GC。
    复制算法:
    将内存空间分为容量大小相等的两块,一次只使用其中的一块。当正在使用的内存空间即将用完时,会触发一次GC。JVM会将已标记的对象复制到另一块未使用的内存空间,再把已使用的空间清理掉。
    实际上,新生代大部分是朝生夕死的,GC后仍存活的对象只占少部分,如果按照1:1去划分内存空间,那将会是极大的浪费。正因为这个原因,JVM将内存划分为一块较大的Eden空间和两块较小的Survivor空间,每次只使用Eden空间和其中一块Survivor空间。当回收时,将Eden空间和其中一块Survivor空间存活的对象复制到另一块Survivor空间上,并清除Eden空间和之前的Survivor空间。
    万一复制的时候另一块Survivor空间的内存空间不足以存放存活的对象,那怎么办?别担心,JVM还提供了分配担保的机制,可以把放不下的对象存到老年代中,相当于向老年代“借”了点空间。
    现在商用虚拟机都采用这个算法来回收新生代。
    标记-整理算法:
    对于老年代,大部分对象都是很难回收的,如果采用复制算法,那么绝大部分的对象都需要进行复制,性能会下降很多。这个时候就有人提出了标记-整理算法——回收时让所以已存活对象移动到同一端,再清理掉端界外的对象。
    从JVM中对象访问定位两种方式一文中可以推断出,移动对象的开销是要比复制对象小很多的,所以对于老年代,标记-整理算法比复制算法的效率要高得多。
    分代收集算法:
    根据各个年代的特点采用适当的垃圾收集算法。新生代采用复制算法,老年代使用标记-整理算法或标记-清除算法。

9、垃圾收集器有哪些?各有什么特点?

    Serial收集器(新生代,复制算法):
    最基本、历史最悠久的单线程收集器。因为是单线程,所以不能一边运行工作线程,一边进行垃圾回收,在垃圾回收的时候必须暂停其他所有的工作线程,这种现象就叫做“Stop The World”。
    它是虚拟机运行在Client模式下的默认收集器,简单而高效。在用户的桌面应用场景中,分配给虚拟机管理的内存一般不会太大,收集几十兆甚至两百兆的新生代,停顿时间完全可以控制在几十毫秒最多一百多毫秒之内,只要不是频繁发生,这点停顿是可以接受的。
    ParNew收集器(新生代,复制算法):
    ParNew收集器是Serial收集器的多线程版本。相对于Serial收集器,在单线程的环境下,性能没有Serial收集器好,但是随着CPU数量的增加,多线程带来的性能提升会愈发明显。
    达到一个可控制的吞吐量
    Parallel Scavenger 收集器(新生代,复制算法,并行):
    Parallel Scavenger 收集器的目标是达到一个可控制的吞吐量吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。
    提供了两个参数来控制吞吐量,分别是控制最大垃圾收集时间和-XX:MacGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。最大垃圾收集时间不应该设置得过于小,因为GC停顿时间的缩短是以牺牲吞吐量和新生代空间来换取的。
    开启-XX:+UseAdaptiveSizePolicy参数,则开启了GC的自适应调节策略——虚拟机根据当前程序的运行情况收集性能监控信息,动态调整晋升年龄、Eden区和Survivor区比例等参数以提供最合适的停顿时间或最大吞吐量。
    Serial Old收集器(老年代,标记-整理算法):
    Serial Old收集器是一个单线程收集器。这个收集器的主要意义是被Client模式下的虚拟机使用。如果在Server模式下,它有两个用途:
    一是在JDK1.5及之前版本与Parallel Scavenger 收集器搭配使用。
    二是作为CMS收集器的后备预案
    Parallel Old收集器(老年代,标记-整理算法):
    Parallel Scavenger 收集器老年代的版本。JDK1.6中才开始提供,在此之前Parallel Scavenger 收集器的处境会比较尴尬——能与它匹配的老年代收集器只有Serial Old。在多CPU等硬件比较高级的环境中,Parallel Scavenger+Serial Old的组合甚至比不上ParNew+CMS的组合。
    CMS收集器(老年代,标记-清除算法):
    全称是Concurrent Mak Sweep,目的是获取最短回收停顿时间。它的运作过程复杂一点,流程如下:
在这里插入图片描述
    CMS收集器的优点是并发收集、低停顿,它的缺点主要有三点:
    一是CMS收集器对CPU资源比较敏感。在并发阶段,会占用一部分CPU资源(线程),从而导致程序变慢。
    二是无法处理浮动垃圾。所谓的浮动垃圾指的是并发清除过程中产生的垃圾。因为浮动垃圾的存在,JVM不能等到老年代会满了才进行GC,需要预留一定的空间,默认是68%会触发GC,这个数值是可以通过参数修改的。如果CMS运行期间预留的内存无法满足,就会出现一次"Concurrent Mode Fail"失败,这个时候JVM会启动Serial Old收集器重新进行GC。
    三是标记-清除算法带来的空间碎片问题——GC后产生大量的不连续的空间碎片。为了解决这个问题,JVM提供XX:UseCMSCompactAtFullCollection参数,作用是在Full GC后进行一次碎片整理。
    G1收集器(老年代,标记-整理算法):
    JDK1.6开始提供使用。它有两个显著的优点:
    一是标记-整理算法不会产生内存碎片。
    二是它可以非常精确地控制停顿,能让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集的时间不超过N毫秒。
    G1收集器可以实现在基本不牺牲吞吐量的情况下完成低停顿的内存回收,这是由于它极力的避免全区域的回收,G1收集器将Java堆(包括新生代和老年代)划分为多个大小固定的独立区域(Region),并且追踪这些区域的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的时间,优先回收垃圾最多的区域 。(这就是Garbage First名称的来由)

10、如何根据业务场景选择合适的垃圾收集器?

    引用Java——七种垃圾收集器+JDK11最新ZGC文章的一张图解答:在这里插入图片描述

三、参考

《深入了解Java虚拟机:JVM高级特性与最佳实践》书籍
图解 JVM GC 过程
JVM中对象访问定位两种方式
Java——七种垃圾收集器+JDK11最新ZGC

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值