垃圾收集(Garbage Collection,GC)主要做这几件事情:确定哪些内存需要回收、什么时候回收、如何回收。
判断对象是否死亡
引用计数算法
给对象添加一个计数器,每当有一个地方引用它时,计数器加1;当引用失效,计数器减1;计数器为0的对象就是不可能再被使用的。但是至少主流的Java虚拟机没有选择该算法来管理内存对象,其中最主要原因是它无法解决对象之间的相互循环引用问题。
可达性分析算法
通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链时,则证明此对象是不可用的。可以作为GC Roots的对象:(1)虚拟机栈(栈帧中的本地变量表)中引用的对象;(2)方法区中类静态属性引用的对象;(3)方法区中常量引用的对象;(4)本地方法栈中JNI(即一般说的Native方法)引用的对象。
垃圾收集算法
- 标记-清除算法分为两个阶段,标记和清除。首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。缺点:(1)标记和清除两个过程的效率都不高;(2)标记清除之后会产生大量不连续的内存碎片。
- 复制算法将内存按容量划分为大小相等的两块区域,每次只使用其中的一块。当一块内存用完了,就将其中还存活的对象复制到另一块区域上,然后再将已经使用过的内存区域一次性清理掉。
- 标记-整理算法 根据老年代的特点,有人提出了“标记-整理”算法。其中标记过程和“标记-清除”算法一样,但是后续步骤不是直接对可回收的对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
- 分代收集算法当前商业虚拟机的垃圾回收都是采用“分代收集”算法,根据对象的存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适合的收集算法。在新生代中,每次垃圾收集时都会发现有大量对象死去,只有少量对象存活,那就选择复制算法;而老年代中因为对象存活率较高,没有额外空间对它进行分配担保,就必须采用“标记-清除”或者“标记-整理”算法来进行回收。
HotSpot的算法实现
面临的问题
- GC Roots节点主要在全局性引用(常量或类静态属性)与执行上下文中(栈帧中的本地变量表),现在很多应用仅仅方法区就有数百兆,如果要逐个检查里面的引用,会消耗很多时间。
- 为了保证分析的准确性,在分析期间需要停顿所有Java线程,即Stop The World。
解决方案
在类加载完成的时候就把对象内什么偏移量上是什么类型的数据计算出来(使用OopMap),在JIT编译过程中也会在特定位置记录下栈和寄存器中哪些位置是引用。
垃圾收集器
Serial收集器
Serial收集器是最基本、发展历史最悠久的收集器。是一个单线程的收集器,因为在它进行垃圾收集时,必须暂停其它所有的工作线程(Stop-The-World)。
ParNew收集器
Serial收集器的多线程版本,使用多条线程进行垃圾收集。
Parallel Scavenge收集器
Parallel Scavenge收集器的目标是达到一个可控的吞吐量(Throughoutput)。 吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总共运行了100分钟,垃圾收集用了1分钟,那么吞吐量就是99%。
Serial Old收集器
Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法。
Parallel Old收集器
是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。
CMS(Concurrent Mark Sweep)收集器
以获取最短回收停顿时间为目的的收集器。基于“标记-清除”算法。 收集过程分为4个步骤:(1)初始标记;(2)并发标记;(3)重新标记;(4)并发清除。初始标记标记GC Roots能够直接关联到的对象,并发标记进行GC Roots Tracing,重新标记修正并发标记期间变动的那一部分对象,最后执行并发清除。耗时最长的并发标记和并发清除步骤都是与用户工作线程一起工作的,所以很快。优点:并发收集,低停顿。缺点: 对CPU资源敏感。无法处理浮动垃圾,伴随CMS程序运行时产生的新的垃圾,所以必须预留足够的内存空间给用户使用。因为基于“标记-清除”算法,所以会产生碎片,碎片过多会触发Full GC,Full GC会卡顿。
G1(Garbage-First)收集器
G1(Garbage-First)收集器不再只用于新生代、老年代,它把堆划分为多个大小相等的区域,然后跟踪各个区域中垃圾的价值(回收获得的空间大小以及回收所需时间),在后台维护一个优先列表把各个区域按照垃圾的价值排列。每次回收时优先回收价值最大的区域。
G1收集器工作分4步:初始标记—并发标记—最终标记—筛选回收
前三步与CMS的初始标记、并发标记、重新标记一样,筛选回收则先对各个区域进行垃圾价值的排序,然后根据用户期望的GC停顿时间来制定回收计划,最后进行回收。
G1的优点:
1)并行与并发:在多处理器器的环境下仍然可以并发标记、并发清理。
2)分代收集:新生代和老年代不再是物理上隔离的内存,而是不同的区域集合罢了。
3)空间整合:从整体看是基于 标记—整理算法,局部看是基于 赋值算法
4)可预测停顿:使用者可指定每次垃圾回收时间不超过M毫秒。
复制代码
内存分配与回收策略
- 对象优先在Eden分配,大多数情况下,对象都在新生代的Eden区中分配内存。而因为大部分的对象都是“朝生夕死”的,所以新生代又会频繁进行垃圾回收。
- 大对象直接进入老年代,需要大量连续空间的对象,如:长字符串、数组等,会直接在老年代分配内存。这是因为,这样可以避免在新生代区频繁的GC时发生大量的内存赋值(新生代的GC是采用复制算法的)。
- 长期存活的对象将进入老年代,新生代中经历了多次GC仍然存活的对象,当年龄达到一定程度(默认15)时就会晋升到老年代。为了更好地适应内存情况,虚拟机不是要求对象必须到达阀值才可晋升老年代的,而是采用动态年龄判定的方法:如果Servivor空间中相同年龄的对象大小大于Servivor空间的一般时,由于下一次的MinorGC时,这些对象如果仍然存活的话,复制到ToServivor空间时就放不下了。所以,在本次GC时就可以把这些对象以及年龄大于等于这些对象的直接进入老年代。在MinorGC时,如果Eden和FromServivor中存活的对象在复制到ToServivor时放不下了,也会直接分配到老年代。
- 空间分配担保,在MinorGC之前,会先检查老年代最大可用空间是否可以容纳新生代所有对象(防止新生代全部晋升时放不下),如果可以容纳,则MinorGC可以安全执行。否则,检查是否允许担保失败,是则检查老年代最大可用空间是否大于历次晋升到老年代的对象的平均大小,是则尝试进行MinorGC;小于或者MinorGC失败,则会发起一次FullGC清理老年代。
如果读完觉得有收获的话,欢迎点赞、关注、加公众号【Java在线】,查阅更多精彩历史!!!