第 9 周作业
一、作业说明
请简述 JVM 垃圾回收原理。
设计一个秒杀系统,主要的挑战和问题有哪些?核心的架构方案或者思路有哪些?
二、作业实现
1.JVM 垃圾回收原理
Java 堆是 JVM 主要的内存管理区域,里面存放着大量的对象实例和数组。在垃圾回收算法和垃圾收集器之前,首先要做的就是判断哪些对象已经“死去”,需要进行回收即不可能再被任何途径使用的对象。
1.1 引用计数法
引用计数法是这样:给对象中添加一个引用计数器,每当有一个地方使用它时,计数器值就加 1。当引用失效时,计数器就减 1。任何时刻计数器为 0 的对象就是不可能再被使用的。
现在主流的 Java 虚拟机都没有使用引用计数法,最主要的原因就是它很难解决对象之间互相循环引用的问题。
1.2 可达性分析
可达性分析的基本思路:通过一系列称为"GC Roots"的对象作为起点,从这些节点开始向下搜索,如果从 GC Roots 到一个对象不可达,则证明此对象是不可用的,如下图所示。
Java 语言中,可作为 GC Roots 的对象包括下面几种:
虚拟机栈(栈帧中的本地变量表)中引用的对象
本地方法栈 JNI(即一般说的 Native 方法)引用的对象
方法区中类静态常量引用的对象
方法区中常量引用的对象
对于 Java 程序而言,对象基本都位于堆内存中,简单来说 GC Roots 就是有被堆外区域引用的对象。
2. 四种引用
在 JDK 1.2 以前的版本中,若一个对象不被任何变量引用,那么程序就无法再使用这个对象。也就是说,只有对象处于可达状态,程序才能使用它。
从 JDK 1.2 版本开始,对象的引用被划分为4
种级别,从而使程序能更加灵活地控制对象的生命周期。这4
种级别由高到低为:强引用、软引用、弱引用、虚引用。
2.1 强引用(StrongReference)
强引用是使用最普遍的引用,如下的方式就是强引用:
如果一个对象具有强引用,那垃圾回收器绝不会回收它。直到强引用的对象不使用或者超出对象的生命周期范围。则 GC 认为该对象不存在引用,这时候就可以回收这个对象。
当内存不足时,JVM 宁愿抛出 OutOfMemoryError 的错误,使程序异常终止,也不会靠随意回收具有强引用对象来解决内存不足的问题。
举例来说,
如下图在一个方法内部具有一个强引用,这个引用保存在虚拟栈的栈帧中,而真正的引用内容 Object 则保存在 Java 堆中。当这个方法运行完成后,退出方法栈。这个对象不再被 GC Roots 可达,那么这个对象在下次 GC 时就会被回收。
如下图一个类的静态变量需要一个强引用,这个引用保存在方法区中,而真正的引用内容 Object 则保存在 Java 堆中。当将这个引用手动制空
strongReference = null
后。这个对象不再被 GC Roots 可达,那么这个对象在下次 GC 时就会被回收。
2.2 软引用(SoftReference)
如果对象只具有软引用,则
内存空间充足时,垃圾回收器不会回收
内存空间不足时,就会尝试回收这些对象。只要垃圾回收器没有回收它,该对象就可以被程序使用
软引用可以和一个引用队列(ReferenceQueue)联合使用。如果软引用的对象被垃圾回收,JVM 就会把这个软引用加入到与之关联的引用队列中。
注意:
软引用对象是在 JVM 内存不够的时候才会被回收,我们调用 System.gc()方法只是起通知作用,JVM 什么时候扫描回收对象是 JVM 自己的状态决定的。
就算扫描到软引用对象真正开始 GC 也不一定会回收它,只有内存不够的时候才会回收。
软引用对象回收规则适应的前提条件是这个对象只有软引用。所以在上面的用例中要把强引用清除。
也就是说,垃圾收集线程会在虚拟机抛出 OutOfMemoryError 之前回收软引用对象,而且虚拟机会尽可能优先回收长时间闲置不用的软引用对象。对那些刚构建的或刚使用过的较新的软对象会被虚拟机尽可能保留。
应用场景:
浏览器的后退按钮。按后退时,这个后退时显示的网页内容是重新进行请求还是从缓存中取出呢?这就要看具体的实现策略了。
如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新构建;
如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出。
这时候就可以使用软引用,很好的解决了实际的问题:
2.3 弱引用(WeakReference)
相比较软引用,只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它锁管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
同样,弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用的对象被垃圾回收,JVM 就会把这个弱引用加入到与之关联的引用队列中。
2.4 虚引用(PhantomReference)
虚引用顾名思义,就是形同虚设。与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
应用场景:
虚引用主要用来跟踪对象被垃圾回收器回收的活动。 虚引用与软引用和弱引用的一个区别在于:
虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要进行垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
3. 垃圾收集算法
3.1 标记-清除算法
标记-清除算法分为“标记”和“清除”两个阶段,执行过程如下图所示:
标记:首先标记出所有需要回收的对象
清除:在标记完成后统一回收所有被标记的对象
标记-清除算法主要有两个不足:
效率问题,标记和清除的两个过程效率都不高
标记-清除会产生大量不连续的内存碎片,这会导致在后面需要分配连续的大对象时,无法找到足够大的连续内存而导致不得不提前触发另一次垃圾收集动作
3.2 复制算法
复制算法的大致思路如下,其执行过程如下图所示:
首先将可用内存分为大小相等的两块,每次只使用其中的一块。
当这一块的内存用完了,就将还存活的对象连续复制到另一块上面,然后把使用过的内存空间一次清理掉
复制算法的代价就是将内存缩小为原来的一半。
现在虚拟机都是采用复制算法来回收新生代。
新生代的内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间。
每次使用 Eden 和一块 Survivor,当进行回收是,将 Eden 和 Survivor 中还存活的对象一次性复制到另一个 Survivor 空间上。然后,清理掉 Eden 和刚刚使用过的 Survivor 空间。
HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例为 8 : 1,这样每次新生代可用内存为整个新生代的 90% (10% + 80%),只有 10%的内存会被浪费。
3.3 标记-整理算法
标记-整理算法分为“标记”和“整理”两个阶段,执行过程如下图所示:
标记:首先标记出所有需要回收的对象
整理:让所有的存活的对象都向一端移动,然后直接清除掉边界以外的内存
3.4 分代收集算法
分代收集算法就是降 Java 堆分为新生代和老年代,根据其各自的特点采用最适当的收集算法。
新生代中大批对象死去,只有少量存活,就选用复制算法
老年代中对象存活几率高,没有额外的空间对它进行分配担保,就必须使用标记-清除或者标记-整理算法。
4. 垃圾回收器
JVM 垃圾收集器发展历程大致可以分为以下四个阶段: Serial(串行)收集器 -> Parallel(并行)收集器 -> CMS(并发)收集器 -> G1(并发)收集器
下图展示了 7 种作用域不同分代的收集器,如果两个收集器之间存在连续,就说明它们可以搭配使用。下面逐一介绍这些收集器的特性、基本原理和使用场景。
4.1 Serial 类收集器
Serial 类收集器是一个单线程的收集器:
它只会用单个收集线程去进行垃圾回收的工作
它在进行垃圾收集的时候会“Stop The World”暂停其他所有的工作表线程,直到它收集结束
Serial 收集器采取复制算法在新生代进行单线程的回收工作
Serial Old 收集器采取标记-整理算法在老年代进行单线程的回收工作
4.2 Parallel 类收集器
Parallel 类收集器就是 Serial 收集器的多线程版本:
它使用多个收集线程取进行垃圾回收工作
它在进行垃圾收集的时候也会“Stop The World”暂停其他所有的工作表线程,直到它收集结束
ParNew 收集器采取复制算法在新生代进行多线程的回收工作
Parallel Scavenge 收集器也是一个新生代收集器,不过它被称为“吞吐量优先”收集器,它提供了 2 个能精确控制吞吐量的参数:
-XX : MaxGCPauseMillis:控制最大垃圾收集停顿时间
-XX : GCTimeRatio : 直接设置吞吐量大小,垃圾收集时间占总时间的比率
Parallel Scavenge 收集器还有一个开关参数-XX: UseAdaptiveSizePolicy,打开这个开关后就不用手动指定新生代的大小(-Xmn),Eden 与 Survivor 区的比例(-XX:SurvivorRatio)等细节参数了,JVM 会动态调整这些参数已提供最合适的停顿时间或者最大吞吐量。
Parallel Old 收集器是 Parallel Scavenge 收集器的老年代版本,使用多线程和标记-整理算法在老年代进行垃圾回收。
4.3 CMS 收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它是一个基于标记-清除算法实现的,运作过程分为 4 个步骤:
初始标记(CMS initial mark): 需要“Stop The World”,仅仅只是标记下 GC Roots 能直接关联到的对象,速度很快
并发标记(CMS concurrent mark): CMS 线程与应用线程一起并发执行,从 GC Roots 开始对堆中对象进行可达性分析,找出存活对象,耗时较长
重新标记(CMS remark):重新标记就是为了修正并发标记期间因用户线程继续运作而导致标记产生变动的那一部分对象的标记记录,可以多线程并行
并发清除(CMS concurrent sweep):CMS 线程与应用线程一起并发执行,进行垃圾清除
CMS 收集器优点:并发收集、低停顿
CMS 的三个明显的缺点:
CMS 收集器对 CPU 的资源非常敏感。CPU 的数量较少不足 4 个(比如 2 个)时,CMS 对用户程序的影响就可能变的很大。
CMS 收集器无法处理浮动垃圾(Floating Carbage),可能出现"Concurrent Mode Failture"失败而导致产生另一次 Full GC 的产生。浮动垃圾就是并发清理阶段,用户线程产生的新垃圾。
CMS 是基于标记-清除算法的,收集结束后会有大量的空间碎片,就可能会在老年代中无法分配足够大的连续空间而不得不触发另一次 Full GC。
4.4 G1 收集器
同优秀的 CMS 一样,G1 也是关注最小停顿时间的垃圾回收器,也同样适合大尺寸堆内存,官方也推荐用 G1 来代替选择 CMS。
G1 收集器的最大特点就是引入了分区的思路,弱化了分代的概念
G1 从整体来看是基于标记-整理算法实现的,从局部(两个 Region 之间)来看是基于复制算法实现的
4.4.1 G1 相对于 CMS 的改进
G1 是基于标记-整理算法,不会产生空间碎片,在分配大的连续对象是不会因为无法得到连续空间而不得不提前触发一次 Full GC
停顿时间可控,G1 可以通过设置停顿时间来控制垃圾回收时间
并行与并发,G1 能更充分的利用 CPU,多核环境下的硬件优势来缩短 stop the world 的停顿时间
4.4.2 G1 与 CMS 的区别
(1)堆内存模型的不同
G1 之前的 JVM 堆内存模型,堆被分为新生代,老年代,永久代(1.8 之前,1.8 之后是元空间),新生代中又分为 Eden 和两个 Survivor 区。
G1 收集器的堆内存模型,堆被分为很多个大小连续的区域(Region),Region 的大小可以通过-XX: G1HeapRegionSize 参数指定,大小区间为[1M,32M]。
(2)应用分代的不同
G1 可以在新生代和老年代使用,而 CMS 只能在老年代使用。
(3)收集算法的不同
G1 是复制+标记-整理算法,CMS 是标记清除算法。
4.4.3 G1 收集器的工作流程
G1 收集器的工作流程大致分为如下几个步骤:
初始标记(Initial Marking): 需要“Stop The World”,仅仅只是标记下 GC Roots 能直接关联到的对象,速度很快
并发标记(Concurrent Marking): G1 线程与应用线程一起并发执行,从 GC Roots 开始对堆中对象进行可达性分析,找出存活对象,耗时较长
最终标记(Final Marking): 最终标记阶段则是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,需要“Stop The World”,可以多线程并行
筛选回收(Live Data Counting and Evacuation): 对各个 Region 的回收价值和成本进行排序,根据用户所期待的 GC 停顿时间制定回收计划。具体地,在后台维护一个优先队列,每次根据允许的收集停顿时间,优先回收价值最大的 Region。
4.4.4 G1 的 GC 模式
G1 提供了两种 GC 模式,Young GC 和 Mixed GC,两种都是完全 Stop The World 的
(1)YoungGC
在分配一般对象(非巨型对象)时,当所有的 Eden Region 使用达到最大阈值并且无法申请到足够内存时,会触发一次 YoungGC。
每次 YoungGC 会回收所有的 Eden 以及 Survivor 区,并且将存活对象复制到 Old 区以及另一部分的 Survivor。
(2)MixedGC
当越来越多的对象晋升到老年代 old region 时,为了避免堆内存被耗尽,虚拟机会触发一次 mixed gc,该算法并不是一个 old gc,除了回收整个 young region,还会回收一部分的 old region。这里需要注意:是一部分老年代,而不是全部老年代,可以选择哪些 old region 进行收集,从而可以对垃圾回收的耗时时间进行控制。G1 没有 fullGC 概念,需要 fullGC 时,调用 serialOldGC 进行全堆扫描(包括 eden、survivor、o、perm)。
评论