架构师训练营作业 (第九周)

用户头像
王海
关注
发布于: 2020 年 08 月 02 日

题目一

请简述JVM垃圾回收原理。

题目1解答

一、怎么判断对象是垃圾,可以被回收

1.1 引用计数器算法

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1。任何时刻计数器值为0的对象就是不可能再被使用的,将会被回收。

引用计数器法实现简单,判定效率也很高,但是,它很难解决对象之间相互循环引用的问题。也就是说当对象A引用对象B,对象B又引用对象A,那么此时A,B对象的引用计数器都不为0,也就造成无法完成垃圾回收。

1.2 可达性分析算法

可达性分析算法的基本思路就是:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,所走过的路径称为“引用链”,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,会被判定为可回收的对象。

在java中可以作为GC Roots得到对象有以下几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。

  • 方法区中静态属性引用的对象

  • 方法区中常量引用的对象

  • 本地方法栈中JNI引用的对象



二、垃圾回收算法

2.1 标记-清除算法

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

它的主要不足有两个:

  • 效率问题

标记和清除两个过程的效率都不高

  • 空间问题

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

2.2 复制算法

将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次性清理掉。这样使得每次都是对 整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半,代价太高。

2.3 标记整理算法

标记-整理算法,标记过程与标记清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

2.4 分代收集算法

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

三、垃圾回收器

垃圾回收器

新生代收集器:Serial、ParNew、Parallel Scavenge

老年代收集器:CMS、Serial Old、Parallel Old

整堆收集器:G1、ZGC

并行收集:指多条垃圾收集线程并行工作,但此时用户线程扔处于等待状态。

并发收集:指用户线程与垃圾收集线程同时工作(不一定是并行的可能会交替执行)。用户线程在继续运行,而垃圾收集线程运行在另一个CPU上。

吞吐量:即CPU用于运行用户代码的时间与CPU总消耗时间的比值(吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间))

3.1 Serial收集器





特点:单线程、简单高效。对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(Stop The World)。

应用场景:适用于Client模式下的虚拟机

3.2 ParNew收集器

Serial 的多线程版本,使用多条线程进行垃圾收集。

特点:多线程、ParNew收集器默认开启的收集线程数与CPU的数量相同,在CPU非常多的环境中,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。和Serial收集器一样存在Stop The World问题。

ParNew收集器其实跟Parallel收集器很类似,区别主要在于它可以和CMS收集器配合使用。

新生代采用复制算法,老年代采用标记-整理算法。

它是许多运行在Server模式下的虚拟机的首要选择,除了Serial收集器外,只有它能与CMS收集器(真正意义上的并发收集器,后面会介绍到)配合工作。

应用场景:ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器,因为它是除了Serial收集器外,唯一一个能与CMS收集器配合工作的。

3.3 Parallel Scavenge 收集器

特点:属于新生代收集器也是采用复制算法的收集器,又是并行的多线程收集器(与ParNew收集器类似)。该收集器的目标是到达一个可控制得到吞吐量。

Parallel收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和Serial收集器类似。默认的收集线程数跟cpu核数相同,当然也可以用参数(-XX:ParallelGCThreads)指定收集线程数,但是一般不推荐修改。

Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU)。CMS等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。 Parallel Scavenge收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解的话,可以选择把内存管理优化交给虚拟机去完成也是一个不错的选择。

新生代采用复制算法,老年代采用标记-整理算法。

3.4 Serial Old 收集器

Serial Old收集器是Serial收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS收集器的后备方案

3.5 Parallel Old 收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源的场合,都可以优先考虑 Parallel Scavenge收集器和Parallel Old收集器(JDK8默认的新生代和老年代收集器)。

3.6 CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用,它是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。

从名字中的Mark Sweep这两个词可以看出,CMS收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:

  • 初始标记: 暂停所有的其他线程(STW),并记录下gc roots直接能引用的对象速度很快

  • 并发标记: 并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程, 这个过程耗时较长但是不需要停顿用户线程, 可以与垃圾收集线程一起并发运行。因为用户程序继续运行,可能会有导致已经标记过的对象状态发生改变。

  • 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。主要用到三色标记里的增量更新算法(见下面详解)做重新标记。

  • 并发清理: 开启用户线程,同时GC线程开始对未标记的区域做清扫。

  • 并发重置:重置本次GC过程中的标记数据。

从它的名字就可以看出它是一款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下面几个明显的缺点:

  • 对CPU资源敏感(会和服务抢资源);

  • 无法处理浮动垃圾(在并发标记和并发清理阶段又产生垃圾,这种浮动垃圾只能等到下一次gc再清理了);

  • 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生,当然通过参数-XX:+UseCMSCompactAtFullCollection可以让jvm在执行完标记清除后再做整理

  • 执行过程中的不确定性,会存在上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况,特别是在并发标记和并发清理阶段会出现,一边回收,系统一边运行,也许没回收完就再次触发full gc,也就是"concurrent mode failure",此时会进入stop the world,用serial old垃圾收集器来回收



CMS的相关核心参数

  1. -XX:+UseConcMarkSweepGC:启用cms

  2. -XX:ConcGCThreads:并发的GC线程数

  3. -XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩整理(减少碎片)

  4. -XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩一次,默认是0,代表每次FullGC后都会压缩一次

  5. -XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比)

  6. -XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction设定的值),如果不指定,JVM仅在第一次使用设定值,后续则会自动调整

  7. -XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc,目的在于减少老年代对年轻代的引用,降低CMS GC的标记阶段时的开销,一般CMS的GC耗时 80%都在标记阶段

  8. -XX:+CMSParallellnitialMarkEnabled:表示在初始标记的时候多线程执行,缩短STW

  9. -XX:+CMSParallelRemarkEnabled:在重新标记的时候多线程执行,缩短STW



3.7 G1收集器

G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征.



G1将Java堆划分为多个大小相等的独立区域(Region),JVM最多可以有2048个Region。

一般Region大小等于堆大小除以2048,比如堆大小为4096M,则Region大小为2M,当然也可以用参数"-XX:G1HeapRegionSize"手动指定Region大小,但是推荐默认的计算方式。

G1保留了年轻代和老年代的概念,但不再是物理隔阂了,它们都是(可以不连续)Region的集合。

默认年轻代对堆内存的占比是5%,如果堆大小为4096M,那么年轻代占据200MB左右的内存,对应大概是100个Region,可以通过“-XX:G1NewSizePercent”设置新生代初始占比,在系统运行中,JVM会不停的给年轻代增加更多的Region,但是最多新生代的占比不会超过60%,可以通过“-XX:G1MaxNewSizePercent”调整。年轻代中的Eden和Survivor对应的region也跟之前一样,默认8:1:1,假设年轻代现在有1000个region,eden区对应800个,s0对应100个,s1对应100个。

一个Region可能之前是年轻代,如果Region进行了垃圾回收,之后可能又会变成老年代,也就是说Region的区域功能可能会动态变化。



G1垃圾收集器对于对象什么时候会转移到老年代跟之前讲过的原则一样,唯一不同的是对大对象的处理,G1有专门分配大对象的Region叫Humongous区,而不是让大对象直接进入老年代的Region中。在G1中,大对象的判定规则就是一个大对象超过了一个Region大小的50%,比如按照上面算的,每个Region是2M,只要一个大对象超过了1M,就会被放入Humongous中,而且一个大对象如果太大,可能会横跨多个Region来存放。

Humongous区专门存放短期巨型对象,不用直接进老年代,可以节约老年代的空间,避免因为老年代空间不够的GC开销。

Full GC的时候除了收集年轻代和老年代之外,也会将Humongous区一并回收。



G1收集器一次GC的运作过程大致分为以下几个步骤:

  • 初始标记(initial mark,STW):暂停所有的其他线程,并记录下gc roots直接能引用的对象,速度很快 ;

  • 并发标记(Concurrent Marking):同CMS的并发标记

  • 最终标记(Remark,STW):同CMS的重新标记

  • 筛选回收(Cleanup,STW):筛选回收阶段首先对各个Region的回收价值和成本进行排序根据用户所期望的GC停顿STW时间(可以用JVM参数 -XX:MaxGCPauseMillis指定)来制定回收计划,比如说老年代此时有1000个Region都满了,但是因为根据预期停顿时间,本次垃圾回收可能只能停顿200毫秒,那么通过之前回收成本计算得知,可能回收其中800个Region刚好需要200ms,那么就只会回收800个Region(Collection Set,要回收的集合),尽量把GC导致的停顿时间控制在我们指定的范围内。这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。不管是年轻代或是老年代,回收算法主要用的是复制算法将一个region中的存活对象复制到另一个region中,这种不会像CMS那样回收完因为有很多内存碎片还需要整理一次,G1采用复制算法回收几乎不会有太多内存碎片。(注意:CMS回收阶段是跟用户线程一起并发执行的,G1因为内部实现太复杂暂时没实现并发回收,不过到了ZGC,Shenandoah就实现了并发收集,Shenandoah可以看成是G1的升级版本)

G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region(这也就是它的名字Garbage-First的由来),比如一个Region花200ms能回收10M垃圾,另外一个Region花50ms能回收20M垃圾,在回收时间有限情况下,G1当然会优先选择后面这个Region回收。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限时间内可以尽可能高的收集效率。



被视为JDK1.7以上版本Java虚拟机的一个重要进化特征。它具备以下特点:

  • 并行与并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程来执行GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。

  • 分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。

  • 空间整合:与CMS的“标记--清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。

  • 可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1 和 CMS 共同的关注点,但G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段(通过参数"-XX:MaxGCPauseMillis"指定)内完成垃圾收集。



毫无疑问, 可以由用户指定期望的停顿时间是G1收集器很强大的一个功能, 设置不同的期望停顿时间, 可使得G1在不同应用场景中取得关注吞吐量和关注延迟之间的最佳平衡。 不过, 这里设置的“期望值”必须是符合实际的, 不能异想天开, 毕竟G1是要冻结用户线程来复制对象的, 这个停顿时间再怎么低也得有个限度。 它默认的停顿目标为两百毫秒, 一般来说, 回收阶段占到几十到一百甚至接近两百毫秒都很正常, 但如果我们把停顿时间调得非常低, 譬如设置为二十毫秒, 很可能出现的结果就是由于停顿目标时间太短, 导致每次选出来的回收集只占堆内存很小的一部分, 收集器收集的速度逐渐跟不上分配器分配的速度, 导致垃圾慢慢堆积。 很可能一开始收集器还能从空闲的堆内存中获得一些喘息的时间, 但应用运行时间一长就不行了, 最终占满堆引发Full GC反而降低性能, 所以通常把期望停顿时间设置为一两百毫秒或者两三百毫秒会是比较合理的。

G1垃圾收集器优化建议

假设参数 -XX:MaxGCPauseMills 设置的值很大,导致系统运行很久,年轻代可能都占用了堆内存的60%了,此时才触发年轻代gc。

那么存活下来的对象可能就会很多,此时就会导致Survivor区域放不下那么多的对象,就会进入老年代中。或者是你年轻代gc过后,存活下来的对象过多,导致进入Survivor区域后触发了动态年龄判定规则,达到了Survivor区域的50%,也会快速导致一些对象进入老年代中。所以这里核心还是在于调节 -XX:MaxGCPauseMills 这个参数的值,在保证他的年轻代gc别太频繁的同时,还得考虑每次gc过后的存活对象有多少,避免存活对象太多快速进入老年代,频繁触发mixed gc.

什么场景适合使用G1

  1. 50%以上的堆被存活对象占用

  2. 对象分配和晋升的速度变化非常大

  3. 垃圾回收时间特别长,超过1秒

  4. 8GB以上的堆内存(建议值)

  5. 停顿时间是500ms以内



题目二

设计一个秒杀系统,主要的挑战和问题有哪些?核心的架构方案或者思路有哪些?

题目二解答

1、主要的挑战和问题有哪些?

1.1、高并发读和写

秒杀时间一到,高并发读写请求向服务器发送,服务器如何处理这些请求

1.2、秒杀系统和其他系统如何隔离

​秒杀系统只是在某个时间点会出现峰值高点,给服务器带来高负载,如何做到系统之间的隔离,秒杀系统的处理逻辑不影响正常业务的逻辑

1.3、高并发下数据库的负载,一致性问题

​1.4、防止刷单

1.5、防止请求和返回的内容过多,出现服务器带宽不足

2、处理秒杀系统的思路

2.1、动静分离

静态内容和动态内容进行分离,利用CDN加速静态内容,减少请求之间发送给秒杀服务器

2.2、页面压缩减少传输带宽

2.3、负载均衡,集群秒杀服务器,分担请求流量

2.4、单个秒杀服务器进行削峰填谷

通过消息队列对请求进行缓冲,同步改异步,防止下游服务器压力过大

通过验证机制增加请求时间,防止机器刷单

通过分层架构,把流量化解到最前端,如(浏览器缓存,CDN缓存,Cache缓存)

2.5 容灾兜底

通过限流,熔断机制增加服务器的稳定性,防止过载流量压垮秒杀系统



发布于: 2020 年 08 月 02 日 阅读数: 18
用户头像

王海

关注

还未添加个人签名 2018.06.17 加入

还未添加个人简介

评论

发布
暂无评论
架构师训练营作业 (第九周)