分代收集理论
- 弱分代假说:绝大多数对象都是朝生夕死
- 强分代假说:熬过越多次垃圾回收的对象越难以消亡
- 跨代引用假说:跨代引用相对于同代引用是极少数
根据1、2建立了,新生代,老年代模型,以及其适用的收集算法
根据3产生了Remember Set(卡表实现)
GC种类
Minor GC/Young GC:年轻代GC。会STW
年轻代内存分配失败时触发
Major GC/old GC:老年代GC。只有CMS有
Mixed GC:混合GC。只有G1有
Full GC:整堆回收(年轻代、老年代、方法区)
- 担保失败时触发
- 方法区满了的时候触发
- 老年代满了时候触发
- 晋升对象大于老年代剩余空间时
- System.gc()
垃圾回收算法
标记-清除
缺点,产生内存碎片,CMS采用此算法。
CMS提供了机制清理碎片,即每标记清除5次,进行一次标记整理
标记-整理
缺点慢
标记-复制
年轻代的SSE基于此方法,8:1:1,年轻代 : 老年代最好为1 : 2, 1 : 3.
由于8:1:1在复制的时候可能存在1的空间不足,所以引出担保机制。
- 担保机制
生存对象S区放不下时,放不下的部分直接进入老年代。
担保失败:
老年代的空间不足以放下担保进来的年轻代对象时触发。
- 老年代的最大可用的连续空间是否大于新生代所有对象总空间
- 判断是否允许担保失败(JDK 6 Update 24之前)
- 老年代最大连续可用空间是否大于历次晋升到老年代对象的平均大小
2(JDK 6 Update 24之后不需要)或者3不满足的时候,进行Full GC,否则进行Minor GC。
什么样的对象直接进入老年代
- 大对象
- 15次之后
- 动态年龄判定(此次回收时,如果某一个年龄的对象总和大于S区的一半,则大于等于该年龄的对象直接进入老年代)
- 担保对象
CMS回收流程
初始标记
根结点枚举阶段
根结点枚举时候,采用 OopMap 记录,GC Root中哪些是对象(GC主要是回收对象类型)
为了更新OopMap,引出了安全点和安全区域
安全点
更新OopMap的点,也是用户线程停下来开始垃圾回收的点。
选取原则是指令复用序列之前,例如:方法调用、循环跳转、异常跳转等。
那么,下一个问题来了,线程怎么停下来。
抢占式中断
系统先中断,如果不在安全点再恢复运行
主动式中断
系统设置一个标志位,由执行线程区轮训这个标志位
现在虚拟机都采用主动式中断。
什么时候检查标志位
安全点 & 内存申请之前(因为申请内存可能会内存不足)
标志位怎么实现
内存保护陷阱的方式:
虚拟机会把一个内存设置为不可读,其他线程去读这个内存位置会出现异常,再在事先设置的异常处理器中挂起线程实现等待。
由于有些线程处于休眠状态,再次引出安全区域的概念
安全区域
安全区域是能确保在某一段代码片段中,引用关系不会发生变化的地方
线程在进入安全区域是会告诉虚拟机,虚拟机在回收垃圾时就不再关注这些线程
线程在离开安全区域时,要交验虚拟机是否完成了根结点枚举。如果没有需要一直等待。
由于跨代假说理论,所以需要Remember Set作为跨代引用的GC Root,存在年轻代,用于记录老年代对年轻代的引用,从而避免扫描整个老年代。G1里面也用来记录Region指向其他Region和其他Region指向当前Region
卡表
用于实现Remember Set,一个卡页512字节,即如果,卡表标示的起始地址为0x0000的话,rs[0]为1则0x0000~0x01FF内存中存在跨代引用
为了维护卡表引出了写屏障(区别于volatile的写屏障)类似AOP,在新建对象时会插入写后屏障保证卡表。
G1之前都是写后屏障
G1除了写后屏障维护卡表,为了实现原始快照(STAB)算法,还需使用写前屏障跟踪并发时的指针变化
卡表的伪共享问题
现在都是加载缓存行,假设缓存行64字节,由于卡表元素占用1字节,如果虚拟机同时修改764 * 512=32KB内存内的内容时,会存在伪共享问题。可以设置-XX:+UseCondCardMark来告诉虚拟机,写入之前是否查一下(如果是1就不写了)
并发标记
用户线程和垃圾回收线程共同执行。三色模型运行的第一阶段
此阶段可分为三个部分
- 并发标记
- 预清理
重新标记并发阶段,新生代(新分配对象)和老年代变化的引用关系(采用modUnionTable记录) - 可中断的预清理
触发条件是新生代E区大于2M(默认值参数为CMSScheduleRemarkEdenSizeThreshold)
循环处理 - 2中的事情
- 处理from和to区中的对象,标记可达的老年代对象
退出条件是
a. 可以设置循环次数
b. 设置循环时间默认是5秒
c. 上一阶段E区10%使用率,这一阶段变成了50%,上涨明显
再次标记
在并发标记阶段,由于用户线程和垃圾回收线程同时运行。引用关系会发生变化,会导致存活对象被删除的情况,所以采用此阶段进行补救。根据对象消失问题理论,只有如下两个条件都成立时才会出现对象消失问题
- 赋值器插入了一条或多条从黑色对象到白色对象的引用。
- 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。
产生了两种再次标记算法解决如上问题:
增量更新
记录新的白的插入到黑的节点变化,再以这些黑色为根重新扫描(对大堆不友好,变动的黑色对象全扫一遍)
原始快照
记录删除了白色对象的灰色对象,再以此为根重新扫描
G1采用此算法,实现时将删除对象作为存活对象处理
G1 SATB 致力于第二个条件的打破,利用 pre-write barrier 记录引用关系的删除,采用最保守的做法,把变更前的引用对象记录下来,当作是存活对象,让其活过这一周期;另外之前也提及了,SATB 在初始标记的时候打了快照,NextTAMS 指针之后的的对象也在这一个周期里隐式存活,因此 G1 的 SATB 会产生更多的浮动垃圾。但是换来的好处就是,不需要像 CMS 那样 remark,再走一遍 root trace 这种相当耗时的流程
并发清除
并发清除
经典垃圾收集器
Serial(New Old)
单线程,桌面版采用。
新生代:标记复制
老年代:标记整理
ParNew(New)
就是Serial的并行版本,只有他可以和CMS配合
并发:用户线程和垃圾回收线程
并行:垃圾回收线程之间
Parallel Scavenge(New)Parallel Old(是Parallel Scavenge的老年代收集器)
关注吞吐量
CMS
默认线程数是(处理器核心数+3)/4
Concurrent Mode failure会冻结用户线程采用Serial Old来兜底
G1
多个Region(S,E,O,Humongous)取值为1-32MB
维护双向卡表
TAMS,用户回收时分配对象,保证TAMS指针之间的内容不会被回收
G1 vs CMS
G1
可以指定最大停顿时间
不会产生内存碎片
原始快照消耗小,最终标记时停顿短
CMS
内存占用小(G1有双向卡表,且每个Region都有维护,可能占堆的20%)
内存屏障消耗小