垃圾收集器与内存分配策略
对象已死吗
引用计数器
- 给对象添加一个引用计数器,每当有一个地方引用它时,计数器加1,引用时效,计数器减1.任何时刻计数器为0的对象就不能再被使用了
- 不能解决对象相互循环引用的问题.
可达性分析算法
- 在主流的商务程序语言中都是通过可达性分析来判定对象是否存活的
- 从GC Roots的对象作为起始点,当一个对象到GC Roots没有任何应用链相连时,则证明此对象不可用.
- 在JAVA中,可作为GCRoots的对象
虚拟机栈中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI(即一般说的Native方法)引用的对象
引用
- 强引用
例: Object obj = new Object();
只要强引用存在,垃圾回收器永远不会回收掉被引用的对象 - 软引用
描述一些还有用但并非必需的对象
系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行二次回收 - 弱引用
描述非必需对象,强度比软引用更弱.
当垃圾收集器工作时,无论当前内存是否足够,都会回收掉纸杯弱引用关联的对象. - 虚引用
最弱的一种引用.
目的是能在这个对象被收集器回收时收到一个系统通知
回收方法区
废弃常量和无用的类
垃圾收集算法
标记-清除算法
- 标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象
- 不足之处
效率问题 : 标记和清除两个过程效率都不高
空间问题:产生大量不连续的内存碎片
标记-复制算法
- 将可用内存按容量划分为大小相等的2块,每次只使用一块,当一块用完了,将还活着的对象复制到另一块上边,然后一次性清理到已经使用过的内存空间
收集新生代 Eden/From Survivor/To Survivor 8:1:1
标记-整理算法
- 让所有活着的对象都向一端移动,然后直接清理掉边界以外的内存
分代收集算法
- 新生代使用复制算法
- 老年代使用 标记-清理 或者 标记-整理
HotSpot的算法实现
枚举根节点
- 导致GC进行时必须停顿所有的Java执行线程(STW)
- 通过OopMap可以快速且准确的完成GC Roots枚举
安全点
- 并不是每个指令都生成OopMap,只有在特定的位置记录这些信息,这些位置称为安全点
- 程序执行时并非在所有地方都能停下来进行GC,只有到达安全点时才能暂停
- GC发生时让所有线程都跑到安全点在停下来
抢先式
几乎没有采用这种方式的虚拟机
GC发生时,让所有线程全部中断,没有到安全点的线程,就恢复线程,让它跑到安全点
主动式
设置一个标志,线程去主动轮询这个标志,发现标志则中断挂起
轮询标志的地方和安全点时重合的
安全区域
- 安全区域指在一段代码片段中,引用关系不会发生变化,在这个区域的任何地方开始GC都是安全的
记忆集和卡表
- 为了解决分代收集对象跨代引用所带来的的问题,垃圾收集器在新生代中建立了记忆集的数据结构
- 记忆集记录从非收集区域指向收集区域的指针集合的抽象数据结构
- 记录精度
字长精度 :每个记录精确到一个机器字长,该字包含跨代指针
对象精度:每个记录精确到一个对象该对象里有字段含有跨代指针
卡精度(最常用的一种记忆集实现形式) :每个记录精确到一块内存区域,该区域有对象含有跨代指针
卡精度 是用 卡表的方式去实现记忆集 - 卡表
- 缩减GC Roots扫描问题
写屏障
通过写屏障技术维护卡表状态
垃圾收集器
Serial收集器
单线程工作:它在进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束
虚拟机运行在客户端模式下的新生代收集器
标记-整理算法
ParNew收集器
Serial收集器的多线程并行版本
服务端模式下,JDK1.7之前新生代收集器的首选
标记-整理算法
Parallel Scavenge收集器
新生代收集器
基于标记-复制算法实现
吞吐量优先收集器
Serial Old 收集器
老年代收集器
标记-整理算法
Parallel Old 收集器
老年代收集器
标记-整理算法
CMS收集器
- 以获取最短回收停顿时间为目标的收集器
- 标记-清除算法
- 过程
初始标记
需要STW
标记GC Roots能直接关联的对象
并发标记
从GC Roots的直接关联对象开始遍历整个对象图的过程
重新标记
需要STW
修正并发标记期间因用户线程运作而导致标记产生变动的标记记录
并发清除
清除掉标记阶段判断已经死亡的对象
- 完整过程阐述
1.标记GC Roots能直接关联的对象
2.从GC Roots的直接关联对象开始遍历整个对象图的过程
3.修正并发标记期间因用户线程运作而导致标记产生变动的标记记录
4.清除掉标记阶段判断已经死亡的对象 - 缺点
无法处理浮动垃圾
默认回收线程是(核心线程数+3)/4 ,核心线程数少于4个,对用户程序影响很大
标记-清除算法 会产生大量空间碎片,会导致Full GC ,不得不在full gc 之前进行碎片整理 - 并发标记阶段保证收集线程与用户线程互不干扰是通过增量更新算法实现
Garbage First (G1收集器)
- JDK9之后成为服务端默认垃圾回收器
- 标记-整理 算法实现
- 并发标记阶段保证收集线程与用户线程互不干扰是通过原始快照算法(SATB)实现
- G1将java堆划分为多个大小相同的独立区域(Region),将Region作为单次回收的最小单元.避免了在整个java堆中进行全区域的垃圾收集
- G1至少要消耗java堆容量的10%~20%的额外内存来维持收集器工作
- 过程
初始标记
仅仅只是标记一下GC ROOTS 能直接关联到的对象,并修改TAMS的值. 暂停用户线程
并发标记
从GC ROOTS开始对堆中对象进行可达性分析.递归扫描整个堆里的对象图.找到要回收的对象
最终标记
用户线程短暂暂停,用于处理并发阶段结束后仍遗留下来的最后少量的SATB记录
筛选回收
对各个Region的回收价值和成本进行排序.因为设计存活对象的移动,用户线程暂停
低延迟垃圾收集器
- LOW-LATENCY GARBAGE COLLECTOR(实验阶段)
- LOW-PAUSE-TIME GARBAGE COLLECTOR(实验阶段)
- Shenandoah 收集器
非官方团队开发,只在openjdk中存在,oracleJDK没有
Shenandoah收集器的工作过程大致可以划分为以下九个阶段
初始标记
并发标记
最终标记
并发清理
并发回收
初始引用更新
并发引用更新
并发清理
- ZGC收集器
标志性设计:染色指针技术 用作并发整理
过程
并发标记
并发预备重分配
并发重分配
并发重映射
内存分配与回收策略
- 对象优先在Eden分配
大多数情况下,对象在新生代Eden区中分配,当Eden中没有足够的空间进行分配时,将发起Minor GC - 大对象直接进入老年代
- 长期存活的对象将进入老年代
对象通常在Eden区里诞生,如果经过第一次 Minor GC后仍然存活,并且能被Survivor容纳的话,该对象会被移动到Survivor空间中,并且将其对象 年龄设为1岁。对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程 度(默认为15),就会被晋升到老年代中 - 动态对象年龄判断
如果Sur空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代 - 空间分配担保