随笔,笔记
垃圾回收机制
为什么要了解垃圾回收机制和内存分配?
当需要排查各种内存溢出、内存泄露问题时,当垃圾收集器成为系统达到更高并发量的瓶颈时,我们就必须对这些 自动化(垃圾回收)技术实施必要的监控和调节。
如何判断对象是死是活?
- 引用计数器算法:在对象中添加一个引用计数器,每当有一个地方引用它时,计数器加一;当引用失效时,计数器值减一;任何时刻计数器为 零就是不可能被使用的。
- 原理简单,判定高效
- 产生循环引用问题。A->B->C->A 这样 计数器永远不为 零。
- 可达性分析算法:通过称为
GC Roots
的根对象作为引用起始点集,从这些结点开始根据引用关系向下搜索,搜的过程所走过的路径称为 引用链,如果某个对象到 GC Roots 间没有任何引用链相连,,证明对象不可能再被使用。- 固定可以作为 GC Roots 的对象有哪些?
- 在虚拟机栈中引用的对象。
- 在方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 在本地方法栈中JNI 引用的对象。
- 虚拟机内部的引用。
- 被同步锁持有的对象
- 反映java 虚拟机内部情况的。
- 还可以临时加入其它对象。
- 固定可以作为 GC Roots 的对象有哪些?
引用
- 强引用:普遍存在的引用赋值,无论在什么情况下,只要强引用关系还存在,垃圾回收器就永远不会回收掉被引用的对象。
- 软引用:描述一些还有用,但非必须的对象。在系统将要发生内存溢出异常前,会被垃圾回收器回收。提供了 SoftReference 类来实现软引用。
- 弱引用:非必须引用,强度比 软引用弱一些。被软引用关联的对象只能生存到下一次垃圾收集发生为止。通过
WeakReference
类来实现弱引用。 - 虚引用:最弱的一种引用关系。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。通过
PhantomReference
类来实现。
生存还是死亡
一个对象死亡,至少要经历两次标记过程。
第一次标记:就是通过可达性分析,发现对象与引用链没有建立连接,进行第一次标记。
筛选:此对象是否有必要,执行finalize() 方法。没有覆盖 finalize 方法或已经执行过一次,被视为没必要。
第二次标记:在筛选阶段 还没有拯救自己,就被标记第二次,移除 即将回收
集合,基本上就被回收了。
方法区回收
回收性价比低,主要回收废弃的常量和不在使用的类型。不使用类型的判断条件:
- 该类所有的实例都已经被回收。
- 加载该类的类加载器已经被回收。
- 该类对应的java.lang.Class 对象没有在任何地方被引用,无法通过反射访问该类。
垃圾收集算法
分代收集理论
建立在两个分代假说之上:
- 弱分代假说:绝大数对象都是朝生夕灭。
- 强分代假说:熬过越多次数垃圾收集过程的对象就越难以消亡。
意味着要将 堆进行分区,一个根据其年龄分配不同的区域中去。分为老年代和新生代。
产生一个问题,跨代引用问题。解决办法就是提出第三条假说:
- 跨代引用假说:跨代引用相对于同代引用来说仅占极少数。
在新生代上建立一个全局的数据结构,这个结构把老年代划分成若干小块,标识出老年代的那一块内存存在跨代引用。只有包含跨代引用的小内存才会进行扫描。
- 标记-清除算法:
- 首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象。
- 缺点:
- 执行效率不稳定。
- 内存空间碎片化问题,会产生大量不连续的内存空间。
- 标记-复制算法:
- 将可用内存按容量的大小划分为大小相等的两块,每次只使用其中一块,当块用完时,就将还存活的对象复制到另一块中,然后把已使用的内存空间一次清理掉。
- 优点:
- 不用 考虑 内存碎片化问题。
- 简单,高效
- 缺点:
- 如果有大量存活对象,会产生大量内存复制开销。
- 内存缩小为一半,空间浪费太多。
- Appel式回收:将新生代按照
8:1
的比列划分为一个Eden空间和两个较小Survivor
空间。每次只使用一个Eden 和 一个 Survivor空间,发生垃圾回收时,将存活对象,复制到另一个未使用的 Survivor 空间,并直接清理掉已使用的Eden空间和Survivor 空间。- 当 Survivor 空间不足时,会依赖其他内存区域进行分配担保。
- 标记-整理算法
- 像 标记-清除算法一样进行标记,在回收时,让所有存活的对象向内存空间一端移动,然后直接清理掉边界以外的内存。
- 缺点:
- 移动需要暂停 应用程序,造成 Stop the Word。
- 和稀泥式:平常使用标记-清除算法,在内存碎片化程度影响到对象分配时,使用标记整理算法整理一次,获得完整内存空间。
根节点枚举
就是分析 可达性分析 遇到的一些问题。
- 在收集器在根节点枚举时,必须要暂停用户线程。造成 Stop the Word。
- 根节点枚举始终 必须在一个保障一致性的快照中才得以进行。否则无法保证准确性。
使用 OopMap 数据结构,来保证不必一个不漏地检查所有执行上下文和全局的引用位置。
OopMap 会记录对象上什么偏移量是什么类型的数据,那些位置是引用。
安全点
为了解决:引用关系变化,或者导致 OopMap 内容变化。
设置 安全点:保证用户程序不要在指令流的任意位置停顿下来,开始进行垃圾回收,而是强制要求到达安全点后才能够暂停。
安全点的选取标准是:是否具有让程序长时间执行的特征。一般会在方法调用,循环跳转,异常跳转等指令序列复用的地方,会产生 安全点。
如何让线程跑到最近的安全点,有两种方案:
- 抢断式中断:在发生垃圾回收时,系统会把所有用户线程都中断,若发现有的线程没有运行到安全点,就让他继续执行,然后重新中断,直到跑到安全点。(几乎没有虚拟机采用了)
- 主动式中断:当垃圾收集发生时,会设置一个标志位,每个线程在执行过程中,会不断轮询这个标志,若发现为 true,在运行到 安全点时,主动中断挂起。
安全区域
安全点 解决了,执行线程如何进入垃圾回收的状态,但是对于那些阻塞状态的线程就无法保证。所以引入安全区域
。
安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化,因此这个区域中任意地方开始垃圾收集都是安全的。
当线程 进入安全区域,会标识自己已经进入安全区域,这样垃圾回收时,就不必搭理他;当线程离开安全区域时,会检查虚拟机根节点枚举是否完成,如完成了,就继续执行,否则等待。
记忆集与卡表
记忆集是一个用于记录 从非收集区域指向收集区域的指针集合的抽象数据结构。
可选择的记录精度:
- 字长精度:每个记录精确到一个机器字长,该字包含跨代指针。
- 对象精度:每个记录精确到一个对象,对象中包含跨代指针。
- 卡精度:每个记录精确到一块内存区域,区域的对象中含有跨代指针。
卡精度 通过 卡表实现。卡表是记忆集的具体实现。
- 卡表的最简单形式是一个字节数组。
- 数组中的每个元素都相应的标识着内存区域中的一个内存块,这个内存块被称为 卡页。
- 一个卡页的内存中不止一个对象。
- 若卡页内有一个对象,存在跨代指针,那么卡表中对应元素位置就标为 1,表示变脏。
- 垃圾收集 只筛选变脏的数据
写屏障
为了解决 卡表的维护问题。如何变脏。
写屏障,可以看作在虚拟机层面 对 引用类型字段赋值 的这个动作的 AOP 切面,在引用对象赋值时会产生一个环形通知,供程序执行额外的动作。也就是说,赋值的前后都在写屏障的覆盖范畴内。
卡表在高并发场景下的 伪共享问题:现代中央处理器的缓存系统中是以缓存行为单位存储的,当多线程修改相互独立的变量时,如果这些变量恰好共享同一个缓存行,就会彼此影响,而导致性能降低。
解决方案:不采用无条件写屏障,先检查卡表标记,只用卡表元素未被标记过,才标记为变脏。
并发的可达性分析
利用 三色标记 讲解了 ,可达性分析算法,在不保证 一致性的情况下 ,会遇到的问题。
- 问题一:将原本消亡的对象标记为 存活。(可以容忍)
- 问题二:把存活的对象 标记为 消亡。(致命)
对象消亡问题 需要同时满足两个条件才会产生:
- 赋值器插入了一条或多条从 黑色对象 到 白色对象的新引用。
- 赋值器删除了全部从 灰色对象 到该白色对象的直接或间接引用。
解决对象消亡问题只要破坏其中一个条件就可以,用两种方案:
- 增量更新:破坏第一个条件。当添加新的黑色对象指向白色对象的引用时,就记录下引用关系,当扫描结束,对记录的引用,进行重新的扫描。
- 原始快照:破坏的第二条件。当要删除灰色对象指向白色对象的引用时,先不删除,而是记录下来,等扫描结束,再将这些记录下来的对象进行重新的扫描。
垃圾收集器
Serial 收集器
- 是 新生代 收集器,单线程工作收集器。
- 采用标记-复制算法。
- 在收集时,必须暂停所有工作线程,直到收集结束。
- 可以 配合 CMS。
- 简单高效。
- 收集器里的额外内存消耗 最小。
- 没有线程交互的开销。
- 对于运行在客户端模式下的虚拟机是一个很好的选择。
ParNew 收集器
- 新生代收集器,serial 收集器的多线程版本。
- 采用 标记-复制算法。
- 可以配合 CMS 使用。
- 默认开启的收集线程数与处理器核心数量相同。
并行与并发的区别?
- 并行,描述的是多条垃圾收集器线程之间的关系,同一时间有多条这样的线程在协同工作,通常默认用户线程处于等待状态。
- 并发,描述的是垃圾收集器线程与用户线程之间的关系,同一时间垃圾收集器线程与用户线程都在运行。
Parallel Scavenge 收集器
- 新生代收集器
- 基于 标记-复制算法。
- 目标是到达一个可控制的吞吐量。
- 吞吐量 是处理器用于运行用户代码的时间与处理器总消耗时间的比值。
- 可以设置停顿时间,和吞吐量。
CMS 收集器
CMS收集器是一种以获得最短回收停顿时间为目标的收集器。
- 基于标记-清除算法。老年代
- 并发收集,低停顿
运作过程分为 四步:
- 初始标记
- 仅仅只是标记一下 GC Roots 能直接关联的对象。速度快。会 Stop The World。
- 并发标记
- 从直接关联的对象开始遍历整个对象图,耗时长但不需要停顿。
- 重新标记
- 修正 在 并发标记阶段,因用户线程操作导致标记变动一些对象。会出现 停顿
- 并发清除
- 清理删除掉标记阶段判断已经死亡的对象。
三个明显缺点:
- 对处理器资源非常敏感。由于占用线程资源,导致应用程序变慢。
- 默认启动的回收线程数是:(处理器核心数量 + 3)/ 4。
- 在 核心数量 在超过 4 个时,回收时垃圾收集线程 只占 不超过 25% 的处理器运算资源
- 但是当 核心数量 少于 4 个时,回收时垃圾收集线程 占到一半的运算资源。
- 虚拟机提供 增量式并发收集器 进行解决
- 利用 抢占式 线程调度的思想,垃圾收集过程会更长
- 已被弃用
- 无法 处理浮动垃圾。
- 在 CMS 的并发标记和并发清理阶段,用户线程还在运行,程序还会产生新的垃圾对象,只能留到下次垃圾收集时清理,这部分 垃圾称为 浮动垃圾。
- 同时由于用户线程在运行,要预留出内存空间给用户线程使用。当预留的内存无法满足用户线程的需要,就会出现并发失败。会启动备用方案,使用 serial old 收集器对老年代进行垃圾收集,停顿时间长。
- 由于是 基于 标记-清除 算法。会产生大量空间碎片。无法满足需要时,会触发 Full GC
G1收集器
G1 开创了收集器面向局部收集的设计思路和基于 Region 的内存布局形式,主要面向服务端应用的垃圾收集器。
停顿时间模型,能够支持指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过N毫秒这样的目标。
G1可以面向堆内存任何部分来组成回收集进行回收,衡量标准不再是它属于哪个分代,而是那块内存存放的垃圾数量最多,回收收益最大,这就是 Mixed GC 模式。
关键是 基于 Region 的堆内存布局。
G1 将连续的Java堆划分为多个大小相等的独立区域,每个 Region 都可以根据需要扮演新生代的空间或老年代的空间。
Region 中还有一个 特殊的 Humongous 区域,专门用来存储大对象。G1认为只要大小超过 一个 Region 容量一半的对象就判定为 大对象。
G1 更具体的处理思路:让收集器去跟踪各个 Region 里面的垃圾堆积的价值大小,价值即回收所获得的空间大小以及回收所需要的时间的经验值,然后后台维护一个优先级列表,每次根据用户设定的收集停顿时间,优先处理收益最大的那些Region。
G1 收集器运行过程大致分为 4个步骤:
- 初始标记
- 仅仅标记能与 GC Roots 直接关联的对象,并修改TAMS 指针的值,让下一阶段与用户线程并发运行时,能正确在可用的 Region 中分配新对象。暂停
- 并发标记
- 开始进行可达性分析,遍历对象对象图。还要重新处理SATB 记录下的并发时有引用变动的情况。
- 最终标记
- 暂停,处理并发阶段结束后,扔遗留下来的最后少量的STAB 记录。
- 筛选回收
- 负责 更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所希望的停顿时间来定制回收计划。
- 将 Region 回收集 中的 存活对象 要复制到 空的 Region 中,需要 暂停。
在延迟可控的情况下获得尽可能高的吞吐量。
CMS 和 G1 的比较
- CMS 是 标记-清除 算法。G1 整体上是 标记-整理 算法,局部是 标记-复制算法。
- G1 的内存占用 比 CMS 更高。比如 卡表
目前在小内存上 CMS 的表现大概率仍然会优于 G1,平衡点在 6G至8G之间。