GC:Garbage Collectors 垃圾回收器
C/C++,手动回收内存;难调试、门槛高。忘记回收、多次回收等问题
Java、Golang等,有垃圾回收器:自动回收,技术门槛降低
一、如何定位垃圾?
https://www.infoq.cn/article/3wyretkqrhivtw4frmr3
https://juejin.cn/post/7123853933801373733
1、引用计数法
无法解决循环调用的问题
2、根可达分析算法
一般需要STW(stop-the-world)
二、哪些内存需要回收?
运行期间动态创建的、内存分配和回收具有不确定性的:堆、常量池(方法区)
三、常见的GC算法
标记清除算法、标记复制法、标记压缩法、分代收集算法
1、mark-sweep 标记清除法(三色标记法)
位置不连续 产生碎片 效率偏低(两遍扫描)
黑色区域表示待清理的垃圾对象,标记出来后直接清空。该方法简单快速,但是缺点也很明显,会产生很多内存碎片。
2、mark-copy 标记复制法
没有碎片,但浪费空间(只能使用50%的内存)
将内存对半分,总是保留一块空着(上图中的右侧),将左侧存活的对象(浅灰色区域)复制到右侧,然后左侧全部清空。避免了内存碎片问题,但是内存浪费很严重,相当于只能使用 50%的内存。
3、mark-compact 标记压缩法
没有碎片,效率偏低(两遍扫描,指针需要调整)
避免了上述两种算法的缺点,将垃圾对象清理掉后,同时将剩下的存活对象进行整理挪动(类似于 windows 的磁盘碎片整理),保证它们占用的空间连续,这样就避免了内存碎片问题,但是整理过程也会降低 GC 的效率。
4、generation-collect 分代收集算法
综合使用了标记清除、标记复制、标记压缩算法。堆内存逻辑分区:新生代(Young Generation)、老年代(Old Generation),Young Genaration 更是又细为分 eden,S0,S1 三个区。
YGC/FGC过程:
- 产生的新对象,放eden区
- 当 eden 区放不下时,就会发生 minor GC(也被称为 young GC):标记不可达对象,然后将可达对象移动到S0区,清空eden区(综合运用了“【标记-清理 eden】 + 【标记-复制 eden->s0】”算法)
- eden 如果又满了,再次触发 minor GC:先做标记,s0 区和 eden 区的存活对象,将直接搬到 s1 区。然后将 eden 和 s0 区的垃圾清理掉
- 对象在年青代的 3 个区(edge,s0,s1)之间,每次从 1 个区移到另 1 区,年龄+1,在 young 区达到一定的年龄阈值后,将晋升到老年代
- 如果老年代,最终也放满了,就会发生 major GC(即 Full GC),由于老年代的的对象通常会比较多,因为【标记-清理-整理(压缩)】的耗时通常会比较长,会让应用出现卡顿的现象,这也是为什么很多应用要优化,尽量避免或减少 Full GC 的原因。
JVM调优:参数指定内存大小
jdk8 开始,用 MetaSpace 区取代了 Perm 区(永久代),所以相应的 jvm 参数变成-XX:MetaspaceSize 及 -XX:MaxMetaspaceSize。
四、常见的GC
1、GC的演变过程(内存不断扩大)
常见组合:Serial+Serial Old【单线程】、PS+PO【多线程】 、ParNew+CMS
垃圾回收器 | 使用的GC算法 | 说明 |
---|---|---|
Serial | 标记-复制 | 【Y】单线程、会发生STW。早期单核 |
Serial Old | “标记-整理” | 【O】单线程、STW。耗时长 |
Parallel Scavenge | 标记-复制 | 【Y】ParNew 的升级版本(吞吐量优先的收集器,吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)),区别在于提供了两个参数:-XX:MaxGCPauseMillis 最大垃圾回收停顿时间;-XX:GCTimeRatio 垃圾回收时间与总时间占比 |
Parallel Old | “标记-整理” | 【O】Serial Old的多线程版本,STW |
ParNew | 标记-复制 | 【Y】Serial 的多线程版本,会 STW,多核机器 |
CMS | 标记清理,三色标记 | 【O】并发多线程,STW有所缩短。最大问题是会产生漏标 |
G1 | 标记-整理 | 不分代。并发多线程,解决了CMS的缺陷 |
ZGC | 不分代。目前最好用的GC。 | |
Epsilon | 不执行任何垃圾回收工作。一旦java的堆被耗尽,jvm就直接关闭。特殊场景:1.开发JVM的人debug使用;2.程序运行完都用不完内存,等程序运行完直接清理所有内存。 | |
shenandoah | 开源,成熟度也比较高。 |
![image-20230714092649450](D:\Users\helen.zeng\AppData\Roaming\Typora\typora-user-images\image-20230714092649450.png
2、Serial
蓝色是业务线程、黄色是GC线程(暂停所有业务线程来做垃圾回收处理)
3、ParNew
多线程,有STW。
4、CMS
CMS采用了多种方式尽可能降低GC的暂停时间,减少用户程序停顿。
停顿时间降低的同时牺牲了CPU吞吐量 。因为并发情况占用大量cpu资源
这是在停顿时间和性能间做出的取舍,可以简单理解为"空间(性能)"换时间
三色标记算法:黑色、灰色、白色
CMS:Concurrent Mark Sweep
1)Inital Mark 初始标记:主要是标记 GC Root 开始的下级(注:仅下一级)对象,这个过程会 STW,但是跟 GC Root 直接关联的下级对象不会很多,因此这个过程其实很快。
2)Concurrent Mark 并发标记:根据上一步的结果,继续向下标识所有关联的对象,直到这条链上的最尽头。这个过程是多线程的,虽然耗时理论上会比较长,但是其它工作线程并不会阻塞,没有 STW。
3)Remark 再标志:这是修正过程。为啥还要再标记一次?因为第 2 步并没有阻塞其它工作线程,其它线程在标识过程中,很有可能会产生新的垃圾。
4)Concurrent Sweep:并行清理,这里使用多线程以“Mark Sweep-标记清理”算法,把垃圾清掉,其它工作线程仍然能继续支行,不会造成卡顿。
5、G1:Garbage-First
G1最大的特点是引入分区的思路,弱化了分代的概念。物理不分代、逻辑分代。G1也不需要跟别的收集器一起配合使用,自己就可以搞定所有内存区域。
毛病:一次回收要把年轻代全回收完。
6、ZGC:
分页算法,完全不分代了,而且分区非常灵活。LoadBarrier(读屏障)。把比较满的分区做回收
G1算法通过只回收部分Region,避免了全堆扫描,改善了大堆下的停顿时间
7、Shenandoah
ColoredPointers + LoadBarrier(颜色指针算法+读屏障)
程序中最难调试的bug
- 野指针(不再指向任何对象的指针):同一个对象有两个指针,一个释放了,另一个不知道(还在使用),NPE
- 并发问题:多线程访问同一块内存空间