对象已死吗
首先判定生死
引用计数法(判断被引用的数量,0就删除,不好解决循环应用),可达性分析算法(速度快,从gc根去一次次引用,坏处是要一次次遍历,引入安全点,三色标记法)
固定作为GC根的对象包括以下:
1. 虚拟机栈中引用的对象,如:各个线程被调用的方法堆栈中使用到的参数
2. 在方法区中类静态属性引用的对象
3. 方法区中常量引用的对象
4. 在本地方法栈中的JNI(即通常所说的native方法)
5. java虚拟机内部引用,如基本数据类型对应的class类对象
6. 所有被同步锁持有的对象
引用
分为四种:
强引用:如Student a = new Student() 就是强引用,内存溢出也不回收
软引用:有用但非必须,系统即将发生溢出之前就会回收
弱引用:每一次垃圾收集都会回收
虚引用:被回收发个通知
回收方法区
方法区也需要回收,虽然效率不高,但是必须回收
垃圾收集算法
分代收集理论
不同的条件下,应使用不同的策略来执行
强分代假说:熬过越多垃圾收集过程的对象月难以消亡
弱分代假说:大部分对象都是照生新灭
把分代收集理论具体放到现在的java虚拟机中,就会被分为新生代和老年代
如果只做新生代收集,而新生代被老年代引用,那么为了找出他,就不得不遍历所有老年代确保可达性正确
而这样会消耗大量内存,所以引出了:
跨代引用假说
跨代引用相较于同代引用极少,所以只需要在新生代上建立一个全局的数据结构 -- 记忆集,这个结构吧老年代划分成若干小块,标志出老年代拿一块内存存在跨代引用,此时发生minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到gc roots进行扫描
Minor GC/Young GC:新生代收集
Major GC/Old GC:老年代收集,目前只有CMS收集器才会有单独收集老年代的行为
Mixed GC:混合收集,指新生代以及部分老年代的垃圾收集,目前只有G1收集器才会有这种行为
上述三种都是部分收集:Partial GC
Full GC:整堆收集,收集整个java堆和方法区的垃圾收集
标记清除算法
标记所有被回收的对象,然后回收
最基础的算法,其他算法大多以它为基础
缺点:执行效率不稳定,内存空间碎片化
标记复制算法
把整个内存分为两块,在一半内进行分配,当这一半满了,就把不回收的对象复制到另一半去,然后把这一半清空掉,循环往复
复制算法实现简单,运行高效,内存规整
但是缺点也很明显,就是内存缩小为原来的一半
后来IBM公司做了研究,发现百分之九十八的对象熬不过第一轮,所以不需要按照1:1的比例来划分新生代内存空间,
所以他把新生代分为三歌曲,一个Eden和两个Survivor,内存比例为8:1:1,Eden区满了就复制到S0区,清空Eden
Eden区第二次满了之后,就把Eden区和S0区复制到s1区,然后清空,最后把S1改成S0,进行下一轮
如果某一次Eden和S0活得比较多,S1放不下,那么就会触发空间分配担保,直接把存活的对象放入老年代
标记整理算法
标记整理与标记复制比较类似,把标记的对象往一边移动移动完成之后把存活对象边界以外的对象清空
移动对象是一个比较重的负担,要全程暂停用户线程
HotSpot的算法细节实现
枚举根节点
我们不知道gc根节点都在什么位置,所以我们需要去枚举他们,所有的收集器都需要暂停用户线程
HotSpot是使用一组名为OopMap的数据结构来达到这个目的的
OopMap的产生就引入了 安全点
安全点
在特定的位置把信息记录到OopMap,这些位置被称为安全点
安全点位置的选取基本上是以"是否具有让程序长时间执行的特征"为标准进行选定的
常见的安全点选取,如:方法调用,循环条状,异常跳转
安全区域
安全区域是指能够确保在一段代码之内,引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的,我们可以把安全区看作被扩展拉伸了的安全点
记忆集和卡表
记忆集是:非手机区域指向收集区域的指针集合的抽象数据结构
最简单的实现可以用非收集区域中所有含跨代引用的对象数组
记录精度:
字长精度:每一个记录精度到一个机器字长(就是处理器的寻址位数,如常见32或64,这个京都决定了机器访问物理内存地址的指针长度),该字包含跨代指针
对象精度:每个记录精确到一个对象,该对象里有字段含有跨代指针
卡精度:每个记录精确到一块内存区域,该区域内有对象含有跨代指针
并发的可达性分析
堆越大,存储的对象越多,对象图结构越复杂,要标记更多对象而产生的停顿时间自然就更长,而在这方面进行优化,提升也是巨大的
三色标记
白色:没有访问过对的对象
黑色:这个对象已经被垃圾收集器访问过,所有引用扫描过。安全存活
灰色:被访问过,但是有引用没被扫描过
并发造成的错误,一种是原本消亡的对象错误标记为存货,另一种是原本应存活的标记成消亡,第二种很致命,程序会因此报错
第二种发生的两个条件:
1. 赋值器插入了一条或多条从黑色对象到白色对象的新引用
2. 赋值器删除了全部从灰色对象到白色对象的直接或间接引用
要解决误删,只需要破坏其中一个条件就可以,所以给出两个解决方法
1. 增量更新:当黑色对象插入新的白色对象引用关系时,把它记录下来,等并发扫描结束后,再将这些记录下来的黑色对象为根扫描一次,简单地讲就是黑色对象插入白色对象的饮用后,就变回灰色对象
2. 原始快照:当灰色对象要删除指定的白色对象时,九江这个新插入的引用记录下来,等并发扫描结束之后,再将这些及路过的引用关系的黑色对象为根,重新扫描一次。简单说就是无论引用关系删除与否,都会按照刚刚一开始扫描那一刻的对象图快照来进行搜索
CMS是基于增量更新做的,G1和Shenandoah基于原始快照
经典垃圾收集器
Serial
单线程收集器,最基础,久远的收集器,他进行收集的时候,其他线程不能工作
ParNew
Serial的多线程并行版本
parallel Scavenge
这个收集器负重比较大
Serial Old
Parallel Old
CMS
目的是:最短回收停顿时间,基于标记清除算法实现
分为四个步骤:
1. 初试标记:标记一下GC Root能直接关联到的对象,速度很快
2. 并发标记:从GC Root 的直接关联对象开始遍历整个对象图的过程
3. 重新标记:修正并发标记期间,因为程序运行导致对象引用变化
4. 并发清除:清除掉标记阶段判断已经死亡的对象
其中,初试标记和重新标记两个步骤还需要stop the world
垃圾收集器线程可以与用户线程一起工作
优点是:并发收集,低停顿
是HotSpot的对追求低停顿的第一次尝试,但是不完美,有三个缺点
1. 对处理器资源非常敏感
2. 无法处理"浮动垃圾",且要是CMS运行期间预留内存不足以满足程序分配新对象的需要,回造成并发失败,然后调用Serial Old进行老年代垃圾收集
3. 因为是基于标记清除实现的,所以会产生大量空间碎片
Garbage First(g1)
开创了收集器面向局部手机的设计思路 和 基于Region的内存分布形式
面向服务端应用的垃圾收集器
G1宣告取代Parallel Scavenge加Parallel Old组合
是能够支持指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过N毫秒这样的目标
G1也不坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每个Region都可以根据需要,扮演新生代的Eden,Survivor,或者老年代空间
回收时,每一个Region上都有它的回收价值,先挑回收价值高的进行回收
他也分为四个步骤
1. 初始标记
2. 并发标记
3. 最终标记:
4. 筛选回收
G1不仅仅面向低延迟,而且最大幅度提高垃圾收集效率,为了保证吞吐量,选择了完全暂停用户线程的实现方案
低延迟收集器
Shenandoah
ZGC
内存分配与回收策略
1. 对象优先在Eden分配
2. 大对象直接进入老年代 - 比如超长字符串,元素数量很大的数组 - 减少大量的复制操作
3. 长期存活的对象将进入老年代 - 经过一次Minor GC就加一岁。15岁就升到老年代
4. 动态对象年龄判定 - 如果Suvivor空间中相同年龄所有对象大小的中和大于Suvivor空间一半,则把年龄大于或等于该年龄的对象直接进入老年代,无需到MaxTenuringThreshold中的要求年龄(15)
5. 空间分配担保 -必须先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间