垃圾收集算法
分代收集理论
当前虚拟机的垃圾收集都采用分代收集
即将堆分为新生代和老年代,人后根据各个年代的特点选择合适的垃圾手机算法
标记复制算法
将内存分为大小相同的两份,每次使用其中一份,这一份内存使用完后,将GC过后还存活的对象复制到另一边,再清空内存
缺点:浪费内存,每一次都会有一半内存是空的无法使用
标记清除算法
标记存活对象后统一回收所有未被标记的对象(反之也行)
缺点
如果需要标记的对象太多,效率会不高
清除后会产生大量不连续的内存碎片
标记整理算法
标记存活对象,将所有存活对象像另一端移动,再清理末端边界以外的内存
解决了内存碎片问题
垃圾收集器
之所以会有那么多垃圾收集器是目前还没有真正能够适应所有场景的收集器,一直在完善收集器
Serial(串行收集器)
最基本,最古老的收集器,收集器的实现是单线程去完成的
只会有一条垃圾收集线程去执行垃圾回收,并且暂停其他工作线程知道回收结束
新生代(Serial)采用复制算法,老年代(Serial Old)采用标记整理算法
由于没有线程交互,在单线程机器上可以获得很高的单线程收集效率
用途
在JDK5及以前的版本中使用
作为CMD收集器的后备方案
参数配置
-XX:+UseSerialOldGC
老年代使用
Parallel(并行收集器)
属于Serial收集器的多线程版本,将单线程实现垃圾回收改为多线程并发回收
垃圾回收线程数跟CPU核数相同
-XX:parallelGCThreads
修改线程数(不建议)
新生代(Parallel)采用复制算法,老年代(Parallel Old)采用标记整理算法(JDK8默认)
参数配置
-XX:+UseParallelOldGC
老年代使用
ParNew
与Parallel收集器很类似,主要是可以配合CMD收集器一起使用
是许多运行在Server模式下的虚拟机的首选,除了Serial外可以和CMD一起使用的
CMS(Concurrent Mark Sweep)
以获取最短回收停顿时间为目标的收集器,注重用户体验
是HotSpot第一款真正意义上的并发收集器,实现了垃圾收集线程与其他线程(基本上)同时工作
属于一种标记清除算法实现
实现:
初始标记
暂停其他线程(STW),并记录GC ROOT直接能引用的对象,速度很快
并发标记
从GC ROOT直接引用对象开始遍历整个对象图,耗时较长,但是不STW,GC线程与应用线程并发执行
标记完的时候,已经标记过的对象可能状态已经发生改变
重新标记
为了修正并发标记期间,由于应用线程还在执行导致对象状态发生变动的这部分标记记录
标记也会暂停其他线程,且停顿时间会比初始标记稍长
主要用三色标记里面的增量更新算法做重新标记
并发清理
开启应用线程,同时GC线程开始清空未标记的区域
如果有新增对象会进行标记,不做任何处理(三色标记)
优缺点
缺点
对CPU资源敏感,会和服务抢资源
无法处理浮动垃圾
并发标记和并发清理阶段又产生的垃圾(只能等下一次GC)
使用的回收算法会有大量空间碎片产生
执行过程中的不确定性
上一次垃圾回收还没执行完,然后又触发Full GC
即有可能上一次GC还在并发标记或者并发清理阶段,应用线程来了大对象又造成GC
concurrent mode failure,会STw,然后用Serial Old进行垃圾回收
核心参数
-XX:+UseConcMarkSweepGC
使用CMS垃圾收集器
-XX:ConcGCThreads
并发的GC线程数
-XX:UseCmsCompactAtFullCollection
FullGC之后做压缩整理(减少碎片)
-XX:CMSFullGCBeforeCompaction
进行多少次GC后整理,默认0
-XX:CMSInitiatingOccupancyFraction
设置老年代触发GC的阈值,默认92%
-XX:+UseCMSInitiatingOccupancyOnly
只使用设定的GC阈值,否则JVM仅在第一次设定改值,后序会自动调整大小
-XX:+CMSScavengeBeforeRemark
在CMS GC启动前启动一次minorGC,减少老年代会年轻代的引用,降低标记时间
-XX:+CMSParallelInitialMarkEnabled
初始标记的时候使用多线程去执行
-XX:+CMSParallelRemarkEnabled
重新标记的时候使用多线程去执行
G1(Garbage First)
是一款面向服务器的垃圾收集器,主要对配置多核处理器及大容量内存的机器,以极高概率满足GC停顿时间要求的同时还具备高吞吐量
内存划分原理
概念
保留了年轻代和老年代的概念,但是不再进行物理隔阂
将java堆分为多个大小相等的独立区域(Region),JVM目标是不超过2048个Region,实际使用可以超过,但是不推荐
Region大小一般为堆内存除以2048,也可手动指定(不推荐)
分代存储
年轻代
占堆内存5%
在运行过程中会不停增加Region
最高占比不会超过60%(默认),也可手动调整
Eden与Survivor的占比还是8:1:1
大对象(Humongous)
专门存放短期巨型对象,不用直接进老年代
存储占用内存超过了一个Region大小50%的对象
好处:节约老年代的空间,避免因为老年代空间不够的GC开销
一个Region可能之前是年轻代,如果Region进行了垃圾回收,之后可能又会变成老年代
收集器分类
YoungGC
会先计算Eden区回收需要的时间是否接近用户设定值
远远小于则增加年轻代的Region,继续给新对象存放,再次计算
接近设定值,触发YoungGC
MixedGC(主要)
老年代堆占有率达到设定值触发
回收所有的Young和部分Old(根据期望的GC停顿时间确定old区垃圾收集的优先顺序)以及大对象区
需要把各个region中存活的对象拷贝到别的region里去,拷贝过程中如果发现没有足够的空region能够承载拷贝对象就会触发一次Full GC
FullGC
停止应用线程,采用单线程进行标记、清除和压缩整理出Region供下一次MixedGC使用
非常耗时
MixedGC 执行流程
初始标记(STW)
暂停所有其他线程,记录GC ROOT直接引用的对象(速度很快)
筛选回收(STW)
先计算各Region的回收价值和成本排序,根据用户所期待的GC停顿STW(G1基本围绕这个STW时间实现)时间来制定回收计划
筛选
在后台维护了一个优先列表,优先选择回收价值最大的Region
回收算法主要用的是复制算法,将一个Region中存活的对象复制到另一个Region中,几乎不会有太多碎片
STW执行的原因
STW期望时间是用户控制的,只能回收一部分Region
停顿其他线程可以大幅提高收集效率
注意:CMS回收阶段是跟用户线程一起并发执行的,G1因为内部实现太复杂暂时没实现并发回收,不过到了ZGC,Shenandoah就实现了并发收集,Shenandoah可以看成是G1的升级版本
适用场景
50%以上的堆被存活对象占用
对象分配和晋升的速度变化非常大
垃圾回收时间特别长,超过1秒
8GB以上的堆内存(建议值)
停顿时间是500ms以内
优化建议
调节 -XX:MaxGCPauseMills 这个参数的值,在保证他的年轻代gc别太频繁的同时,还得考虑每次gc过后的存活对象有多少,避免存活对象太多快速进入老年代,频繁触发mixed gc
核心参数
ZGC(The Z Garbage Collector)
三色标记算法(并发标记过程)
按照“是否访问过”为条件进行访问对象颜色标记
现代追踪式(可达性分析)的垃圾回收器几乎都借鉴了三色标记的算法思想,尽管实现的方式不尽相同:比如白色/黑色集合一般都不会出现(但是有其他体现颜色的地方)、灰色集合可以通过栈/队列/缓存日志等方式进行实现、遍历方式可以是广度/深度遍历等等
三色解释
黑色
表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过,且是安全存活的
如果有其他对象引用指向黑色对象,无需重新扫描
黑色对象不可以直接(不经过灰色)指向某个白色对象<br>
灰色
表示对象已经被垃圾回收器访问过,但对象上至少存在一个引用还没有被扫描过
白色
表示对象尚未被垃圾回收器扫描过
可达性分析刚刚开始阶段,所有对象都是白色的
如果扫描结束仍然还是白色,即代表不可达
问题
多标引起浮动垃圾
起因
在标记过程中,由于部分局部变量(GC ROOT)被销毁,而刚有这个引用又被扫描过且标记为非垃圾对象,这部分本来应该是要被回收的确又没有进行回收
并发标记(并发清理)开始后产生的新对象,通常做法是直接全部变成黑色,不进行清除,但是期间也可能会变成可回收对象
浮动垃圾不会影响垃圾回收正确性,只是需要下一轮GC才会回收
漏标
漏标会导致被引用的对象被当成垃圾对象回收,这属于严重BUG
读写屏障
写屏障
给某个对象的成员变量赋值前后,加一些处理
正常赋值
写屏障赋值
写屏障实现SATB,当对象A的成员变量的引用发生变化时,比如新增引用(a.d = d),我们可以利用写屏障,将A新的成员变量引用对象D记录下来:
读屏障
在获取对象属性前后,加一些处理
读屏障读取成员信息
读取成员变量是记录读取到的对象