参考:
- 《深入理解 JVM&G1 GC》- 周明耀[略过 3、5、7 章]
- 《垃圾回收的算法与实现》
以 JVM 来说,JVM 的的实现是由 c/c艹实现的,众所周知Java 是不需要手动管理内存的,因为内存的分配和回收都是依靠底层的 c/c艹,对于 Java 是完全透明的。
GC 主要是算法上的事,分为可达性分析,内存的管理区域划分(内存分代),内存的回收机制。
所有的GC 在进行 GC 时,都会进入一个 Stop The Word(STW) 的时刻。因为在 GC 的时候,如果内存还在变换的话,会影响 GC 回收。
Stop The Word:为了防止 GC 线程在进行垃圾回收的时候 mutator还在不断得生成垃圾,因此在进行 GC 的时候需要将 mutator 的线程挂起,这个时间也称为暂停时间。mutator 的线程在需要挂起的时候不会马上就挂起,会执行到该线程的 safepoint 才会进行挂起。等mutator 的所有线程都执行到 safepoint 并且挂起时,gc 线程就会开始工作。
下面整篇讨论的都是 JVM 的 GC。
- 虚拟机会单独运行一个线程专门用来 GC。
- HotSpot 虚拟机中,Java 的实例在内存中的布局分为对象头、实例数据、对齐填充。对象头包含实例在运行时的标记。如_mark(如哈希、GC 分代、锁标志),_metadata(指向类元数据)。
- 内联:方法调用由方法体直接替换,避免开辟新的函数栈。
- 垃圾标记算法:
- 计数算法:
- 无法解决循环引用
- 根搜索算法:
- 根对象集合包括(Java 栈内对象引用、本地方法栈对象引用、运行时常量池对象引用、方法区类静态属性对象引用、类对象的 Class 对象)
- 当一个对象要被回收的时候,会调用其 finalize()方法并且终身只调一次。如果在 finalize()中没有自救,则会被回收。jvm 会对热点代码进行本地编译,而jvm 启动的时候可以设置热点代码的阈值,当方法调用超过这个阈值,则会启动编译。可以猜测内部对于方法有一个方法执行的计数器。因此才可以确保 finalize()只调用一次。
- 计数算法:
- 比较常见的有标记-清除(Mark-Sweep)、复制(Copying)、标记-压缩(Mark-Compact)
-
标记-清除。
- collector 从 mutator 根对象开始遍历,对可访问对象的 header 标记为可达。随后对 heap 进行线性遍历,回收不可达对象。
- 效率低下,容易产生内存碎片。
-
复制。
- 内存分为两块,每次保持一块空闲。会将另外一块存活对象复制到空闲块。清除正在使用的内存块对象。交换内存块角色。
- 内存使用率折半。
- 因为 JVM 绝大对象都是瞬时状态,生命周期非常短暂。Copying 广泛适用于年轻代。
-
标记-压缩
- 标记出垃圾对象后,将存活对象移动到规整联系的空间,执行 Full GC。此时已用和未用内存各自一边。分配对象时使用指针碰撞(Bump the Pointer)进行内存分配。
-
增量算法(Incremental Collecting)
- 为了减少 STW,GC 线程一次只收集一小片区域,与应用程序线程切换。反复进行。但是增加了线程切换和上下文转换消耗,造成吞吐量下降。
-
分代收集算法(Generational Collection)
- 将内存区间根据对象特点划分几块,根据每一块内存区的特点,选取不同的回收算法提供垃圾回收效率。
- 一般讲对象分为年轻代、老年代、持久代。不同的生命周期对象使用不同的算法。
-
GC
- GC 的工作任务分为内存的动态分配和垃圾回收。
- 现在几乎所有的 GC 都采用分代手机算法。Java 堆分为年轻代(YoungGen)和老年代(OldGen),年轻代又可以分为 Eden、From Survivor、To Survivor。
- 内存空间如何划分完全依赖 GC 设计。
- 评估 GC 的性能:吞吐量、垃圾收集开销、 暂停时间、收集频率、堆空间、快速
-
JVM 的 GC
- Serial/Serial Old 收集器、ParNew 收集器、Parallel/Parallel Old 收集器、CMS(Concurrent-Mask-Sweep)收集器、G1(Garbage-First)收集器。
- 分类
- 按线程:串行 Collector 和并行 Collector。
- 按工作模式:并发 Collector 和 独占式 Collector
- 处理方式:压缩式 Collector 和非压缩式 Collector
- 按工作内存区间:年轻代 Collector 和老年代 Collector
-
Serial GC(串行收集器)
- 作用于年轻代,采用复制算法、串行回收和 STW机制。默认作为 HotSpot Client 模式下的年轻代 Collector
- 还提供 Serial Old Collector 供老年代使用。使用标记-压缩、串行回收和 STW 机制
-
- ParNew GC
- ParNew GC 是 Serial GC 的多线程版。
- 采用并行回收,其余与 Serial GC 一致。
- 单线程下,Serial GC 效率比 ParNew GC 效率高,因为后者需要线程切换
- Parallel GC
- 采用复制算法、并行回收和 STW 机制。
- 可以控制吞吐量,被称为吞吐量优先的垃圾收集器。并且可以控制 GC 执行频率和 STW 的暂停时间阈值。PS:在 GC 中,吞吐量和低延迟是矛盾的。
- Parallel Old GC 采用标记-压缩算法、并行回收、STW机制。
- 吞吐量优先的使用场景,可以使用 Parallel GC 和 Parallel Old GC 组合。
- CMS GC(Concurrent-Marking-Sweep)
- 基于并行回收,老年代 GC。低延迟。采用标记-清除、STW 机制。
- CMS 执行阶段:初始标记(Initial-Mark)、并发标记(Concurrent-Mark)、再次标记(Remark)、并发清除(Concurrent-Sweep)
- STW -> initial-Mark - 恢复应用线程 ->并发标记(GC 线程与应用线程同时运行)(Concurrent-Mark) -> STW -> Remark -> Concurrent-Sweep
- Serial Old GC 和 Parallel Old GC 使用标记-压缩避免 Full GC 产生碎片,然后使用指针碰撞(Bump the Pointer)分内新对象内存。CMS 使用标记-清除,因此只能选择空闲列表(Free List)执行内存分配。
- CMS 可以设置 Full GC 后进行压缩整理,会增加停顿时间。也可以设置 N 次 Full GC 后进行压缩整理。
- CMS与应用线程并发执行,相互抢占 CPU,因此会影响吞吐量。
- 因为 CMS 在工作的时候,mutator 还在产生垃圾,而这些垃圾只能在下次 GC 清除。因此可能会造成本次 GC 失败,失败后将会启动 Serial Old GC 进行。
- Garbage First(G1) GC
- G1 的核心在于将内存分为若干大小相等的 Region,每个 Region
不指定分代,在运行时动态指定。并且内存回收管理以 Region 为单位。譬如某一 Region 前一刻是存储年轻代,当被回收后,后一刻可能就被用来存储老年代了。 - 进行 GC 时,G1 计算每个 Old 内存活对象数量并且在 sweep 阶段进行评分。开始混合回收时,年轻代尽量回收,老年代根据评分选择性回收。
- 每一个 Region 都有个一个 RemberedSet(RSet),维护了其他 Region 对该 Region 中对象的引用。
- 有一个 Collection Set(CSet),包含了GC 要回收的 Region 的集合。
- GC 的过程分为:初始标记阶段、根区间扫描阶段、并行标记阶段、重标记阶段、清除阶段
- G1 的核心在于将内存分为若干大小相等的 Region,每个 Region
网友评论