前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java垃圾回收机制详解(面试题)

Java垃圾回收机制详解(面试题)

作者头像
攻城狮的那点事
发布2019-10-08 16:28:25
2.1K0
发布2019-10-08 16:28:25
举报

Java的垃圾回收,主要是针对JVM内存中的堆空间。

一,对象是否可被回收的判断方法

1,引用计数法

每个对象都有一个引用计数器,当对象被引用一次计数器就加 1;当引用失效时计数器就减 1。当对象的计数器为 0 时,对象就是要被回收的。简单高效,缺点是无法解决对象之间相互循环引用的问题。

2,可达性分析算法

以 Roots节点作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。此算法解决了上述循环引用的问题。

二,垃圾收集算法

1,标记-清除算法

最基础的收集算法是“标记-清除”(Mark-Sweep)算法,分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

它的主要不足有两个:

  1. 效率问题,标记和清除两个过程的效率都不高;
  2. 空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

标记—清除算法的执行过程如下图。

2,复制算法

为了解决效率问题,一种称为“复制”(Copying)的收集算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半。复制算法的执行过程如下图:

现在的商业虚拟机都采用这种算法来回收新生代,IBM 研究指出新生代中的对象 98% 是“朝生夕死”的,所以并不需要按照 1:1 的比例来划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor 。

当回收时,将 Eden 和 Survivor 中还存活着的对象一次性地复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。HotSpot 虚拟机默认 Eden:Survivor = 8:1,也就是每次新生代中可用内存空间为整个新生代容量的 90%(其中一块Survivor不可用),只有 10% 的内存会被“浪费”。

当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于 10% 的对象存活,当 Survivor 空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。

内存的分配担保就好比我们去银行借款,如果我们信誉很好,在 98% 的情况下都能按时偿还,于是银行可能会默认我们下一次也能按时按量地偿还贷款,只需要有一个担保人能保证如果我不能还款时,可以从他的账户扣钱,那银行就认为没有风险了。

内存的分配担保也一样,如果另外一块 Survivor 空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代。关于对新生代进行分配担保的内容,在本章稍后在讲解垃圾收集器执行规则时还会再详细讲解。

3,标记-整理算法

复制算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费 50% 的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都 100% 存活的极端情况,所以在老年代一般不能直接选用这种算法。

根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,“标记-整理”算法的示意图如下:

4,分代收集算法

当前商业虚拟机的垃圾收集都采用“分代收集”(Generational Collection)算法,根据对象存活周期的不同将内存划分为几块并采用不用的垃圾收集算法。

一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收。

三,垃圾收集器

1,Serial收集器

Serial收集器是最基本的、历史最悠久的收集器,曾经是JDK 1.3.1之前虚拟机的新生代收集的唯一选择。Serial这个名字揭示了这是一个单线程的垃圾收集器,特点如下:

  • 仅仅使用一个线程完成垃圾收集工作;
  • 在垃圾收集时必须暂停其他所有的工作线程,知道垃圾收集结束;
  • Stop the World是在用户不可见情况下执行的,会造成某些应用响应变慢;
  • 使用复制算法;

Serial收集器的工作流程如下图:

虽然如此,Serial收集器依然是虚拟机运行在Client模式下的默认新生代收集器。它的优点同样明显:简单而高效(单个线程相比),并且由于没有线程交互的开销,专心做垃圾收集自然课获得最高的单线程效率。在一般情况下,垃圾收集造成的停顿时间可以控制在几十毫秒甚至一百多毫秒以内,还是可以接受的。

2,ParNew收集器

ParNew收集器其实是Serial收集器的多线程版本,与Serial不同的地方就是在垃圾收集过程中使用多个线程,剩下的所有行为包括控制参数、收集算法、Stop the World、对象分配规则和回收策略等都一样。ParNew收集器也使用复制算法。ParNew收集器的工作流程如下图:

ParNew收集器看似没有多大的创新之处,但却是许多运行在Server模式下的虚拟机中首选的新生代收集器,因为,除了Serial收集器外,目前只有ParNew收集器能够与CMS收集器配合工作,而CMS收集器是HotSpot在JDK 1.5时期推出的具有划时代意义的垃圾收集器(后面会介绍到)。

ParNew收集器在单个线程的情况下由于线程交互的开销没有Serial收集器的效果好。不过,随着CPU个数的增加,它对于GC时系统资源的有效利用还是很有好处的。它默认开启的收集线程数与CPU的数量相同。可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。

3,Parallel Scavenge收集器

Parallel Scavenge收集器和ParNew类似,是一个新生代收集器,使用复制算法,又是并行的多线程收集器。不过和ParNew不同的是,Parallel Scavenge收集器的关注点不同。

CMS等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目的则是达到一个可控制的吞吐量。吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+运行垃圾收集时间)。如果虚拟机一共运行100分钟,垃圾收集运行了1分钟,那么吞吐量就是99%。

停顿时间越短就越适合与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效的利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

Parallel Scavenge收集器提供了两个参数来精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。

MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,收集器将尽可能在给定时间内完成垃圾收集。不过垃圾收集时间的缩短是以牺牲吞吐量和新生代空间为代价的,短的垃圾收集时间会导致更加频繁的垃圾收集行为,从而导致吞吐量的降低。

4,Serial Old收集器

Serial Old是Serial的老年版本,在Serial的工作流程图中可以看到,Serial Old收集器也是一个单线程收集器,使用“标记-整理”算法。这个收集器主要给Client模式下的虚拟机使用。如果在Serve模式下,它有两个用途:一个是在JDK 1.5之前的版本中与Parallel Scavenge收集器搭配使用;另一个就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。这个收集器的工作流程在Serial的后半部分有所体现。

5,Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年版本,它也使用多线程和“标记-整理”算法。这个收集器是在JDK 1.6开始提供。

在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器的组合。Parallel Old收集器的工作流程如下:

6,CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。在重视响应速度和用户体验的应用中,CMS应用很多。

CMS收集器使用“标记-清除”算法,运作过程比较复杂,分为4个步骤:

  • 初始标记(CMS initial mark)
  • 并发标记(CMS Concurrent mark)
  • 重新标记(CMS remark)
  • 并发清除(CMS Concurrent Sweep)

其中,初始标记和并发标记仍然需要Stop the World、初始标记仅仅标记一下GC Roots能直接关联到的对象,速度很快,并发标记就是进行GC RootsTracing的过程,而重新标记阶段则是为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段长,但远比并发标记的时间短。

由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以整体上说,CMS收集器的内存回收过程是与用户线程一共并发执行的。下图是流程图:

CMS的优点就是并发收集、低停顿,是一款优秀的收集器。不过,CMS也有缺点,如下:

  • CMS收集器对CPU资源非常敏感。CMS默认启动的回收线程数是(CPU数量+3)/4,当CPU个数大于4时,垃圾收集线程使用不少于25%的CPU资源,当CPU个数不足时,CMS对用户程序的影响很大;
  • CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC;
  • CMS使用标记-清除算法,会产生内存碎片;

7,G1收集器

G1(Garbage first)收集器是最先进的收集器之一,是面向服务端的垃圾收集器。与其他收集器相比,G1收集器有如下优点:

  • 并行与并发:有些收集器需要停顿的过程G1仍然可以通过并发的方式让用户程序继续执行;
  • 分代收集:可以不使用其他收集器配合管理整个Java堆;
  • 空间整合:使用标记-整理算法,不产生内存碎片;
  • 可预测的停顿:G1除了降低停顿外,还能建立可预测的停顿时间模型;

G1中也有分代的概念,不过使用G1收集器时,Java堆的内存布局与其他收集器有很大的差别,它将整个Java堆划分为多个大小相等的独立区域(Region),G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划的避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里垃圾堆积的价值大小(回收所获得的空间大小以及回收所需要的时间的经验值),在后台维护一个优先列表,每次优先收集价值最大的那个Region。这样就保证了在有限的时间内尽可能提高效率。

G1收集器的大致步骤如下:

  • 初始标记(Initial mark)
  • 并发标记(Concurrent mark)
  • 最终标记(Final mark)
  • 筛选回收(Live Data Counting and Evacuation)

收集器的流程如下图:

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-09-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 攻城狮的那点事 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一,对象是否可被回收的判断方法
  • 1,标记-清除算法
  • 2,复制算法
  • 3,标记-整理算法
  • 4,分代收集算法
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档