GC基础知识

1. 什么是垃圾?

各大主流语言怎么申请和释放内存的? C语言:malloc free C++:new delete Java:new _(自动回收)

? java自动回收内存有什么优点:

  • 编程简单
  • 系统不容易出错 (手动释放,容易犯 忘记回收和多次回收 的错误)

    垃圾 没有任何引用指向一个或一系列相互引用的对象(内部循环引用但未提供给外部使用)。
    内存泄漏 大量可回收内存没有及时回收,造成内存严重浪费。
    内存溢出 内存满了 装不下

2. 如何找到垃圾?

2.1 引用计数

弊端:1. 无法回收循环引用的垃圾

2.2 RootSearching 根可达算法

?Which instances are roots? (哪些实例可以作为GC ROOTS)

  • JVM Statck 线程栈变量
  • native method stack 本地方法指针
  • static references in method area 方法区静态变量
  • runtime constant pool 运动时常量池
  • Clazz 类信息

一句话总结:凡是和main方法相关的直接或间接引用的对象。

引用类型

gc在标记对象是否为垃圾时,并不是没有引用就一定时垃圾,还要根据引用类型来区分。

  • 强 宁可oom也不会回收该类型引用
  • 软 内存不足时回收。可用用来实现内存敏感的高速缓存(硬盘读性能差但又占内存的数据)。
  • 弱 只要下次gc必定回收
  • 虚 任何时候都有可能被回收,主要用于测试对象是否已经从内存中删除,跟踪对象被gc回收的活动。

3. 常见垃圾回收算法

3.1 Mark-Sweep (标记清除)

标记可回收的内存区,擦除。
image.png
缺点:位置不连续,产生碎片

3.2 Copying 复制算法

把内存分两块,每次只使用其中一块,gc发生时,把存放对象移动到内存的一边,然后把边界外的另一侧全部擦除。
image.png
优点:没有碎片
缺点:浪费空间,一部分空间永远无法使用。

3.3 Mark-Compact 标记压缩(标记整理)

在清除垃圾的同时,把存活对象整理到空位,使得存活对象区间和可用空间都尽可能聚集、连续。
image.png
压缩 将存活对象整理到更合适的位置
三色指针 白、黑、灰,白色未标记 ,灰色自身被标记而子引用未标记,黑色从自身及其子引用都被标记 (go的垃圾回收也用这个)
优点:效果最好,即不会产生内存碎片,也不会浪费空间
缺点:性能偏低。压缩时要应对并发问题。

  1. 初始标记 找到所有gc root,stw
  2. 并发标记 找到所有垃圾
  3. 最终标记 纠错 stw
  4. 并发整理

缺点:

  1. gc时总是需要全量扫描分代内存区,不适合超大内存
  2. 性能不佳、incrementalUpdate 解决漏标问题时,会重复标记,造成浪费。

哪上图左,发生了漏标,在整个gc rootSearching过程中,始终没有处理过D, 也就是说D被最终认为与GCRoots无关联,也就是说会把D误当成垃圾,这就是漏标和误回收。
解决办法 :

  1. cms -> incrmentalUpdate 增量更新 关注引用的增加,当发现A的引用数量增加时,会把A重置为灰色,再次扫描标记
  2. G1 -> SATB (snapshot at begining) 关注引用的删除,当引用变更时,将引用关系变更维护在队列中,当二次标记(B从色-黑)时,会去队列查B的引用关系变更,最后精准标记。

4.4 G1 SATB

G1对CMS误标的问题的解决方案:把对象引用的变更维护到队列里,如上示例,在gc开始标记B的孩子的时候,去队列里找B的引用变更。

写屏障 类似于spring aop,在对象成员变量变更指令前后加入自己的代码。
putfield指令(.class jvm指令)前加一个soreLoad, 使得为成员变量赋值前,调用update_harrier_set_pre设置更新前置屏障的方法里,调用了G1SATBCardTaableModRefBS::enqueue 将引用变更入队。
因为这个缘故,使用了G1作为垃圾收集器的应用,比使用其他垃圾回收器运行的效率要低3-4%。(稍微提升下cpu配置就可以把性能拉回来,而解决了误标才是关键)

4. 垃圾分代回收

不同的内存区域使用相应的垃圾回收算法

新生代 young/minor

特点:对象朝生夕死,存活率低
使用复制算法,效率高,浪费空间很少,一般eden/suvivor=8
young gc或者 minor gc。 ygc也会有STW,但很多回收器会把stw时间降到可以忽略

老年代 old/major

特点:垃圾少
一般使用标记-整理, g1使用copy
老年代满了,会触发full gc (major gc)
gc调优的重点就是减少full gc (full gc 会触发STW,且性能不佳)

永久代/元数据区 permenant/metadata space

放常量、类信息
永久代必须指定大小限制,元数据可以设置上限也可以不设置
字符串常量1.7放在永久代,1.8放在堆上。

各版本jvm分代组合
  • 1.7 新生代+老年代+永久代
  • 1.8 新生代+老年代+元数据区

分代逻辑

image.png

  1. 新生代:老年代一般=1:3 , eden:survivor=8:1
  2. 复制算法和分代年龄 ;每次minor gc时:
    1. 把suvivor1 复制到survivor2, 对象gc年龄+1,分代年龄大于15(老的回收器15,cms=6)的对象 ,将被移到到老年代
    2. 把eden中存活对象复杂到survivor1
  3. 空间担保策略:大对象无法进入survivor,将直接进入老年代

eden区大对象什么时候进入old区:
-XX:PretenureSizeThreshold=0 默认是0,也就说不论任何对象都要先进入eden区,除非eden区装不下;可以通过设置该值来定义大对象阈值,比如超过1M直接进tenure.

5. 常见的垃圾收集器

image.png

Serial (young) / Serial Old

image.png
STW, 然后串行单线程回收
仅使用于客户端的垃圾回收,在单机情况下,serial gc效果还是可以的。

Parallel Scavenge(young)+ Serial Old /Parallel Old

image.png
STW,然后串行多线程回收
一般配合Paralle Old 或Seial Old
jdk1.8 默认gc回收器 ps+parallel old, 因此gc调优也主要是针对该组合

ParNew (young) + CMS (old)

image.png
parNew和ps 原理一样,但主要用于配置CMS使用

CMS (stw<200ms)

cms关注吞吐量。 单位时间内gc停顿时间越少,吞吐量越高
image.png
CMS的独特之处就在于,在并发标记和并发清理两个阶段,真正实现了并发执行,降低STW的时间到200ms以内(官方说法);

没有任何一种gc可以在任何情况下性能最优,Serial Old在老机器低并发场景下性能最好;ParNew+cms 10g以内的内存 垃圾回收不成问题而且相较g1省内存。G1能hold住几百G的大内存,ZGC 1个T的内存空间不成问题。

G1和ZGC

G1 (stw<10ms)

The Garbage First Gabage Collector (G1 GC) is the low-pause,server-style generational garbage collector for java hotspot vm.

G1的设计原则是”首先收集尽可能多的垃圾(Garbage First)“。将内存分无数个region,并不会等内存不足才回收,而是在内部采用启发式算法,主动去回收垃圾最多的region.
G1的收集都是STW的,采用混合收集的方式。

适用于多核心、大内存机器,可指定gc停顿时间,则时保持高吞吐。
image.png
逻辑分代 分治法
哪块(region)快满了 就回收某一块就可以了.
在collection_set中记录哪个region的垃圾比较多 需要回收。把在使用的区(单元格)最终存活的对象复制到其他区,然后清除原region.
复制算法效率比较高,而且直接复制到整理后的空闲region, 不产生碎片(使得正在使用的内存尽可能聚集、连续,那么可用的内存空间就是连续的一整块)。

collection_set 占用堆空间的1%不到

特点
  1. 每个区都即可以是年轻代,也可以是老年代,但同一时刻只能属于某个代
  2. 优先回收垃圾最多的region
  3. 自带压缩 gc回收时,把存活对象复制到另一个区的过程,就伴随着压缩,每个区大小不同,从1-32M,但都是2的n次方
  4. GC形式有两种:1. YCG 2.mixedGC 当内存使用率大于45%时,会触发MixedGC 混合回收,ygc+类似于cms的并发标记回收

并发标记回收:

  • 初始标记 找到所有gc root,stw
  • 并发标记 找到所有垃圾

    三色标记

  • 最终标记 纠错 stw

  • (筛选回收)复制/清除 stw

年轻代比例弹性变化 ,年轻化占比多少是G1系统控制的(5%-60%),不需要程序员调节。

优势:每次只清理一部分,而不是full gc, 由此来保证每次gc停顿时间不会太长。

G1的吞吐量比PN+CMS低10-15%,但响应时间提高了好几倍。(资源换时间)

?G1有full gc吗?

有 。因此g1的调优目标是尽量不要产生full gc

主要参数
  • -XX:+UseG1GC 指定使用GC
  • -XX:InitiatingHeapOccupancyPercent 整个堆占用率达到多少,开始并发标记阶段
  • -XX:MaxGCPauseMillis 指定停顿时间 默认200ms
  • -XX:G1HeapRegionSize 每个region大小 范围1-32M

ZGC(stw<1ms) 和 Shenandoah

两个性能差不多,stw时间都很短,且作用于1T以上超大内存的服务器上时,性能仍然优良。其他不了解。

Epslion

不实际执行垃圾回收,一般用于debug调试,也可用于内存比较大,程序会在预期时间内不超出使用内存的情况下执行完毕,不需要回收垃圾,则可使用epslion减少回收垃圾带来的性能消耗。

6. 生产环境下的GC

JVM参数分类

标准:-开头,所有hotspot 都支持 非标准:-X开头,特定版本的HotSpot支持 不稳定:-xx开头,下个版本可能取消

-XX: +PrintCommandLineFlags 系统启动时打印命令行参数

7. GC执行时机和安全点

image.png

安全点

程序只有在特定位置才能执行GC, 这些位置就叫 安全点 - SafePoint
安全点如果过少会gc异常,安全点过多又影响程序运行时性能。选取安全点的时候,通常会根据 是否具有让程序长时间执行 的特征,选择指令执行时间比较长的点作为安全点。

有哪些安全点?
  • 循环的末尾
  • 方法临返回前
  • 调用方法后
  • 抛异常的位置

    GC线程的中断策略

    如何在发生gc时,检查所有线程都跑到最近的安全点停顿下来了呢?

  • 抢先式中断(目前没有虚拟机采用)

首先中断所有线程,如果有线程不在安全点,就恢复线程,让不在安全点的线程跑到安全点。

  • 主动式中断:

设置一个中断标志,各个线程执行到SF的时候主动轮询这个标志,如果中断标志为真,则将自己主动挂起。

安全区域 safe region

如果线程不执行,例如处于sleep或blocked状态,无法响应jvm的gc中断请求,线程可能在gc时非安全地运行。这时候就需要安全区域来运行。
安全区域: 在一个代码片段中,对象的引用关系不会发生变化 ,在这个区域内的任何位置都可以开始gc.

如何在安全区域执行GC
  1. 当线程运行到安全区域时,首先标记进入,如果这段时间发生gc,jvm会忽略对该线程做gc中断请求,认为其是gc安全的;
  2. 当线程即将离开时,会检查jvm是否完成gc,如果完成 ,则继续运行,否则,线程要等待gc完成才能离开。

拓展

?golang是否有垃圾回收器
有 而且stw比较严重
?有没有一种语言既不需要程序员手动回收垃圾,也不需要gc回收器, 也没有stw
有 Rust