引言:
Java最早做了垃圾回收机制,也就是我们说的GC,jvm通过垃圾回收机器,也随着jdk版本的迭代,不断的再进步。
jvm中,最重要的就是垃圾回收器这里了,这篇文章将专门对垃圾回收做出讲解,包括基础认识,垃圾回收算法,垃圾回收器的更迭和将来趋势以及选择。
为了我们开发不被内存泄漏和内存溢出困扰,jvm做了自动回收机制,指的是不用的垃圾对象被标记,然后被回收,释放占用的内存空间,但是万事皆有利弊,垃圾回收器不一定完全解决内存泄漏问题,编码时还要注意规范。
而且垃圾回收还会占用系统资源,影响程序的性能,回收的时候还会触发STW(stop the world),导致程序卡顿。
这个问题问的就是垃圾回收的方式,也就是垃圾回收算法,
有:标记清除,标记复制,标记整理。
垃圾回收算法是垃圾回收期的方法论,而垃圾回收器是垃圾回收算法的具体落地实现。
方法1:引用计数法
方法2:可达性分析算法
就是一个对象被其他对象持有,一个变量就+1,两个引用就+2,没有引用,为0的话,就是死对象了,可以被回收了。
引用对象有个问题,
就是循环引用。
public class Main {
public static void main(String[] args) {
A a = new A();
B b = new B();
a.setB(b);
b.setA(a);
a = null;
b = null;
System.gc();
}
}
class A {
private B b;
public void setB(B b) {
this.b = b;
}
}
class B {
private A a;
public void setA(A a) {
this.a = a;
}
}
看这个demo,虽然虚拟机栈中,a,b都指向了null,但是对于堆中的对象A和对象B,他们是互相持有,形成一个闭环,这就出现了循环引用问题。
如何解决,就是另一种判断垃圾的方法
这个方法,大家可以理解为一个树,但是又不是树,和树一样有根节点,但是长出来的是一个图,
从GC ROOT能推到的对象就是可用的,这样即使对象之间互相引用,也不会出现循环引用导致不能被回收的问题
GCROOT指的就是,比如,User user = new User();
user就是GCROOT
JVM中GC Roots包括一下几种,
无非就是对象的引用的地方,
虚拟机栈:指的是栈帧中,本地变量表中引用的对象
方法区:静态变量引用的对象,jdk1.7以后,静态变量引用的对象从方法区移动到了堆中
:常量引用的对象,字符串常量池也从jdk1.7以后,由方法区移动到了堆中。
本地方法栈:JNI,JNI就是Native方法,引用的对象,
总结:就是指向堆中的变量和指针。
标记回收算法,有三种,
最基础的是标记清除算法。但是因为内存碎片问题,于是出现,标记复制算法,然而标记复制算法由于需要额外空间,就诞生了标记整理算法,也叫标记压缩算法。
算法并无好坏,要根据不同的算法来。
这个算法,就是把死忘的对象标记出来,然后清除掉,那么清除完之后,就会出现碎片问题,因为存活的对象和死亡的对象在内存中是掺杂一起的,把死亡对象清走,留下的就是零零碎碎的位置。
这个算法,就是为了避免内存碎片问题,标记存活的对象,移动到另一片空闲的位置,空闲区域,之后,以新移动的空间作为现在的活动区,之前活动区域,直接全清理掉。作为空闲区,方便下一次GC。
这种算法,不会像标记清除算法那样,有内存碎片问题,且不会像标记复制算法那样,需要额外的空间。但是移动的操作,算法的效率就比标记辅助慢些,所以没有万金油,要根据合适的场景。
这里介绍一下,堆空间,如何选择垃圾回收算法的
新生代,98%的对象都撑不过一次GC,所以新生代的死亡时很快的,存活的对象很少,这是我们想到标记复制算法!,复制存活的,就很符合这个算法,所以新生代采用的是标记复制算法,所以有两个分区,其中有一个分区,是空闲的,方便下次gc,复制存货对象到里面,就这一,存活的对象,在一次一次的GC后,到了15岁,就会进入Old区,
Old区,老年代,他没有进行分区,采用的是标记整理方法,Old区存活的对象很多,都是老油条,自然不适合用标记复制的方法,又不想用标记清除产生大量的内存碎片,就采用的标记整理算法。
上面我们看堆空间采用垃圾回收期,采用新生代和老年代,采用的就是分代回收算法的思想,年轻代存活时间短,死的快,就采用高频回收,老年代都是老油条,老不死的,就进行低频回收。
分代算法根据对象特点,年轻代适合标记复制算法,刚才也讲到了,只复制少量存活的,老年代则适合标记清除,标记压
缩算法,因为存活的是多数的,清除少量死亡的。
通过新生代和老年代的划分,使得MinorGC的频率更高,早早的把死的快的对象回收完,减少Old区内存不足,发生FullGC的频率。
很多人对GC的概念,Minor GC ,YongGC,FullGC是什么不知道,为了下面垃圾回收器的讲解,这里科普一下
Young GC Minor GC
Eden、s1、s2的清理,都是发生在新生代
Major GC ,Old GC
Full GC,清理整个堆空间,包括年轻代和老年代,理解
触发FullGC 的场景
这个问题在国内很混淆,我是这样理解的,当满足MinorGC 进入平均大小小于老年代的可用内存之后,会触发OldGC ,可以这么理解,OldGC大部分是由youngGC触发的,其实就是FullGC,我们只需要关注YoungGC,FullGC 就可以了,
其实也就是关注STW的长短,OldGC,也就是MajorGC速度要比MinerGC/YoungGC慢上10倍不止!
STOP THE WORLD
垃圾回收过程中,用户线程运行到安全点(save Point),进入挂起状态,对外表现就是卡顿,
这个安全点,就是操作系统中,进行中断前保存的寄存器和PC程序计数器
所以应当经量减少FullGC发生的次数
随着jdk对性能的追求,jdk版本更迭,垃圾回收器也在不断的变化,
jdk8盛行的时候,ParalNew与CMS一度成为面试必考的内容,随着时代的进步,出现了整堆垃圾回收器,G1与ZGC,对于旧版本,我们了解即可,JDK11属于长期支持(Long Term Support)LTS版本,其默认的G1,将是23年~25年的主流,不要说什么,jdk8不可能动摇,公司都是向钱看的,提高性能就是省钱,我们需要做的就是跟着时代进步,so,要做的就是,认识旧的,熟悉新的!
话不多说,上图,一图胜千言!
垃圾收集器分类
注意
-XX:+PrintCommandLineFlags
查看命令行相关参数(包含使用的垃圾收集器)可以看到,以前常问的CMS,也在JDK14也给废弃了,曾几何时,jvm调优常用的ParNew和CMS组合,竟然被淘汰
说到这里,我们做的就是认识这些老的垃圾收集器,熟悉新一代收集器G1与ZGC,当然G1就可以了,ZGC虽然强大,但是指不定出现更强大的,近几年,还是G1将成为主流。
在讲这些垃圾回收器之前,先看下垃圾收集器应当关注哪些地方,
这么多垃圾回收器,我们要区分差别,就要看一些指标
运行程序占总运行时间的比例
虚拟机100分钟,垃圾回收期花掉1分钟,吞吐量就是99%
就是STW
gc时,程序被暂停的时间,比如100ms,这100ms程序是停止工作的
就是垃圾回收触发gc次数,当然是越少越好
需要重点关注的就是,吞吐量与暂停时间。
这类垃圾收集器就比较鸡肋了,在单核cpu环境比较高效,适用于小型的应用,一般javaweb,springboot不会采用这类收集器
算法,新生代采用标记复制,老年代采用标记整理
上面那张图,
Serial一般和CMS进行配合,或者和Serial Old,
Serial Old是Serial收集器的老年代版本,jdk5之前和Parallel配合使用,或者作为CMS的备选方案
新生代采用单线程进行标记复制算法的回收,老年代也是单线程,进行标记整理算法,都会发生STW
工作在年轻代上,和ParNew的区别就是将穿行改为了并行,其他基本和Serial一样,应用大型项目,单核比Serial低
算法:新生代采用复制算法,老年代采用标记整理算法
新生代多个线程进行标记复制算法,老年代取与对应的回收器
新生代垃圾收集器,
新生代采用标记复制,老年代采用标记整理
算法:新生代采用标记复制算法,老年代采用标记整理算法,
新生代多个线程进行标记复制,老年代标记整理也是多个线程,并行处理,都会发生STW
Parallel与ParNew区别
-XX:+UseParallelGC仅仅对年轻代有效,不可以和CMS收集器同时使用
-XX:+UseParNewGC设置年轻代为多线程收集,可以和CMS配合使用
全程交错Concurrent Mark Sweep,是一款并发的,使用标记清除算法!的垃圾回收期
针对老年代使用的
适用于对响应要求较高的应用程序
整个过程分四步
初始标记、并发标记、重新标记、并发清除
老年代中,初始标记,单线程进行标记与GCROOT直接关联的对象,会发生STW,
并发标记,单个线程处理,此时不会影响用户线程的执行,不会STW
重新标记:处理并发标记出现错误标记的,多个线程重新标记,会发生STW
并发清理:线程与线程并发清理,此时不会STW
上面的很乱,主要区分就是垃圾回收器在新生代和老年代,独特的就是CMS是老年代的垃圾回收期,采用的标记清除,但是是并发的标记清除
上面分为新生代垃圾收集器,老年代垃圾收集器,而G1和ZGC属于整堆垃圾收集器,不划分制约,一个垃圾回收期就可以了
Garbage First所以叫做G1
在JDK9的时候成为默认的垃圾收集器
将内存划分多个独立的区域Region,取消了物理上年轻代与老年代的物理划分,但是保留了逻辑上的年轻代与老年代,新增了一个H区存放大对象,
分代没有固定思想。年轻代与老年代的大小式动态分配的
局部采用标记复制算法,整体采用标记整理算法,没有内存碎片问题
通过动态的判断进行垃圾回收,引入MixedGC,动态回收,尽量避免FullGC的发生,从而提高响应速度
这些很关键,大家多看多回顾
由于G1的特性,动态分配年轻代与老年代空间的 ,所以不手工设置年轻代大小,比如使用 -Xmn 选项或 -XX:NewRatio 等设置年轻代大小
暂停时间的目标不要设置太小,通常100~200ms比较合理,太短的化,每次只能回收很小一部分,可能导致垃圾堆积
我把这里放到最上面,因为这是学完之后总结的,大家可以从这里来带着疑问往下阅读。
Region进行了分区处理,但是Region还是保留了类型的
Eden,Survivor、Old还是和之前一样,有这样分代的思想,有一个H区,专门存放大对象,H区也是属于老年代的
关于H区的特点,看上面的脑图即可
注意:
1:是动态变化的,可能垃圾回收之前是年轻代,护手之后变成老年代,这样回收更精细化
2:整体标记整理,局部标记复制,不会产生内存碎片
除了YongGC,FullGC,还有混合GC:MixedGC
MixedGC是多数对象转移到old 区的时候,避免内存不足,提前进行回收一部分Old区,来避免未来FullGC的发生
使用
-XX:InitiatingHeapOccupancyPercent=n
决定默认:45%,即 当老年代大小占整个堆大小百分比达到该阀值时触发
此外,不仅新增了这一个MixedGC,
YoungGC与之前也有不同,不再等Eden区满,而是预估回收需要的时间,接近这个时间,通过动态的计算,回收ROI(投入产出比更高的对象),
如果接近参数-XX:MaxGCPauseMills
设定的值,会触发Young GC
通过上述动态的垃圾回收策略,避免FullGC的发生,提高应用程序的响应速度。
MixedGC与YoungGC同样,动态的计算ROI,因为MixedGC也是包括YoungGC的,总之动态计算式G1的一个很厉害的特性,计算每个Region回收的ROI。
比如
那么只选一个的化,就会选择Region2
G1的MixGC垃圾收集分为下面几个步骤
-XX:MaxGCPauseMillis
制定计划,回收ROI最符合预期的进行回收说明:
ZGC号称最强的,但是还没有接受检验,他的停顿时间能低于10ms,G1上面我们推荐设置100~200MS的最大停顿时间,所以ZGC是非常强大的,但是近几年还是G1,另外也不一定过几年会出现更厉害的,转型也是选择G1,就像HTTP3.0一样,转型过去的话,至少得10年以后了,所以,ZGC懂得他的奥秘之处,理解、认识即可
使用 –XX:+UseZGC 启用
通过染色指针、读屏障技术,将STW控制在10ms以内!!!
ZGC改进了标记复制算法,
与ZG1类似,进行Region,但是抛弃了分代思想,Region可以动态的创建于销毁
他的Region分为了三种
小型页面、SmallRegion
中型页面 MediumRegion
大型页面 Large Region,容量大小不固定,2MB的倍数即可
特点:
不需要分代(不需要区分代,不需要复杂的回收算法)、
并发处理(几乎所有操作都并发)、
低停顿时间(10ms以内)
可伸缩性(处理不同规模的程序,不论大小)
工作流程
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。