(三)垃圾收集器和内存分配策略
判断对象存活算法
1、引用计数法
给对象添加一个引用计数器,当一个地方引用该对象时,计数器加1,当引用失效时,计数器减1;当计数器为0时,该对象就会被回收;
缺陷:当两个对象互相引用时,导致其计数器不为0,实际上对象已经不再被使用,从而无法被GC回收;
ObjA.instance=ObjB;
ObjB.instace=objA;
虽然此时,这两个对象已经无任何引用,实际上着两个对象,已经无法被访问了,但他们query彼此持有对方引用,导致计数器部位0;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sAITK7aZ-1594971196059)(http://ohjvgq2wj.bkt.clouddn.com/%E5%AF%B9%E8%B1%A1%E4%BA%92%E7%9B%B8%E6%8C%81%E6%9C%89%E5%BC%95%E7%94%A8.png)]
因此,主流虚拟机都不是使用该算法管理内存的;都是使用可达性分析算法
2、可达性分析算法
通过一系列成为"GC root"的对象作为起始点,从这些节点往下搜索(搜索走过的路径成为引用链),当一个对象到GC root没有引用链相连时(GC root到这个对象不可达),这个对象就是垃圾对象;
GC Root对象包括:
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(native方法)引用的对象
- 虚拟机栈(栈帧本地变量表)引用的对象
在查找GC ROOT对象时,必须确保整个分析过程停止在这一“时间点上”,如果对象引用关系还在变化,分析准确性就无法得到保障;这也是导致GC时必须暂停所有java执行线程(Stop the world)的重要原因;
垃圾收集器算法
1.标记-清除法
思路:首先标记出所有需要回收的对象,然后当标记完成后统一回收所有标记的回收对象
该算法不足之处:
- 标记和清除两个过程效率都不高
- 空间问题:标记清除后,会产生大量不连续的空间碎片,空间碎片太多导致程序运行过程中分配较大的对象时,无法找到足够的内存,而不得不提前触发另一次垃圾收集动作;
以下算法都是在标记清除算法基础上,弥补其不足之处:
2.复制算法
解决掉了:标记-清除算法的内存碎片化问题;
每次对整个半区内存进行回收
思路:将内存容量分为大小相等的两块,每次只使用一块,这块内存容量用完时,将还存活的对象全部复制到另外一块内存中,然后将该块使用过的内存空间一次性整体清除掉;
商业虚拟机都采用复制算法来回收新生代(新生代对象生命周期短),但是不是按1:1划分内存的,而是划分为一块Eden空间和两块survior空间(hotspot虚拟机默认大小比例是8:1:1),每次使用eden区和一块Survivor空间,当回收时,将eden和survivor空间存活的对象,复制到另外一块survivor空间,最后清理掉eden和刚才使用的那块survivor空间;
这种方式只有10%的内存被”浪费“,一般情况下98%的对象都是可回收的,但是无法保证每次每次都有不超过10%的对象存活。一旦Eden+survivor空间超过10%的对象存活,将导致另外一块survivor空间不够用,此时就需要其他内存(老年代)进行内存担保,这些存活对象将通过内存分配机制进入老年代存储;
标记-整理算法
复制算法不足:当对象存活率较高时,就面临这大量复制操作,效率降低。
思路:首先标记出所有需要回收的对象,然后将存活对象移向一端,最后直接清理掉端边界以外的内存
分代收集算法
根据对象存活周期将内存分为不同块,一般把java堆分为新生代(对象生命周期短)和老年代。新生代,每次垃圾收集时都会有大量对象死去,只有少量对象存活,所以复制算法比较合适;而老年代对象存活率高,没有额外空间对他进行分配担保,就必须使用标记-清理或者标记-整理算法来回收;
垃圾收集器
前面的垃圾收集算法是理论,这里的垃圾收集器就是具体实现,根据内存年代划分,选择合适的新生代、老年代组合垃圾收集器;
Serial收集器
新生代收集器
单线程收集器,只会使用一个CPU或者一条线程去完成垃圾收集,当进行垃圾收集时,必须暂停所有的工作线程,直到收集结束;
收集算法:复制算法
ParNew收集器
新生代收集器
Serial收集器的升级版,采用多线程收集,其他和Serial收集器一样;
收集算法:复制算法
并行与并发在垃圾收集器的上下文语句中含义
这里的并行收集器、并发收集器:
并行收集器:指多条垃圾收集线程并行工作,但此时用户线程任然处于等待线程;
并发收集器:指用户线程和垃圾收集线程同时执行,用户程序在继续运行,而垃圾收集程序运行在另一个CPU;
Parallel scavenge收集器
新生代收集器,采用复制算法,和ParNew一样是并行收集;
不同点:CMS等收集关注的垃圾收集时降低用户线程停顿时间;而PS收集器关注的是达到可控制的吞吐量
吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
老年收集器:
CMS收集器
尽量降低垃圾收集时用户线程停顿时间
算法:标记-清除
步骤:
- 初始标记:标记GC ROOt能直接关联到的对象;此过程需要暂停用户线程执行(时间较短)
- 并发标记:GC Root Tracing过程(和用户线程同时执行)(时间较长)
- 重新标记:标记并发标记过程中程序继续运行而导致标记变动那一部分的标记记录(此过程用户线程也是需要暂停的)(时间较短)
- 并发清除(时间较长)
G1收集器
一款面向服务器的垃圾收集器;
特点:
算法:标记-整理
- 分代收集:分代概念依然在G1中保留,不需要和其他收集器配合,独立就能管理堆内存
- 可预测的停顿:降低停顿时G1和CMS共同点,G1还可以让使用者明确指定在一个长度为M毫秒的时间片段,消耗在垃圾收集的时间不超过N毫秒;
使用G1收集器,java堆划分为多个大小相等的独立区域,虽然保留新生代和老年代的概念,但它们彼此不再是隔离的;
G1跟踪各个区域里面的垃圾堆积的价值大小(垃圾回收所获取的空间以及垃圾回收所需时间的经验值),在后台维护一个优先列表,每次根据回收时间,优先回收价值最大的区域;
引申的问题:区域不可能是孤立的,一个区域里面的对象不仅可以被本区域的对象所引用,也可以被整个java堆得对象所引用
那不是就意味着判断对象是否存活时,需要扫描整个java堆?
首先这个问题不只是时G1才有,其他收集器也有这问题(新生代引用老年代中的对象,老年代引用新生代的对象).
引申的问题:区域不可能是孤立的,一个区域里面的对象不仅可以被本区域的对象所引用,也可以被整个java堆得对象所引用
那不是就意味着判断对象是否存活时,需要扫描整个java堆?
首先这个问题不只是时G1才有,其他收集器也有这问题(新生代引用老年代中的对象,老年代引用新生代的对象).
***解决方案:***虚拟机使用remembered set来避免全扫描;G1每个区域都会有一个与之对应的Remembered set,虚拟机在发现程序在对引用类型的数据进行写的操作时,会暂停写的操作,检查引用的对象是否处在不同的区域,如果是,则把引用信息记录到引用对象所属的区域的remembered set中。当垃圾回收时,在GC根节点的枚举范围内remembered set即可保证不用全扫描也不会有遗漏;