架构师训练营第九周 - 作业
请简述 JVM 垃圾回收原理
JVM垃圾回收机制
回收发生在哪里?
JVM 的内存区域中,程序计数器、虚拟机栈和本地方法栈这 3 个区域是线程私有的,随着线程的创建而创建,销毁而销毁;栈中的栈帧随着方法的进入和退出进行入栈和出栈操作,每个栈帧中分配多少内存基本是在类结构确定下来的时候就已知的,因此这三个区域的内存分配和回收都具有确定性。
那么垃圾回收的重点就是关注堆和*方法区*中的内存了,堆中的回收主要是对象的回收,方法区的回收主要是废弃常量和无用的类的回收。
对象在什么时候可以被回收?
一个对象不再被引用,就代表该对象可以被回收。目前有以下两种算法可以判断该对象是否可以被回收:
引用计数算法:这种算法是通过一个对象的引用计数器来判断该对象是否被引用了。每当对象被引用,引用计数器就会加 1;每当引用失效,计数器就会减 1。当对象的引用计数器的值为 0 时,就说明该对象不再被引用,可以被回收了。这里强调一点,虽然引用计数算法的实现简单,判断效率也很高,但它存在着对象之间相互循环引用的问题。
可达性分析算法:GC Roots 是该算法的基础,GC Roots 是所有对象的根对象,在 JVM 加载时,会创建一些普通对象引用正常对象。这些对象作为正常对象的起始点,在垃圾回收时,会从这些 GC Roots 开始向下搜索,当一个对象到 GC Roots 没有任何引用链相连时,就证明此对象是不可用的。目前 HotSpot 虚拟机采用的就是这种算法。
在 JDK 1.2 之后,Java 对引用的概念进行了扩充,将引用分为了以下四种:
如何回收这些对象?
JVM 垃圾回收遵循以下两个特性:
自动性:JVM 提供了一个系统级的线程来跟踪每一块分配出去的内存空间,当 JVM 处于空闲循环时,垃圾收集器线程会自动检查每一块分配出去的内存空间,然后自动回收每一块空闲的内存块。
不可预期性:一旦一个对象没有被引用了,该对象是否立刻被回收呢?答案是不可预期的。我们很难确定一个没有被引用的对象是不是会被立刻回收掉。垃圾回收线程在 JVM 中是自动执行的,Java 程序无法强制执行。我们唯一能做的就是通过调用 System.gc 方法来”建议”执行垃圾收集器,但是否可执行,什么时候执行?仍然不可预期
堆的划分
当 Eden 区的空间耗尽, 触发 Minor GC,收集新生代的垃圾。存活下来的对象,被送到 Survivor 区。当发生 Minor GC 时, Eden 区和 from 指向的 Survivor 区中的存活对象会被复制到 to 指向的 Survivor 去,然后交换 from 和 to 指针。
JVM 会记录 Survivor 区中的对象一共被来回复制了几次。如果一个对象被复制的次数达到一个值(对应虚拟机参数 -XX:+MaxTenuringThreshold),该对象会被promo 至老年代。如果带个 Survivor 区被占用 50%(对应虚拟机参数 -XX:TargetSurvivorRatio),较高复制次数的对象也会被 promo 至老年代。
注:JDK 11 引入并发、不分代的 ZGC 回收器。
GC 算法
垃圾回收器的实现
垃圾回收器种类
针对新生代的垃圾回收器
* Serial
* Parallel
* Parallel New
这三个采用的都是标记 - 复制算法。其中,Serial 是一个单线程的,Parallel New 可以看成 Serial 的多线程版本。Parallel Scavenge 和 Parallel New 类似,但更加注重吞吐率。此外,Parallel Scavenge 不能与 CMS 一起使用。
针对老年代的垃圾回收器
* Serial Old
* Parallel Old
* CMS
Serial Old 和 Parallel Old 都是标记 - 压缩算法。同样,前者是单线程的,而后者可以看成前者的多线程版本。CMS 采用的是标记 - 清除算法,并且是并发的。除了少数几个操作需要 Stop-the-world 之外,它可以在应用程序运行过程中进行垃圾回收。在并发收集失败的情况下,Java 虚拟机会使用其他两个压缩型垃圾回收器进行一次垃圾回收。由于 G1 的出现,CMS 在 Java 9 中已被废弃。
G1(Garbage First)
是一个横跨新生代和老年代的垃圾回收器。实际上,它已经打乱了前面所说的堆结构,直接将堆分成极其多个区域。每个区域都可以充当 Eden 区、Survivor 区或者老年代中的一个。它采用的是标记 - 压缩算法,而且和 CMS 一样都能够在应用程序运行过程中并发地进行垃圾回收。G1 能够针对每个细分的区域来进行垃圾回收。在选择进行垃圾回收的区域时,它会优先回收死亡对象较多的区域。这也是 G1 名字的由来。
ZGC
JDK 11引入,并发、不分代的垃圾回收器
GC 性能衡量指标
* 吞吐量:应用程序所花费的时间和系统总运行时间的比值。
* 停顿时间:垃圾回收器运行时,应用程序的暂停时间。
* 垃圾回收频率:通常垃圾回收的频率越低越好,增大堆内存空间可以有效降低垃圾回收发生的频率,但同时也意味着堆积的回收对象越多,最终也会增加回收时的停顿时间。
工具
* jmap 输出内存对象
* [GCViewer](gcviewer download | SourceForge.net) GC日志分析
* [GCeasy](Universal JVM GC analyzer - Java Garbage collection log analysis made easy) GC日志分析
GC 调优策略
* 降低 Minor GC 频率
如果在堆内存中存在较多的长期存活的对象,此时增加新生代空间,反而会增加 Minor GC 的时间。如果堆中的短期对象很多,那么扩容新生代,单次 Minor GC 时间不会显著增加。因此,单次 Minor GC 时间更多取决于 GC 后存活对象的数量,而非 Eden 区的大小。
* 降低 Full GC 频率
* 减少创建大对象
* 增大堆内存空间
* 选择合适的垃圾回收器:要求响应时间短,考虑 G1 回收器;要求吞吐量高,考虑 Parallel Scavenge 回收器。
注意:调整 GC 参数前,首先要有性能衡量指标。
设计一个秒杀系统,主要的挑战和问题有哪些?核心的架构方案或者思路有哪些?
挑战和问题
稳:高可用,保证秒杀活动顺利完成
准:不会出现超卖,造成运营方损失
预防秒杀器
架构原则:“4 要 1 不要”
数据要尽量少
降低CPU的消耗:压缩、编码,简化秒杀页面,去掉不必要的业务装饰效果
请求数要尽量少
合并 CSS 和 JavaScript,减少需要涉及的域名
路径要尽量短
减少网络调用,降低延时
依赖要尽量少
将系统按重要性分级,防止重要系统被不重要系统拖垮
不要有单点
服务无状态化
思路
动静分离
* 静态数据缓存到离用户最近的地方
* 静态化改造 —— 直接缓存 HTTP 连接
二八原则:有针对性地处理系统的热点
* 热点操作:刷新页面、添加购物车、抢下单。这些操作可分为“读”和“写”两类,读可通过缓存优化,写瓶颈通常在存储层。
热点操作通常集中在下单,成功下单后,支付可以给出一段时间后续进行,避免拖垮支付系统
* 热点数据:例如参加秒杀活动的商品数据
可以通过商业手段,例如让卖家提前挑选出热点商品,针对对这些数据进行缓存;或者通过大数据技术手段,预测热点商品
分级门槛
优化、限制、隔离
* 优化:缓存热点数据
* 限制:对流量进行削峰。通常秒杀活动希望更多的人来参与,但真正能下单抢到商品的人数是预先固定的,对此可以只处理部分下单请求,其它请求直接结束。
* 隔离:将秒杀业务与正常业务隔离,将秒杀系统的服务器群集独立出来
预防秒杀器
秒杀 Detail 页面 URL 带上随机 token,临近秒杀前才放出,设置访问次数上限
应急预案
* 备用带宽、服务器
* 服务降级,当系统容量达到一定程度是,限制或关闭系统的某些非核心功能
* 拒绝服务:万能出错页面 —— “秒杀活动已结束”
评论