GC
一,哪些内存需要回收
判断对象是否存活有两个方法:
- 引用计数法
- 可达性分析法
引用计数法
流程:
在对象中添加一个引用计数器,每当有一个地方引用他,计数器值就+1,每当引用失效,计数器值就-1,任何时刻计数器为0的对象就是不可能在被使用的。
优缺点:
- 优点:即使计数器占用一定内存,但是判定效率高
- 缺点:无法解决循环依赖的引用计数判断,导致依赖对象永远无法回收
可达性分析法
基本思路:
通过一系列称为GC ROOT的根对象作为起始点集,从这些点开始,根据引用关系向下搜索,搜索过的路径称为引用链,如果一个对象和GC ROOT没有任何引用链相连则表示该对象不可达,可以被GC回收
即使在可达性分析算法中判定为不可达的对象,也不是“非死不可”的,这时候它们暂时还处于“缓 刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程
第一次标记:
- 对象在发现没有跟GC ROOT相连的引用链后
第二次标记:
- 检查对象是否执行过finalize或者有没有重写过finalize
没有执行过finalize或者没有重写过finalize的对象会被放置在F-QUEUE中,被优先级较低的finalize线程去触发队列中对象的finalize方法,如果对象在finalize方法中重新和引用链关联,则免去被GC
GC ROOT对象有哪些?
- 虚拟机栈引用的对象
- 本地方法栈引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
四种引用:
- 强引用:无论内存是否足够,都不会回收
- 软引用,继承自SoftReference,内存足够时不回收,不足时回收
- 弱引用,继承自WeakReference,只要GC就会被回收
- 虚引用,继承自PhantomReference,与虚引用关联的目的就是为了让这个对象在垃圾回收时收到一个系统通知
二,如何回收
分代收集
分代假说:
- 对大多数对象都是朝生夕灭的
- 熬过越多次垃圾收集的对象越难收集
为什么要分为新生代和老年代?
- 新生代的对象朝生夕灭,将新生代的对象集中在一起,可以不用去标记大量,只关注如何保存少数存活的对象即可
- 老年代的对象存活率较高,可以以较低的速率来回收这个区域
GC分类
- minor GC/young GC:新生代GC
- major GC/old GC:老年代GC
- full GC:整堆和方法区GC
- mixed GC:整个新生代和部分老年代GC
垃圾收集算法
标记-清除算法
- 首先标记出所有需要清除的对象
- 标记完成后统一清除
缺点:
- 因为是标记需要清除的对象,Java堆中,像新生代的对象是朝生夕灭的,所以可能有大量对象需要标记清除,这就导致效率是根据清除对象的数量而下降的
- 标记清除算法容易导致内存碎片问题,碎片太多导致没有一片连续空间可以分配给新对象的话可能导致一次GC
标记-复制算法
- 将可用内存按照容量划分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活着的对象复制到另一块上面去,然后再把已使用过的内存空间一次清理掉
优缺点:
- 如果内存中的对象是多数存活的,那么就会产生大量的内存间复制的开销
- 如果是少数需要复制的话,复制的开销不大而且解决了内存碎片问题,所以综合以上两点,适合新生代垃圾收集
- 缺点就是,可用内存缩小为原来的一半
HotSpot虚拟机中,新生代又分为eden,survivor0,survivor1,比例是:8:1:1
- 每次使用eden和s1或者s2其中之一,举例子:使用eden+s1,将该组合区域内的存活对象复制到s2,直接清理掉s0+eden,下次使用s2+eden
标记-整理算法
- 先标记出需要回收的对象
- 将这些被标记的对象移动到端边界
- 清除端边界
垃圾收集器
总览:
Serial
特点:
- 单线程垃圾收集器
- 在进行垃圾收集时,使用单线程收集
- 垃圾收集时必须暂停所有用户线程(stop the world)
- 新生代垃圾收集器:新生代-复制算法,老年代-标记整理算法
ParNew
特点:
- 多线程垃圾收集器
- 使用多个GC线程进行垃圾回收
- 垃圾收集时也必须暂停所有用户线程(stop the world)
Parallel Scavenge
- 多线程垃圾收集器
- 新生代垃圾收集器,标记-复制算法
- 专注于提高吞吐量(运行用户代码的时间尽可能长)
Serial Old
- 老年代收集器,单线程收集器
Parallel Old
- 多线程收集器
- 吞吐量优先收集器
CMS
- 专注于降低stop the world时间
- 基于标记清除算法实现
- 老年代收集器
四步骤:
- 初始标记
- 仍需要stop the world
- 标记GC Root能直接关联到的对象
- 并发标记
- 从GC Root开始遍历整个对象引用图
- 用户线程和垃圾收集线程一起工作
- 重新标记
- 仍需要stop the world
- 修正并发标记期间,用户线程导致标记产生的变动
- 并发清除
- 清除不可达对象
- 垃圾回收线程和用户线程并发工作
- 标记-清除算法
G1
JDK9官方GC收集器,满足高吞吐量的同时满足GC停顿时间尽可能短
之前的堆分区:
在G1中的堆分区:
在G1中堆被分成一块块大小相等的region,这些region逻辑上是连续的,每块region都会被打上唯一的分代标志,虽然各个分代region是物理离散的,但是逻辑上还是构成了eden,survivor,old空间,每个region大小是1m-32m。大约2000个region
- 在G1中,如果一个对象大于0.5个region小于1个region的话,分配的region的话会直接被标记位old,大于1个region的话,会申请多个连续的region来存储该对象,都是标记为old
Rset & Cset
- Rset记录了其他region引用当前region的集合
- Cset记录的是本次GC需要清理的region集合
为什么叫做G1收集器?
- GC时G1的运行方式和CMS类似,也有一个全局的并发标记,标记完成后,G1知道哪些region可回收的对象多,优先回收这些对象,即优先回收垃圾多的,(Garbage first)
G1中的GC收集
- 当Eden空间被占满之后,就会触发YGC。在G1中YGC依然采用复制存活对象到survivor空间的方式,当对象的存活年龄满足晋升条件时,把对象提升到old generation regions(老年代)。G1中的YGC不需要扫描整个老年代,只需要扫描新生代+RSet
- MIX GC
- 初次标记:标记GCRoot直接引用的对象(GC Root所在的region—rootRegion)
- 扫描old区,找出当前old region rset中引用当前root Region的region
- 并发标记,只需要遍历第二步标记的region即可
- 重新标记,SATB,STW
- 复制清理,STW,选出垃圾较多的region,复制存活对象到空闲region,清理之前的region
总结
三,对象内存分配和回收
对象内存分配
- 概念上讲应该都是在堆上分配,但是可能会经过即时编译器编译后被拆散为标量类型间接在栈上分配
- 分代思想:
- 大部分新生对象直接分配在新生代
- 少数超过阈值的对象直接分配在老年代
对象优先在eden分配
- 大多数情况下,对象在新生代eden分配,当eden分区没有足够空间,JVM会发起一次minor GC
大对象直接进入老年代
- 大对象是指需要大量连续内存空间的对象,最典型的大对象就是很长的字符串和很大的数组
- 在Java虚拟机中要避免大对象的原因是,在分配空间时,它容易 导致内存明明还有不少空间时就提前触发垃圾收集,以获取足够的连续空间才能安置好它们,而当复 制对象时,大对象就意味着高额的内存复制开销
长期存活的对象将进入老年代
- 对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程 度(默认为15),就会被晋升到老年代中
- 如果在Survivor空间中相同年龄所有对象大小的总和大于 Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代
空间分配担保
- 在发生Minor GC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总 空间,如果这个条件成立,那这一次Minor GC可以确保是安全的。如果不成立,则虚拟机会先查看XX:HandlePromotionFailure参数的设置值是否允许担保失败(Handle Promotion Failure);如果允 许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大 于,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者-XX: HandlePromotionFailure设置不允许冒险,那这时就要改为进行一次Full GC。