架构师第九周作业

用户头像
满山李子
关注
发布于: 2020 年 08 月 05 日

请简述JVM垃圾回收原理?

1. 垃圾回收分为三种手段: 清理, 压缩, 复制





清理: 标记-清除算法



标记-清除算法采用从根集合(GC Roots)进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收,如下图所示。标记-清除算法不需要进行对象的移动,只需对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。



压缩:碎片整理

压缩算法的提出是为了克服句柄的开销和解决内存碎片的问题。它开始时把堆分成 一个对象 面和多个空闲面, 程序从对象面为对象分配空间,当对象满了,基于copying算法的垃圾 回收就从根集合(GC Roots)中扫描活动对象,并将每个 活动对象复制到空闲面(使得活动对象所占的内存之间没有空闲洞),这样空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存。





复制:与压缩功能一样,都把整理出来, 一块连续的内存空间; 不同的是压缩从个对象开始,把后面依次拷贝到根对象后面; 复制是把堆内存分为两部分,一部分是空闲空间, 一部分是活对象占用的空间, 把空闲空间的活对象拷贝到活对象占用的空间中。这样处理之后空间空间就是整块的了。



2. 分代回收算法



分代回收算法的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个代就是永久代(Permanet Generation)。老年代的特点是每次垃圾回收时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的回收算法。

2.1 年轻代(Young Generation)的回收算法

a) 所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的回收掉那些生命周期短的对象。

b) 新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。一个Eden区,两个 Survivor区(一般而言)。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。

c) 当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收。

d) 新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)。

2.2 年老代(Old Generation)的回收算法

a) 在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。



b) 内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。

2.3 持久代(Permanent Generation)的回收算法

用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代也称方法区。



3. 垃圾回收器算法

3.1 串行回收器

在串行回收器进行垃圾回收时,JAVA 应用程序中的线程都需要暂停,等待垃圾回收完成。如上图所示。

这种现象称之为Stop-The-Word。会造成非常糟糕的用户体验,在实时性较高的应用场景中,这种现象往往是不能接受的。



3.2 并行回收器

并行回收器有两种: parNew回收器 和 Parallel Scavenge

ParNew回收器就是串行回收器的多线程版本,它也是一个新生代回收器。除了使用多线程进行垃圾回收外,其余行为包括串行回收器可用的所有控制参数、回收算法(复制算法)、Stop The World、对象分配规则、回收策略等与串行回收器完全相同,两者共用了相当多的代码。



Parallel Scavenge回收器也是一个并行的多线程新生代回收器,它也使用复制算法。Parallel Scavenge回收器的特点是它的关注点与其他回收器不同,CMS等回收器的关注点是尽可能缩短垃圾回收时用户线程的停顿时间,而Parallel Scavenge回收器的目标是达到一个可控制的吞吐量(Throughput)。



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

Parallel Scavenge回收器除了会显而易见地提供可以精确控制吞吐量的参数,还提供了一个参数-XX:+UseAdaptiveSizePolicy,这是一个开关参数,打开参数后,就不需要手工指定新生代的大小(-Xmn)、Eden和Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了。

虚拟机会根据当前系统的运行情况回收性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种方式称为GC自适应的调节策略(GC Ergonomics)。自适应调节策略也是Parallel Scavenge回收器与ParNew回收器的一个重要区别。

另外值得注意的一点是,Parallel Scavenge回收器无法与CMS回收器配合使用,所以在JDK 1.6推出Parallel Old之前,如果新生代选择Parallel Scavenge回回收器,老年代只有Serial Old回收器能与之配合使用。



3.3 并发回收器CMS



CMS(Concurrent Mark Sweep)回收器是一种以获取最短回收停顿时间为目标的回收器,它非常符合那些集中在互联网站或者B/S系统的服务端上的Java应用,这些应用都非常重视服务的响应速度。从名字上(“Mark Sweep”)就可以看出它是基于“标记-清除”算法实现的。

CMS回收器工作的整个流程分为以下4个步骤:

  • 初始标记(CMS initial mark):仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要“Stop The World”。

  • 并发标记(CMS concurrent mark):进行GC Roots Tracing的过程,在整个过程中耗时最长。

  • 重新标记(CMS remark):为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。此阶段也需要“Stop The World”。

  • 并发清除(CMS concurrent sweep)

由于整个过程中耗时最长的并发标记和并发清除过程回收器线程都可以与用户线程一起工作。

所以,从总体上来说,CMS回收器的内存回收过程是与用户线程一起并发执行的。通过上图可以比较清楚地看到CMS回收器的运作步骤中并发和需要停顿的时间:

优点

CMS是一款优秀的回收器,它的主要优点在名字上已经体现出来了:并发回收、低停顿,因此CMS回收器也被称为并发低停顿回收器(Concurrent Low Pause Collector)。

缺点

  • 对CPU资源非常敏感 其实,面向并发设计的程序都对CPU资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。

  • CMS默认启动的回收线程数是(CPU数量+3)/4,也就是当CPU在4个以上时,并发回收时垃圾回收线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。但是当CPU不足4个时(比如2个),CMS对用户程序的影响就可能变得很大,如果本来CPU负载就比较大,还要分出一半的运算能力去执行回收器线程,就可能导致用户程序的执行速度忽然降低了50%,其实也让人无法接受。

  • 无法处理浮动垃圾(Floating Garbage) 可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。

  • 由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生。这一部分垃圾出现在标记过程之后,CMS无法再当次回收中处理掉它们,只好留待下一次GC时再清理掉。

  • 这一部分垃圾就被称为“浮动垃圾”。也是由于在垃圾回收阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS回收器不能像其他回收器那样等到老年代几乎完全被填满了再进行回收,需要预留一部分空间提供并发回收时的程序运作使用。

  • 标记-清除算法导致的空间碎片 CMS是一款基于“标记-清除”算法实现的回收器,这意味着回收结束时会有大量空间碎片产生。

  • 空间碎片过多时,将会给大对象分配带来很大麻烦,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象。

3.4 G1 回收器

G1(Garbage-First)回收器是当今回收器技术发展最前沿的成果之一,它是一款面向服务端应用的垃圾回收器,HotSpot开发团队赋予它的使命是(在比较长期的)未来可以替换掉JDK 1.5中发布的CMS回收器。与其他GC回收器相比,G1具备如下特点:



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

  • 分代回收 与其他回收器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他回收器配合就能独立管理整个GC堆,但它能够采用不同方式去处理新创建的对象和已存活一段时间、熬过多次GC的旧对象来获取更好的回收效果。

  • 空间整合 G1从整体来看是基于“标记-整理”算法实现的回收器,从局部(两个Region之间)上来看是基于“复制”算法实现的。这意味着G1运行期间不会产生内存空间碎片,回收后能提供规整的可用内存。此特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。

  • 可预测的停顿 这是G1相对CMS的一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了降低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在GC上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾回收器的特征了。



G1横跨整个堆内存



在G1之前的其他回收器进行回收的范围都是整个新生代或者老生代,而G1不再是这样, 而是跨整个堆内存。

G1在使用时,Java堆的内存布局与其他回收器有很大区别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,而都是一部分Region(不需要连续)的集合。



建立可预测的时间模型

G1回收器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾回收。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的来由)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1回收器在有限的时间内可以获取尽可能高的回收效率。



避免全堆扫描——Remembered Set

G1把Java堆分为多个Region,就是“化整为零”。但是Region不可能是孤立的,一个对象分配在某个Region中,可以与整个Java堆任意的对象发生引用关系。在做可达性分析确定对象是否存活的时候,需要扫描整个Java堆才能保证准确性,这显然是对GC效率的极大伤害。



为了避免全堆扫描的发生,虚拟机为G1中每个Region维护了一个与之对应的Remembered Set。虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作。

检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。



如果不计算维护Remembered Set的操作,G1回收器的运作大致可划分为以下几个步骤:

  • 初始标记(Initial Marking) 仅仅只是标记一下GC Roots 能直接关联到的对象,并且修改TAMS(Nest Top Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可以的Region中创建对象,此阶段需要停顿线程,但耗时很短。

  • 并发标记(Concurrent Marking) 从GC Root 开始对堆中对象进行可达性分析,找到存活对象,此阶段耗时较长,但可与用户程序并发执行。

  • 最终标记(Final Marking) 为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。

  • 筛选回收(Live Data Counting and Evacuation) 首先对各个Region中的回收价值和成本进行排序,根据用户所期望的GC 停顿是时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅度提高回收效率。



通过下图可以比较清楚地看到G1回收器的运作步骤中并发和需要停顿的阶段(Safepoint处):



2. 设计一个秒杀系统, 主要的挑战和问题有哪些?



1. 秒杀系统主要的技术挑战是高并发

高并发的风险

  • 网络贷款耗尽

  • 服务器Load飙高, 停止响应

  • 数据库瘫痪

高并发的事故

  • 事故:网站运营推广页面弹出1M大图片, 导致带宽耗尽

  • 增加审核机制: 运营推广增加图片的流量不能超过现有流量的30%

  • 合作媒体推广:迅雷, 暴风影音浮出广告,导致集群Crash



2. 高并发实例:xx.com 开业秒杀活动

2.1 商业需求:

  • 为庆祝xxx.com开业退出88小时不间断秒杀活动

  • 每小时整点推出8款商品...

  • 每款商品共168件, 每人限批3件, 成交人数56人

  • CCTV黄金广告时间, 各种网络, 平面媒体轰炸, 总广告费:1.5亿.

  • 接到运营通知, 距秒杀开始仅仅5天时间

现状: xx网站的正常流量

  • 并发(单台), 高峰期<10

  • 吞吐量(TPS, 单台) 高峰期 <60

  • CPU负载Load高峰期, <2, 大部分服务器<1

  • CPU使用率, 一般只有1棵核, 平均60%左右

  • 服务器平均响应时间, 高峰期 <150ms

  • 图片总流量贷款1.8G(个网站总和)



2.2 解决方案:

成立组织公司技术好的开发工程师, 成立一个专门的秒杀组开发一个新的秒杀系统



2.2.1 服务器准备(历史是这样的)

  • style服务器(Lighttpd集群): 5台

  • 图片服务器(Nginx集群): 5台

  • 静态护服务器(Apache集群): 10台

  • 交易服务器(JBoss动态集群): 10台



2.2.2 带宽准备

  • 图片出口带宽上限: 2.5G(出口带宽支持10G, 但图片服务器集群处理: 图片服务集群最大并发处理能力X网站平均图片大小 = 2.5G)

  • CDN准备: Chinacache沟通; 借用CDN



2.2.3 秒杀系统: 架构目标:

  1. 图片网络带宽: 1.0G

  2. 新增图片带宽: 必须控制在1.0G左右

  3. 每件商品秒杀页面的图片总大小不得超过: 10000000/(1000*8) = 125K/每件商品

  4. 网站并发:

  5. 单件商品并发: 1000 来自运营的预估

  6. 总并发: 8(件商品) x 1000 (人/商品) = 8000



2.2.4 秒杀系统: 组成

简单系统:

三个页面组成: 秒杀商品列表, 秒杀商品介绍, 下单 (由于公司已经有相应的模板了, 2小时就可以搞定)

下单成功后, 进入支付系统, 走支付流程



2.2.5 秒杀系统: 设计原则



静态化:

  • 采用js自动更新技术,将动态页面转化为静态页面

并发访问控制, 防秒杀器

  • 设置阀门, 只放最前面的一部分人进入秒杀系统

简化流程

  • 砍掉不重要的分支流程, 如下单页面的所有数据库查询

  • 以下单成功作为秒杀成功标志, 支付流程只要在1内完成即可

前端优化

  • 采用YSLOW原则提升页面响应速度



秒杀系统: 静态化(1)

秒杀商品list 和 Detail是静态HTML页面

秒杀系统: 静态化(2)



秒杀商品列表/秒杀商品介绍页面, 如何判断是否开始秒杀

答案: valid-offer.js

这里哪些资源走CDN和缓存都是完全可控的



三道阀门的设计





2.2.6 秒杀器的预防

秒杀Detail页面

  • URL: 随机

  • 秒杀前2秒放出, 脚本生成, 秒杀前

  • 1000次访问上限控制[每件商品只能放入1000人浏览]

下单页面

  • 订单ID, 随机

  • 不能直接跳过秒杀Detail页面进入

  • 每一件秒杀商品, 带预先生成的随机Token做URL参数

  • 如果秒杀过, 直接跳到秒杀结束页面

  • 100次方法上限控制[每件商品只能放入100人下单]



2.2.7 Web Server调优 - Apache调优



  • KeppAlive相关参数调优

  • 其他参数调优

  • HostnameLookups设置为off, 对allowfromdomain等后的域名不进行正向和反向的dn解析

  • 关闭cookies-log日志

  • 打开Linux sendfile()

  • 关闭无用的module

  • mod_Gzip

  • 秒杀页面, 非图片HTML文本流量比重可以忽略不计, zip意义不大

  • mod_Beacom

  • mod_hummock(等反应过来, 秒杀已经over了)





2.2.8 Web Server 调优 - JBoss调优

Mod-jk worker 调优

JBoss AJP Connector



Tomcat APR设定



2.2.9 秒杀静态页面优化

图片合并

  • 8张图片合并1张, css偏移展示

  • 减少HTTP请求书, 减少请求等待数

  • 减少发送Cookies的量



HTML内容压缩

图片压缩: 图片Bytes < 长x宽/2250

HTML Header Cache-Control设置

CSS, JS精简

  • CSS, JS精简到极致, 部分之间写在页面中, 减少HTTP请求次数。





2.2.10 二跳页面的优化

秒杀系统, 其他前端页面

  • 前端优化: Yslow规则调优

  • 减少HTTP请求, 合并JS, CSS, 图片, 充分利用浏览器缓存

  • 图片压缩

  • 避免发送Cookies

交易系统优化

  • 普通订单管理列表和xx秒批订单管理列表分离

  • 进制用模糊查询功能



2.2.11 应急预案

  • 域名分类, 独立域名, 不影响原有业务

  • style集群: style.xx.china.xx.com

  • 图片服务器集群: img.xx.china.xx.com

  • 静态页面集群: page.xx.china.xx.com

  • 出问题直接把xx项目域名卡掉, 所有请求调整到万能出错页面

  • 集中服务器10台备用

  • 拆东墙补西墙战略

  • 5天时间来不及采购服务, 因此SA待命, 随时准备讲非核心应用集群的冗余服务器下线, 加入到秒杀集群

  • 壁虎断尾策略

  • 所有方法均失效的情况下, 例如流量耗尽

  • 非核心应用集群统统停止服务, 如资讯, 论坛, 博客等社区系统.

  • 保住首页, offerDetail, 旺铺页面等核心应用的可用性

  • 万能出错页面: 秒杀活动已结束

  • 任何出错都302跳转到此页面

  • 位于另外的集群

  • 万幸: 最终所有预案都没有用上



2.2.12 秒杀活动结果

88小时秒杀, 坚守阵地, 大获成功

秒杀还是备被秒杀? 终于有了答案

三道阀门设计非常优秀, 拦住了秒杀器



2.3 秒杀系统优化



2.3.1 改进一: 采用更轻量/快速的服务器

采用Lightpd替代Apache杀手锏(AIO)









Lightpd 1.5 VS Apache2.2.4



小页面(100KB)性能

  • http_load -verbose -timeout 40 -parallel 100 -fetches 500 http-load.10M.urls-100M



大页面(10M)性能



性能关键: Web Server高性能 IO





2.3.2 改进二: 前端优化自动化

  • xxxx 服务器响应时间<150ms, 但Offer Detail 页面用户等待时间5s, 大部分时间消耗在路上(资源请求和网络传输)

  • 图片自动压缩(CMS自动压缩)

  • Cookies 服务化(控制cookie的大小)

  • xx前端延迟加载框架 SmartLoad(只加载首屏数据)

  • google mod_pagespeed module

  • 自动压缩图片, 静态资源, 智能浏览缓存技术

  • Google Diffable

  • 增量下载静态资源技术

2.3.3 改进三: 架设镜像站组建山寨DNS

xx青岛镜像站项目





2.3.4 改进四: 采用反向代理 加速核心页面





2.3.5 改进五: 海量数据的透明垂直切分





用户头像

满山李子

关注

还未添加个人签名 2018.09.18 加入

还未添加个人简介

评论

发布
暂无评论
架构师第九周作业