垃圾收集
垃圾收集
程序计数器、虚拟机栈、本地方法栈是线程独立的
栈中的栈帧大小又是确定的,随方法的进入和退出执行出入栈操作
故这几个区域的内存分配和回收都是确定的,不需要过多考虑回收,当方法或线程结束后,内存就跟着回收了
因此垃圾收集的重点在于Java堆和方法区,如:一个接口的多个实现类需要的内存可能不一样,只有在运行期间才能确定内存的分配和回收
Java堆垃圾收集
Java堆存放对象实例,堆的回收就是对象的回收
方法区垃圾收集
《Java虚拟机规范》中不要求虚拟机在方法区实现垃圾收集,方法区的gc主要针对两部分:
- 废弃的常量:没有任何对象引用的常量
- 不再使用的类:需满足该类及其派生子类的所有实例已被回收、加载该类的ClassLoader已被回收、该类的Class没有被任何地方引用
分代收集
分代收集理论
Java堆分为新生代(Young Generaion)和老年代(Old Generaion),依据以下假说
- 弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭
- 强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡
- 跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极少数
对于第三点,存在互相引用的对象是倾向于同时生存或同时消亡的,如新生代被老年代所引用,因为老年代难以消亡,而被引用的新生代会逐渐晋升为老年代
处理跨代引用的方法是在新生代建立一个Remembered Set用于记录存在跨代引用的老年代,当对新生代gc时将里面的对象加入GC Roots
分代收集名词
部分收集(Partial GC):对Java堆的部分区域垃圾收集,其中又分为:
- 新生代收集(Minor GC / Young GC)
- 老年代收集(Major GC / Old GC):CMS收集器独有
- 混合收集(Mixed GC):G1收集器独有
整堆收集(Full GC):对Java堆和方法区垃圾收集
垃圾收集算法
可分为引用计数式垃圾收集和追踪式垃圾收集两大类,下面只讨论后者
标记-清除(Mark-Sweep)算法
标记出所有需要回收的对象,回收被标记的对象(或标志存活的对象,回收未被标记的对象),标记过程就是判断对象是否存活,分配时采用空闲列表,缺点:
- 执行效率不稳定,随着对象数量增长而降低
- 标记-清除后会产生大量不连续的内存碎片,可能导致较大对象无可分配的连续内存,从而不得不提前触发另一次gc
Hotspot的CMS收集器采用此算法,当碎片化程度影响到大对象的分配时,再采用标记-整理算法收集以获得连续的内存空间
标记-复制(Mark-Copy)算法
将可用内存分为相等两块,每次只使用其中的一块,当用完后将还存活的对象复制到另一块,然后整个回收,分配时采用指针碰撞,缺点:
- 若多数对象存活,则复制开销大(但新生代只有少数对象存活)
- 内存缩小为原来的一半
但实际上并不需要平分,如Appel式回收将新生代分为一块Eden和两块Survivor,比例为8:1:1,这样只有10%的空间被浪费
每次只使用Eden和其中一块Survivor,当gc时将Eden和Survivor中存活的对象复制到另一块Survivor
当Survivor空间不够时,需使用其他内存区域进行分配担保
Hotspot的Serial、ParNew新生代收集器采用此算法
标记-整理(Mark-Compact)算法
标记完后,让所有存活对象向内存空间一端移动,然后直接清理边界之外的内存
每次移动存活对象并更新所有引用,需暂停应用程序,其称为Stop The World
Hotspot的Parallel、Scavenge老年代收集器采用此算法
Hotspot算法细节
根节点枚举
所有收集器在根节点枚举(即确定GC Roots)时都会 Stop The World,但查找引用链的过程可以做到和用户线程并发
确定GC Roots并不需要遍历所有执行上下文和全局引用,准确式内存管理的HotSpot使用OopMap
- 在类加载时记录对象内什么偏移量上是什么类型的数据
- 在即时编译时记录栈里和寄存器里哪些位置是引用
安全点
并非在代码指令流的任意位置都能够停顿下来开始垃圾收集,而是要求必须执行到达安全点后才能够暂停
安全点既不能太少以至于gc间隔时间过长,也不能太多以至于多次gc增大运行时的内存负荷,一般在长时间执行的指令序列复用(如循环)才会产生安全点
需要在线程跑到安全点时对其中断:
- 抢先式中断:当gc时先中断所有线程,若有线程不在安全点,则恢复让它跑到安全点(已无虚拟机使用此方式)
- 主动式中断:设置一个标志位,线程会轮询该标志位,为真时在最近的安全点主动中断挂起
安全区域
安全点保证程序执行时的垃圾收集,但当线程Sleep或Blocked时,线程无法响应虚拟机的中断请求,不能跑到安全点中断挂起,虚拟机也不太可能持续等待该线程被重新唤醒
安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化,因此在这个区域中任意地方gc都是安全的
当线程执行到安全区域里面的代码会标识自己,gc时不用管已在安全区域的线程,当线程离开时要检查虚拟机是否结束Stop The World,若未结束则需等待
记忆集和卡表
记忆集(Remembered Set)是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构,其记录精度有:
- 字长精度:精确到一个机器字长,其包含跨代指针
- 对象精度:精确到一个对象,其字段含有跨代指针
- 卡精度(卡表):精确到一块内存区域,其内有对象含有跨代指针
HotSpot的卡表为字节数组,数组元素对应的内存块称为卡页(大小为2的n次幂),如下表示每个卡页占512字节
CARD_TABLE [this address >> 9] = 0;
若卡页内有对象存在跨代引用指针,就将值置为1,称为Dirty,gc时把Dirty的内存块加入GC Roots
写屏障(Write Barrier)
卡表需在非收集区域指向收集区域时(即在赋值时),将对应元素Dirty,利用写屏障,在引用对象赋值时会产生一个环形通知,供程序执行额外的动作,在赋值前叫作写前屏障,在赋值后叫作写后屏障
- 伪共享:缓存以行为单位存储,当多线程修改的变量在同一缓存行,就会彼此影响(写回、无效化或者同步)而导致性能降低
多个卡表可能共享一个缓存行并出现伪共享,故在写入前需检查卡表标记,而不是无条件的写屏障
if (CARD_TABLE [this address >> 9] != 0)
CARD_TABLE [this address >> 9] = 0;
JDK7后可通过-XX:+UseCondCardMark选择是否开启上面的条件判断
并发的可达性分析
可达性分析和用户线程已经可以并发运行,但可能导致
- 原本消亡的对象错误标记为存活,可以容忍,虽然逃过这次gc,但可在下次gc时回收
- 原本存活的对象错误标记为消亡,这会导致错误
黑色表示扫描过可存活的对象,白色表示未扫描或不可存活的对象,灰色表示扫描过但该对象还有未扫描引用,如下两个情况会导致对象消失
- 插入了黑色对象到白色对象的新引用
- 删除了全部从灰色对象到该白色对象的直接或间接引用
分别对应两种解决方案
- 增量更新:将新插入的引用记录下来,再将记录的黑色对象作为根重新扫描(相当于黑色插入白色变成灰色),CMS采用
- 原始快照:将要删除的引用记录下来,再将记录的灰色对象作为根重新扫描(即把删除的补回去,相当于未修改),G1采用