写点什么

架构师训练营 - week09 - 作业

用户头像
lucian
关注
发布于: 2020 年 11 月 22 日

1. 请简述 JVM 垃圾回收原理

基本原理



整理垃圾回收大体分类两个阶段:

  • 垃圾对象标记,通过可达性分析算法对垃圾对象进行识别。具体过程是,从 ROOT 根出发(线程栈帧中局部变量,方法区静态变量),将这些变量引用对象进行标记,如果这些对象引用其他对象进行递归标记。最终,没有进行标记的对象就是垃圾对象。

  • 垃圾对象回收,释放占用空间主要有三种方式

  • 清理:直接标记垃圾对象空间未占用。所有未占用的空间形成链表。当需要分配对象时,从链表中获得一个略大于需求空间的块进行分配。

  • 压缩:从堆空间的头开始,将存活对象拷贝到一边,呈现连续空间。

  • 复制:将对空间分成两部分,只用一部分进行对象分配。空间用完后,拷贝存活的对象到另一个空闲空间。



下面以 G1 进行说明垃圾回收过程



G1 垃圾回收过程

G1 是分代的,增量的,并行与并发的标记-复制垃圾回收器。设计目标是,适应现代系统对大内存需求和利用良好多核并发系统,建立可控暂停时间,同时兼顾吞吐量。



数据结构

G1 对比 CMS,同样也是分代垃圾回收器,但是 G1 把堆划分为大小相等的 Region(固定的内存空间)。每个 Region 可以属于 Young 代,也可以属于 Old 代,但一旦指定不会发生变化。有一类特殊 Region,称为 Humongous,用于巨大对象(对象超过普通 Region 1/2)的分配。



Region 不要求各个类型 Region 物理连续,因此典型分配可能是这样的:


注:图片来自 G1: One Garbage Collector To Rule Them All



每个 Region,都分为两部分,已分配的和未分配的。之间的界限称为 top。这样,当对象分配的时候,只需要移动 top 位置,这种做法被称为撞针(bump the pointer)



对象分配

G1 维护一个空闲 Region 的链表。每次回收后的 Region 都加入到这个链表中。每次都只有一个 Region 处于分配状态,被称为 current Region。



在多线程下,只有一个分配 region 容易引起并发冲突。这里继续使用 CMS 中 TLABs 机制。每个线程预先分配一个 Buffer,供线程独占使用,用完重新申请。每次对 top 的移动使用 CAS 操作保证线程安全。



显然 TLABs 会导致内存碎片化。虽然当前 buffer 有空间,但是不满足当前对象的空间需求。所以,堆的空间碎片程度,受线程数和 buffer 大小影响。



Remember Set 和 Card Table

RS(Remember Set)是一种抽象概念,用于记录从非收集区域指向收集部分的指针集合。在 CMS 中,RS 记录了代于代之间的指针。在 G1 中,RS 记录其他 Region 指向本 Region 内存空间的指针。这样,每个 Region 都有自己的 RS。这样做的优势,垃圾识别不需要整个堆进行扫描,只需要 RS 配合在一个 Region 进行扫描标记垃圾。



如果一个线程修改了 Region 内部的引用,就需要修改 RS 中的记录。G1 使用 CT(Card Table)数据结构实现 RS。Region 被划分为多个卡片( Card)。每个卡都有一个 Byte 记录是否修改过。CT 即这些 Byte 的集合。



CT 的修改也有多线程并发问题。G1 进一步把 RS 划分为多个哈希表。每个线程在自己的哈希表中进行修改。这有个好处,能够去重,RS 大小和修改指针数量相当。



整个关系如下:


注意上图中,RS 是逻辑表示。



垃圾回收算法详解

整个算法可以分成两大部分:

  1. Marking cycle phase:标记阶段,该阶段是不断循环进行的;

  2. Evacuation phase:该阶段是负责把一部分 region 的活对象拷贝到空 Region 里面去,然后回收原本的 Region 空间,该阶段是 STW(stop-the-world)的;



算法也可以分成两种模式:

  1. fully-young generational mode:有时候也会被称为 young GC,该模式只会回收 young region,算法是通过调整 young region 的数量来达到软实时目标的;

  2. partially-young mode:也被称为 Mixed GC,该阶段会回收 young region 和 old region,算法通过调整 old region 的数量来达到软实时目标;



在需要分配老年代的对象的时候,并没有足够的空间。这个时候就只能触发一次 full GC。



而是采用 bitmap 的方式来记录一个对象被标记的情况。这种记录方法的好处就是在使用这些标记信息的时候,仅仅需要扫描 bitmap 而已。G1 统计一个 region 的存活的对象,就是依赖于 bitmap 的标记。



Marking Cycle Phase

算法的 Marking cycle phase 大概可以分成五个阶段:

  1. Initial marking phase:G1 收集器扫描所有的根。该过程是和 young GC 的暂停过程一起的;

  2. Root region scanning phase:扫描 Survivor Regions 中指向老年代的被 initial mark phase 标记的引用及引用的对象,这一个过程是并发进行的。但是该过程要在下一个 young GC 开始之前结束;

  3. Concurrent marking phase:并发标记阶段,标记整个堆的存活对象。该过程可以被 young GC 所打断。并发阶段产生的新的引用(或者引用的更新)会被 SATB 的 write barrier 记录下来;

  4. Remark phase:也叫 final marking phase。该阶段只需要扫描 SATB(Snapshot At The Beginning)的 buffer,处理在并发阶段产生的新的存活对象的引用。作为对比,CMS 的 remark 需要扫描整个 mod union table 的标记为 dirty 的 entry 以及全部根;

  5. Cleanup phase:清理阶段。该阶段会计算每一个 region 里面存活的对象,并把完全没有存活对象的 Region 直接放到空闲列表中。在该阶段还会重置 Remember Set。该阶段在计算 Region 中存活对象的时候,是 STW(Stop-the-world)的,而在重置 Remember Set 的时候,却是可以并行的;



Evacuation

Evacuation 阶段 STW 的,大概可以分成两个步骤:第一个步骤是从 Region 中选出若干个 Region 进行回收,这些被选中的 Region 称为 Collect Set(简称 CSet);而第二个步骤则是把这些 Region 中存活的对象复制到空闲的 Region 中去,同时把这些已经被回收的 Region 放到空闲 Region 列表中。

这两个步骤又可以被分解成三个任务:



  1. 根据 RS 的日志更新 RS:只有在处理完了 RS 的日志之后,RS 才能够保证是准确的,完整的,这也是 Evacuation 是 STW 的重要原因;

  2. 扫描 RS 和其余的根来确定存活对象:该阶段实际上最主要依赖于 RS;

  3. 拷贝存活对象:该阶段只要从 2 中确定的根触发,沿着引用链一直追溯下去,将存活对象复制到新的 region 就可以。这个过程中,可能有一部分的年轻代对象会被提升到老年代;





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


秒杀系统,主要的特点:

  1. 短时间内有超高的并发请求涌向系统

2. 绝大部分的请求最终不会成交,最后成交的订单只占很小的比例



核心的处理思路:

1. 采用分层过滤的方式,在每一层都挡掉一部分请求,使得落在后端系统的压力逐步变小。

2. 秒杀模块尽量独立,从访问域名、网关、应用部署、服务器资源都独立实现。一些必要的关联,例如跟后台交易系统或者支付系统关联功能,放在最后层,也就是到达流量尽量小的模块。

3. 对于关联的系统,通过消息队列进行解耦,由消息队列来完成缓冲的作用

4. 善用缓存,减少对数据库的访问压力。

5. 为了反爬虫,制定对应的安全策略,例如随机的订单 URL,秒杀前不允许访问下单页面等。



处理步骤:

1. 前端客户请求:因为秒杀新增的网络带宽,必须和运营商重新购买或者租借。为了减轻网站服务器的压力,需要将秒杀商品页面静态化,缓存在 CDN,同时需要和 CDN 服务商临时租借新增的出口带宽。



2.前端秒杀页面:秒杀按钮在开始前进行置灰,开始后才可以进行点击,并且为了防止爬虫拿到订单 URL,在开始后传入随机参数加以验证。

使用 JavaScript 脚本控制,在秒杀商品静态页面中加入一个 JavaScript 文件引用,该 JavaScript 文件中包含 秒杀开始标志为否;当秒杀开始的时候生成一个新的 JavaScript 文件(文件名保持不变,只是内容不一样),更新秒杀开始标志为是,加入下单页面的 URL 及随机数参数(这个随机数只会产生一个,即所有人看到的 URL 都是同一个,服务器端可以用 redis 这种分布式缓存服务器来保存随机数),并被用户浏览器加载,控制秒杀商品页面的展示。这个 JavaScript 文件的加载可以加上随机版本号(例如 xx.js?v=32353823),这样就不会被浏览器、CDN 和反向代理服务器缓存。



  1. 秒杀器的应对:

秒杀器一般下单个购买及其迅速,可以通过校验码达到一定的方法,这就要求校验码足够安全,不容易被破解。

采用的方式有:秒杀专用验证码,电视公布验证码,秒杀答题。



4. 网关层进行限流



5. 进行下订单的前置检查,检查全局已提交订单数目:

    已超过秒杀商品总数,返回已结束页面给用户;

    未超过秒杀商品总数,提交到子订单系统;



6. 为了避免绕过前端,直接通过 URL 进行操作,需要在前后端进行时间同步,并且在秒杀的后台任务增加时间控制。



7. 商品库存信息放置在 redis 中,利用 redis 的分布式锁,来避免超卖的情况产生。

方式一:乐观锁

    update auctionauctions set quantity = #inQuantity# where auctionid = #itemId# and quantity = #dbQuantity#

方式二:尝试减库存,扣减成功后再下单操作

    update auctionauctions set quantity = quantity-#count# where auctionid = #itemId# and quantity >= #count#



应急预案:

1. 提前部署好冗余的机器数量,应对访问流量超出预期

2. 如果是秒杀服务反应慢,则进行熔断措施。或者关闭非核心应用系统。

3. 如果出现无法补救的错误,则关闭前端入口,统一 302 定位到类似“秒杀已结束”的页面。


用户头像

lucian

关注

还未添加个人签名 2018.03.13 加入

还未添加个人简介

评论

发布
暂无评论
架构师训练营 - week09 - 作业