GC基础知识
1. 什么是垃圾?
各大主流语言怎么申请和释放内存的? C语言:malloc free C++:new delete Java:new _(自动回收)
? java自动回收内存有什么优点:
- 编程简单
系统不容易出错 (手动释放,容易犯 忘记回收和多次回收 的错误)
垃圾 没有任何引用指向一个或一系列相互引用的对象(内部循环引用但未提供给外部使用)。
内存泄漏 大量可回收内存没有及时回收,造成内存严重浪费。
内存溢出 内存满了 装不下
2. 如何找到垃圾?
2.1 引用计数
2.2 RootSearching 根可达算法
?Which instances are roots? (哪些实例可以作为GC ROOTS)
- JVM Statck 线程栈变量
- native method stack 本地方法指针
- static references in method area 方法区静态变量
- runtime constant pool 运动时常量池
- Clazz 类信息
引用类型
gc在标记对象是否为垃圾时,并不是没有引用就一定时垃圾,还要根据引用类型来区分。
- 强 宁可oom也不会回收该类型引用
- 软 内存不足时回收。可用用来实现内存敏感的高速缓存(硬盘读性能差但又占内存的数据)。
- 弱 只要下次gc必定回收
- 虚 任何时候都有可能被回收,主要用于测试对象是否已经从内存中删除,跟踪对象被gc回收的活动。
3. 常见垃圾回收算法
3.1 Mark-Sweep (标记清除)
3.2 Copying 复制算法
把内存分两块,每次只使用其中一块,gc发生时,把存放对象移动到内存的一边,然后把边界外的另一侧全部擦除。
优点:没有碎片
缺点:浪费空间,一部分空间永远无法使用。
3.3 Mark-Compact 标记压缩(标记整理)
在清除垃圾的同时,把存活对象整理到空位,使得存活对象区间和可用空间都尽可能聚集、连续。
压缩 将存活对象整理到更合适的位置
三色指针 白、黑、灰,白色未标记 ,灰色自身被标记而子引用未标记,黑色从自身及其子引用都被标记 (go的垃圾回收也用这个)
优点:效果最好,即不会产生内存碎片,也不会浪费空间
缺点:性能偏低。压缩时要应对并发问题。
- 初始标记 找到所有gc root,stw
- 并发标记 找到所有垃圾
- 最终标记 纠错 stw
- 并发整理
缺点:
- gc时总是需要全量扫描分代内存区,不适合超大内存
- 性能不佳、incrementalUpdate 解决漏标问题时,会重复标记,造成浪费。
哪上图左,发生了漏标,在整个gc rootSearching过程中,始终没有处理过D, 也就是说D被最终认为与GCRoots无关联,也就是说会把D误当成垃圾,这就是漏标和误回收。
解决办法 :
- cms -> incrmentalUpdate 增量更新 关注引用的增加,当发现A的引用数量增加时,会把A重置为灰色,再次扫描标记
- 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 新生代+老年代+元数据区
分代逻辑
- 新生代:老年代一般=1:3 , eden:survivor=8:1
- 复制算法和分代年龄 ;每次minor gc时:
- 把suvivor1 复制到survivor2, 对象gc年龄+1,分代年龄大于15(老的回收器15,cms=6)的对象 ,将被移到到老年代
- 把eden中存活对象复杂到survivor1
- 空间担保策略:大对象无法进入survivor,将直接进入老年代
eden区大对象什么时候进入old区:
-XX:PretenureSizeThreshold=0 默认是0,也就说不论任何对象都要先进入eden区,除非eden区装不下;可以通过设置该值来定义大对象阈值,比如超过1M直接进tenure.
5. 常见的垃圾收集器
Serial (young) / Serial Old
STW, 然后串行单线程回收
仅使用于客户端的垃圾回收,在单机情况下,serial gc效果还是可以的。
Parallel Scavenge(young)+ Serial Old /Parallel Old
STW,然后串行多线程回收
一般配合Paralle Old 或Seial Old
jdk1.8 默认gc回收器 ps+parallel old, 因此gc调优也主要是针对该组合
ParNew (young) + CMS (old)
parNew和ps 原理一样,但主要用于配置CMS使用
CMS (stw<200ms)
cms关注吞吐量。 单位时间内gc停顿时间越少,吞吐量越高
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停顿时间,则时保持高吞吐。
逻辑分代 分治法
哪块(region)快满了 就回收某一块就可以了.
在collection_set中记录哪个region的垃圾比较多 需要回收。把在使用的区(单元格)最终存活的对象复制到其他区,然后清除原region.
复制算法效率比较高,而且直接复制到整理后的空闲region, 不产生碎片(使得正在使用的内存尽可能聚集、连续,那么可用的内存空间就是连续的一整块)。
collection_set 占用堆空间的1%不到
特点
- 每个区都即可以是年轻代,也可以是老年代,但同一时刻只能属于某个代
- 优先回收垃圾最多的region
- 自带压缩 gc回收时,把存活对象复制到另一个区的过程,就伴随着压缩,每个区大小不同,从1-32M,但都是2的n次方
- 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吗?
主要参数
- -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执行时机和安全点
安全点
程序只有在特定位置才能执行GC, 这些位置就叫 安全点 - SafePoint
安全点如果过少会gc异常,安全点过多又影响程序运行时性能。选取安全点的时候,通常会根据 是否具有让程序长时间执行 的特征,选择指令执行时间比较长的点作为安全点。
有哪些安全点?
首先中断所有线程,如果有线程不在安全点,就恢复线程,让不在安全点的线程跑到安全点。
- 主动式中断:
设置一个中断标志,各个线程执行到SF的时候主动轮询这个标志,如果中断标志为真,则将自己主动挂起。
安全区域 safe region
如果线程不执行,例如处于sleep或blocked状态,无法响应jvm的gc中断请求,线程可能在gc时非安全地运行。这时候就需要安全区域来运行。
安全区域: 在一个代码片段中,对象的引用关系不会发生变化 ,在这个区域内的任何位置都可以开始gc.
如何在安全区域执行GC
- 当线程运行到安全区域时,首先标记进入,如果这段时间发生gc,jvm会忽略对该线程做gc中断请求,认为其是gc安全的;
- 当线程即将离开时,会检查jvm是否完成gc,如果完成 ,则继续运行,否则,线程要等待gc完成才能离开。
拓展
?golang是否有垃圾回收器
有 而且stw比较严重
?有没有一种语言既不需要程序员手动回收垃圾,也不需要gc回收器, 也没有stw
有 Rust