垃圾收集器与内存分配策略
1. 对象是否"存活"
1.1 引用计数算法
描述: 在对象中添加一个引用计数器 , 每当有一个地方引用它时, 计数器值就加一; 当引用失效时, 计数器值就减一; 任何时刻计数器为零的对象就是不可能在被使用的
优点: 原理简单, 判定效率高
缺点: 占用额外内存; 单纯的引用计数难以解决对象之间的循环引用问题
1.2 可达性分析算法
描述: 通过一系列称为"GC Roots
“的根对象作为起始节点集, 从这些加点开始, 根据引用关系向下搜索, 搜索过程所走过的路径称为"引用链”, 如果某个对象到"GC Roots
"间没有任何引用链相连, 或者用图论的话来说就是GC Roots
到这个对象不可达时, 则证明此对象时不可能在被使用的.
可作为GC Roots
的对象:
- 在虚拟机栈中引用的对象
- 在方法区中的类静态属性引用的对象
- 在方法区中常量引用的对象
- 在本地方法栈中
JNI
(Native
方法)引用的对象 Java
虚拟机内部的引用- 所有同步锁持有的对象
- 反应
Java
虚拟机内部情况的JMXBean
、JVMTI
中注册的回调、本地代码缓存
1.3 引用
描述: 传统的引用表述(JDK1.2
之前)为若reference
类型的数据中心存储的数值代表的是另外一块内存的起始地址, 就称该reference
数据是代表某块内存、某个对象的引用.
JDK1.2
之后分为强引用、软引用、弱引用、虚引用, 强度依次减弱
引用细节:
- 强引用: 引用复制; 无论任何情况下, 只要强引用关系还在, 垃圾收集器就永远不会回收袋被引用的对象
- 软引用: 描述一些还有用, 但非必须的对象. 只被软引用关联着的对象, 在系统将要发生内存溢出异常前, 会把这些对象猎金回收范围之中进行第二次回收, 若果这次回收还没有足够的内存, 才会抛出内存映出异常. 类似可清理缓存, 空间足够就留着, 不够就清理掉
- 弱引用: 用来表述非必须对象; 被弱引用关联的对象只能生存到下一次垃圾收集发声位置
- 虚引用: (幽灵引用或者幻影引用)一个对象是否有虚引用的存才, 完全不会对其生存时间构成影响, 也无法通过需应用类取得对象实例. 虚引用的唯一目的只是为了能在这个对象被收集器回收时搜到一个系统通知
1.4 判断是否存活
及时在可达性分析算法中判定为不可达的对象, 也并非非死不可, 这时候暂时处于缓刑阶段, 要真正宣告一个对象死亡, 至少要经历两次标记过程:
- 如果对象在进行可达性分析后发现没有与
GC Roots
相连接的引用链, 那么他将会被第一次标记, 随后进行一次筛选, 筛选的条件是此对象是否有必要执行finalize()
方法, 加入对象没有覆盖finalize()
方法, 或者finalize()
方法已经被虚拟机调用过, 那么虚拟机将这两种情况都视为"没有必要执行" - 被判定为却有必要执行
finalize()
方法的对象会被放置在名为F-Queue
的队列之中, 并在稍后由一条由虚拟机自动建立的、地调度优先级的Finalizer
线程去执行他们的finalize()
方法(指虚拟机会触发这个方法开始运行, 但并不承诺一定会等待它运行结束, 避免某对象finalize()
方法执行缓慢或死循环造成永久等带甚至整个内存回收子系统崩溃).finalize()
方法时对象逃脱死亡命运的最后一次机会, 稍后收集器将对F-Queue
中的对象进行第二次小规模的标记, 若果对象在finalize()
方法中重新与饮用量上的任何一个对象建立连接关系, 那它在第二次标记时将被移除"即将回收"的集合; 如果对象这是后没有逃脱, 那基本上它就真的要被回收了(对象的自救机会只有一次, 因为一个对象的finalize()
方法最多只会被系统自动调用一次, 表示第二次引用链断裂, 不会再次执行finalize()
方法, 对象必然被回收)
1.5 回收方法区
方法区的回收主要分为两部分: 废弃的常量和不再使用的类型
废弃的常量回收: 与回收Java
对象类似, 在没有任何字引用常量池中的某个对象时, 若垃圾收集器判断确有必要的话, 该对象就会被系统清理出常量池
不再使用的类型: 需同时满足三个条件:
- 该类的所有实例都已经被回收.
- 加载该类的类加载器已经被回收.
- 该类对应的
java.lang.Class
对象没有在任何地方被引用
满足此三个条件Java
虚拟机被允许将其回收, 但并非必然会回收
2. 垃圾收集算法
2.1 分代收集理论
分代假说基础:
- 弱分代假说: 绝大多数对象都是朝生夕灭的
- 强分代假说: 熬过越多次垃圾收集过程的对象就越难以消亡
- 跨代引用相对于同代引用来说仅占极少数(存在互相引用关系的对象倾向于同时生存同时消亡)
设计原则: 收集器应该将Java
堆划分出不同的区域, 然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储
新生代: 标记存活对象 -> 高效率
老年代: 标记死亡对象 -> 低频率
将Java
堆划分为新生代和老年代, 新生代中每次垃圾收集都有大量对象死去, 二每次回收后存活的对象都逐步晋升到老年代
跨带引用: 要进行Minor GC
时, 需要关注被老年代引用的对象, 如此便需要扫描整个老年代
解决跨带引用: 在新生代建立一个全局的数据结构(记忆集), 在这个结构中把老面带划分成若干小块, 标志出老年代的哪一块存在跨代引用, 但发生Minor GC
时, 只有包含了跨带引用的小块内存里的对象才会被加入到GC Root
进行扫描
2.2 标记-清除算法
步骤:
- 标记对象: 回收的对象或者存活的对象中的一种
- 清除对象: 回收标记为回收的对象
缺点:
- 执行效率不稳定, 随对象的数量增长而降低
- 内存空间碎片化
2.3 标记-复制算法
将内存分为大小相等的两个半区, 每次只使用一个半区
步骤:
- 标记对象: 存活对象
- 复制对象: 将存活对象复制到另一个半区
- 清除对象: 将原半区的所有对象清除
优点:
- 效率较高
- 不考虑空间碎片化, 只要指针移动, 按顺序分配即可
缺点:
- 空间浪费
- 对象存活率高时就要复制大量的对象
用于新生代
Appel
式回收: 吧新生代分为较大的Eden
和两块较小的Surivivor
空间, 每次分配只使用一块Survivor
, 垃圾收集时, 将Eden
和Survivor
中的存活对象复制到另一块Survivor
中, 直接清理掉Eden
和原Survivor
中的对象, 两块Survivor
的角色调换, 默认E:S = 8: 1
2.3 标记-整理算法
步骤:
- 标记对象: 死亡对象或者存活对象
- 整理对象: 将存活对象向内存空间一端移动
- 清理对象: 清理掉边界以外的内存
优点:
- 不需要复制大量对象
- 不存在空间碎片
缺点:
- 需要移动大量对象并更新引用
- 引用更新需要全程暂停
解决方案:
多数时间采用标记-清除算法, 当内存碎片化程度影响到对象分配时再采用标记-整理算法收集一次
3. HotSpot
的算法细节实现
3.1 根节点枚举
固定可作为GC Root
的节点主要在全局性的引用与执行上下文中
所有收集器在根节点枚举这一步骤时都是必须暂停用户线程的(根节点枚举必须在一个能保证一致性的快照中进行)
HotSpot
使用一组称为OopMap
的数据结构来直接得到哪些的方存放着对象引用. 一旦类加载动作完成的时候, HotSpot
就会把对象内什么偏移量上是什么类型的数据计算出来, 在即时编译过程中, 也会在特定的位置记录下栈里和寄存器里哪些位置是引用. 这样收集器在扫描时就可以直接得到这些信息, 并不需要真正一个不漏的从方法区等GC Roots
开始查找
3.2 安全点
HotSpot
没有为每条指令都生成OopMap
, 只是在"特定的位置"记录了这些信息, 这些位置被称为安全点. 用户程序执行时并非在代码指令流的任意位置都能够停顿下来开始垃圾收集, 而是强制要求必须执行到安全点后才能够暂停.
安全点的选择:
- 不能太少, 让收集器等待时间过长
- 不能太多, 以至于过分增大运行时的内存负荷
- 标准: 是否具有让程序长时间执行的特征 -> 指令序列的复用 -> 方法调用、循环跳转、异常跳转
安全点需要解决的问题: 如何在垃圾收集发生时让所有线程都跑到最近的安全点, 然后停顿下来
方案:
- 抢先式中断:不需要线程的执行代码主动配合, 在垃圾收集发生时, 系统首先把所有用户线程全部中断, 如果发现有用户线程终端的地方不再安全点上, 就恢复这条线程执行, 让它一会再中断, 直到跑到安全点上 -> 系统中断线程, 等待所有线程都执行到安全点上, 不在安全点上的线程继续执行到安全点
- 主动式中断:当垃圾收集需要中断线程的时候, 不直接对线程操作, 仅仅简单地设置一个标志位, 各个线程执行过程时会不停的主动去轮训这个标志, 一旦发现中断标志为真时就自己在最近的安全点主动中断挂起
3.3 安全区域
解决程序不执行问题: 例如用户线程处于Sleep
或者Blocked
状态, 无法响应虚拟机的中断请求, 不能走到安全点中断挂起
安全区域: 指能够确保在某一段代码片段之中, 引用关系不会发生变化, 因此, 在这个去榆中任意地方开始垃圾收集都是安全的
原理: 当用户线程执行到安全区域里时, 首先会标记自己进入安全区域, 这样当这段时间里虚拟机要发起垃圾收集时就不必区管这些在安全区中的线程. 当线程要离开安全区域时, 它要检查虚拟机是否完成了根节点枚举. 完成了, 则线程继续; 没完成则等待完成.
3.4 记忆集与卡表
记忆集(解决跨代引用): 用于记录从非收集区域指向收集区域中的指针集合的抽象数据结构.
记忆集的记录精度:
- 字长精度: 精确到机器字长
- 对象精度: 精确到对象
- 卡精度: 精确到一块内存区域
卡精度的记忆集称为卡表: 一个卡页的内存中通常包含不止一个对象, 只要卡页内有一个或更多对象的字段存在着跨带指针,那就将对应卡表的数组元素的值标识为1, 称为这个元素biang脏, 没有则标识为0
3.5 写屏障
解决卡表的维护问题
卡表何时变脏: 有其他分带区域中对象引用了本区域对象时, 其对应的卡表元素就应该变脏, 时间点原则上应该发生在引用类型字段赋值的那一刻
卡表如何维护: 可以看做在虚拟机层面对"引用类型字段赋值"操作的AOP
切面, 在赋值操作发生时, 在写屏障中做更新卡表操作