垃圾回收.GC
1.垃圾判断算法
1.1 引用计数算法
给对象添加一个引用计数器,当有一个地方引用它,计数器加1,当引用失效,计数器减1,任何时刻计数器为0的对象就是不可能再被使用的。
引用计数器无法解决对象循环引用的问题(A引用B,B引用A)
1.2 根搜索算法
通过一系列的成为GC Roots的点作为起始进行向下搜索,当一个对象到GCRoots没有任何引用链相连,则此对象时不可用的。
1.2.1 GC Roots
- 在vm栈中的引用
- 方法区的静态引用
- jni(netive方法)中的引用
1.2.2 方法区的回收
商业化的虚拟机一般都会实现方法区的回收
- 回收内容:
- 废弃常量
- 无用类
- 回收满足条件:
- 该类的实例都被回收
- 加载该类的类加载器被GC
- 该类的Class对象没有在任何地方被引用,如不能在任何地方通过反射访问到该类。
2.GC算法
2.1 标记-清除算法(MS)
分为标记和清除两个阶段,首先标记所有需要回收的对象,然后回收所有需要回收的对象。
缺点:
- 效率较低 标记和清除的效率都比较低。需要扫描所有对象,堆越大越慢。
- 空间问题:回收后出现不连续空间,后续使用无法找到连续空间易造成再次回收。
2.2 标记-整理算法(MC)
原理:标记过程一样,但后续步骤不是清除,而是令所有存活对象一端移动,然后直接清理掉这端边界以外的内存。
缺点:没有内存碎片,但移动(压缩)对象更加耗费时间,
2.3 复制算法(Copying)
原理*
- 将可用内存划分为两块,每次只使用一块,当半块内存用完了,仅将还存活的对象复制到另外一块上面,原有的一块直接清除。
- 每次回收都是回收一半内存,也不用考虑内存碎片问题。当需要堆中内存时,只需要移动堆指针,按顺序分配内存。
特点 - 这种算法是内存会变为原来一半,比较昂贵。
- 在对象存活率较高的时候,效率有所下降。
- hotspot虚拟机默认eden和survivor的比例为8:1,也就是每次有10%的空闲空间
2.4 分代算法(GN)
原理
- 当前都是采用分代收集,根据对象的不同的存活周期将内存划分为几块。
- 一般把堆内存划分为新生代和老年代,这样根据各个年代的不同特点采用不同的算法。
- 新生代每次cg都有大量对象死去,所以使用复制算法,只需复制少量存活的对象。
- 对于新生代,现代虚拟机使用这种算法:
过程:eden执行复制算法,eden区满后会gc,还存活的对象复制到s1 survivor,当eden又满时,eden和s1 survivor中存活的对象将被复制到s2 survivor。如此循环。
如果对象的复制次数达到16次(默认),该对象就会被送到老年代中。
但是也不是固定的阈值,如果达到某个年龄发现总大小已经大于survivor的50%,所以要动态调整阈值(否则会导致survivor空间不足),让这些存活的对象尽快晋升。
TargetSurvivorRatio=60 ,值为一个百分比,当对象占据survivor60时则调整阈值。
UseConcMarkSweepGC,老年代使用cms算法。
UseParNewGC,新生代使用UseParNewGC算法。 - 老年代:存放了经过一次或多次GC还存活的对象
一般采用Mark-Sweep或者Mark-Compact算法
有多重垃圾回收器可以选择,每种垃圾回收器可以看做一种GC算法的具体实现。根据具体需求选择合适的垃圾回收器。 - 永久代:
并不属于堆,但GC也会涉及这个地方。
存放了每个Class的结构信息,包括常量池,字段描述,方法描述。与垃圾回收器要收集的java对象关系不大。
tops:引用类型
Hostpot将引用分为四种:
- strong:strong为new 出来的
- soft:内存不够时一定会被GC,长期不被引用也会被GC。
- weak:一定会被GC,当被标记为dead时,会在ReferenceQueue中通知。
- phantom:本来就没引用,当从堆中释放是会通知。
GC的时机
- Scavenge GC:
触发时机:新对象生成,Eden满了。
理论上Eden区大多数对象会在Scavenge GC回收,复制算法效率高,Scavenge GC时间短。 - Full GC:
对整个jvm整理,包括Young,Old 和 Perm
触发时机:1 Old满了,2 Perm 3.主动system.gc()。
效率很低,尽量减少Full GC。
3.垃圾回收器的实现和选择
3.1 什么是垃圾回收器
- 分代模型:GC的愿景
- 垃圾回收器:GC的具体实现
- hotspot提供多种垃圾回收器,我们要根据具体应用的需要采用不同的回收器。
- 没有万能的垃圾回收器,每一种垃圾回收器都有自己使用的场景。
3.2 垃圾回收器的并行和并发
- 并行:指多个收集器的线程同时工作,但是用户线程处于等待状态。
- 并发:指收集器在工作的同时,可以允许用户线程工作。
- 并发并不代表解决的gc停顿的问题,该停顿的步骤还是要停顿,比如标记垃圾时。但清除垃圾时可以并发执行。
3.3 垃圾回收器
1.Serial收集器
- 单线程收集器,收集时会暂停所有的工作线程,使用复制收集算法,虚拟机运行在clent模式下默认新生代收集器。
- 在老年代采用标记整理算法。
- 可以添加jvm参数UseSerialGC配合PretenureSizeThreshold参数改变阈值,使得超过某大小的对象直接在老年代创建。
2.Serial old收集器
- 单线程,使用标记整理算法,老年代收集器。
3.parnew收集器
- Serial的多线程版本,除了多个收集线程外,其余行为包括算法,stw,对象分配规则。回收策略等都与Serial收集器一样。单cpu核心场景下,此收集器不会比Serial更好。
4.parallel scavenge收集器
- 是一个多线程收集器,复制算法。但他是以吞吐量最大(gc时间占总运行时间最小)为目标的收集器。
Parallel Scavenge(-XX:+UseParallelGC)框架下,默认是在要触发full GC前先执行一次young GC,并且两次GC之间能让应用程序稍微运行一小下,以期降低full GC的暂停时间(因为young GC会尽量清理了young gen的死对象,减少了full GC的工作量)。控制这个行为的VM参数是-XX:+ScavengeBeforeFullGC。这是HotSpot VM里的奇葩。
5.parallel Old
- 老年代版本吞吐量收集器,多线程,标记整理算法。
MaxTenuringThreshold=5 进入到老年代的最大存活年龄
+PrintTenuringDistribution
6.cms
- 标记清除算法 Concurrent Mark Sweep
- cms是一种以最短停顿时间为目标的收集器,使用cms并不能达到gc效率最高,但他能尽可能的降低gc时服务的停顿时间。
- 步骤:
- 初始标记:标记gcroots关联的引用,快速,暂停用户线程。
- 并发标记:gcroots关联的引用往下接着标记,不影响用户线程。
- 并发预先清理:
- 并发可能失败预清理:
- 重新标记:修正并发标记时变动的引用,较快速,暂停用户线程。
- 并发清除:清除垃圾,不影响用户线程。
- 并发重置:线程重置
-
缺点:
- 依赖cpu资源,处理时间短,但处理次数多。
- 无法处理浮动垃圾,有时虚拟机会使用备案,使用Serial Old收集器来重新进行老年代的垃圾回收。
- 空间碎片问题,
空间分配担保:
在minor gc之前,虚拟机会检查老年代最大可用连续空间是否大于新生代所有对象总空间。当条件成立,在minorgc之后大量对象如果还会存活,则需要老年代进行空间分配担保,把survivor无法容纳的对象直接进入老年代。如果老年代判断剩余空间不足(根据每一次回收晋升到老年代对象容量的平局值作为经验),则进行full gc。
4. 内存泄漏及原因
- 对象定义在错误的范围
- 异常处理不当
- 集合数据管理不当
5. 安全点理论
枚举根节点:知道所有的根节点
hotspot使用一组成为OopMap的数据结构来达到这个目的。安全点:
- hotspot并没有为每条指令都生成OopMap,而只是在特定位置记录了这些信息,这些位置称为安全点,即程序执行时并非在所有地方都能停下来gc,而是在安全点才能gc.
- 安全点的选定不能太少也不能太多,选定安全点的标准是:“是否具有让程序长时间执行的特征”,例如方法调用,循环跳转,异常跳转。
- 对于safepoint,另一个需要考虑的是如何在gc时,让所有的线程都跑跑到最近的安全点停下来。
抢占式中断:gc时先中断,若有线程没在安全点,则恢复线程跑到安全点上。(此方式已被淘汰)
主动式中断:如果要gc,虚拟机设置一个标志,各个线程轮询这个标志,发现标志为真时就中断挂起。轮询的地方和安全点是重合的,另外在加上创建对象需要分配内存的地方。
- 安全区域:
- 安全点也有解决不了的情况,当线程休眠,没有给线程分配cpu时间,线程无法响应jvm中断请求,jvm也不太可能等待休眠结束。此时需要安全区域解决问题。
- 线程执行到安全区域,首先标识自己进入了安全区域,jvm在gc时不用管在安全区域的线程。当线程要离开安全区域是,要检查jvm是否完成了根节点枚举或整个gc,如果完成则继续执行,否则等待gc完成。
6. G1垃圾回收器
G1垃圾回收器是一种兼顾吞吐量和响应时间的垃圾回收器
G1回收器的堆结构和传统的堆结构有很大不同,传统堆结构各区域是大块分开,G1回收器堆内存的物理结构是无序的,堆空间eden,survivor,old区域是交织在一起的,并且区域的分类是会变化的。
6.1 G1收集器的内存结构
- 将堆划分为大小相等的区域(regions),每个regions都有一个分代的角色。
- 对每个角色的数量并没有强制限定,也就是说对每种分代内存的总大小,可以动态变化。
- G1可以高效执行回收,优先执行那些大量对象可回收的区域。
- 使用个停顿可预测模型,根据用户设定的停顿时间,g1会选择哪些region要清除,一次清除多少个。
- g1从多个region中回收,放入一个regin中(复制算法)。
因为会根据用户设定的停顿时长gc,每次只收集少数的region,做到了间隔时间少,且使用copy算法,内存碎片少。
6.2 几个概念
- 分区:将整个堆分为大小相同的分区
- 原有的eden,survivor,old属于不同的块
- eden,survivor,old只是一种逻辑概念。
- 优先回收垃圾最多区域(region)
- 收集集合(cset):一组可被回收的分区的集合
- 存活的数据会被移动到另一个空闲分区
- cset中的分区可以来自eden,survivor,old。
- 已记忆集合(rset):rset记录了其他region中对象引用本region中对象的关系。
- gc时不必扫描整个堆,而是扫描rset即可。即为减少扫描
- 因为eden区gc时要全部扫描,所以只需记录老年代到新生代的引用即可。
rset的两个作用
RSet究竟是怎么辅助GC的呢?在做YGC的时候,只需要选定young generation region的RSet作为根集,这些RSet记录了old->young的跨代引用,避免了扫描整个old generation。 而mixed gc的时候,old generation中记录了old->old的RSet,young->old的引用由扫描全部young generation region得到,这样也不用扫描全部old generation region。所以RSet的引入大大减少了GC的工作量。
标记出RootRegion指向O区的region,标记这些region是为了降低并发标记的扫描范围,因为并发标记需要扫描GCROOT引用或间接的所有对象,而这些对象一定是在RootRegion出发指向的Region中的。MIXGC中Y区本来就要全扫,所以这里再按照O区过滤下,这样就缩小了扫描范围。该阶段的操作为遍历O区region查询Rset是否有来自RootRegion的,(RootRegion是初始标记得到的)。
- STAB:
- 在并发标记阶段对快照标记
- stab是g1 gc在并发标记阶段使用的增量式的标记算法。
- 并发标记时并发多线程的,但并发线程在同一时刻只扫描一个区。
拓展
card page:
通常将堆空间划分为一系列2次幂大小的卡页.
card table:
一card张表,将region划分为小区域在这张表上。
points-out:
指明我引用谁
points-into:
谁引用我
6.3 gc模式
- YongGC:选定所有年轻代的region,通过控制年轻代region,来控制gc的时间
- 触发时机:eden区满了时
- gc步骤:
- 根扫描:静态和栈引用
- 更新rs:处理dirty card队列更新rs
- 处理rs:检测从年轻代指向老年代的对象
- 对象拷贝:拷贝存活对象
- 处理引用队列:软弱虚引用处理
- MixedGC:选定所有年轻代regin,外加根据global concurrent marking 统计出的若干高收益的老年代region,在用户指定的stw时间内尽可能的选择收益高的region。
- 触发时机:由一些参数控制,另外也控制着那些老年代regin会被选入cset
- global concurrent marking:全局并发标记
执行过程类似于cms,但主要为MixedGC提供标记服务,并不是gc中的必须环节。
步骤:
- 初始标记:标记了从GCroot开始可以直接可达的对象。(在YongGC中进行,伴随着YongGC)
- 并发标记:GCroot对heap中的对象进行标记,标记线程与用户线程并发进行,收集region的存活对象信息
- 重新标记:标记在并发阶段变化的对象
- 清理:清空没有存活对象的region,将region加入到free list.
- 三色标记法:STAB中的算法
对象分为三种类型
- 黑色:根对象,或者该对象与他的子对象都被扫描过了(对象被标记了,且他的所有fieldd都被标记完了)
- 灰色:对象本身被扫描,但还没扫描完该对象中的子对象(field还没有被标记)
- 白色:未被扫描对象,扫描完成所有对象后最终为白色的为不可达对象,即垃圾对象。(对象没有被标记到)
satb步骤
- 开始标记时生成快照图
- 在并发标记阶段所有被改变的对象入队(若在此阶段改变引用,将原有引用指向的对象变为非白),主要是防止回收非垃圾对象。
在并发标记阶段可能产生漏标记,所以对于gray灰对象一处的引用标记为灰色,对于black引用新增加的引用标记为黑色。 - 可能存在浮动垃圾,将在下次护手
漏标记情况举例
漏标只会发生在白色对象中,需满足以下两个条件。
- 并发标记时,应用线程给一个黑色对象的引用赋值了白色对象
- 并发标记时,应用线程删除所有灰色对象到该白色对象的引用
扫描全部的老年代对象,是因为ygc的时候,老年代的对象全都被看做gcroots了,需要在年轻代找被老年代任何对象引用的对象,这些对象都不应被清理。所以必须扫描所有老年代,进行你说的链式查找的过程。而如果有了rset就知道有些old区region中根本没有对年轻代对象的引用,直接就可以跳过这个region了。
[参考]
深入理解JVM 虚拟机
垃圾收集器|g1收集器