对象已死吗
要进行垃圾回收首先需要判断对象是否还活着,当前主要有两种算法:引用计数算法和可达性分析算法。引用计数算法思想是给对象添加一个引用计数器,当有一个地方引用它时,计数器加1,当引用失效时,计数器减1,当计数器为0时对象就死掉了。这种方法用于python语言,存在的问题是解决不了循环引用的问题。可达性分析算法的思想是以一系列“GC Roots”对象为起始点,从这些节点向下搜索,搜索走过的路径为引用链,当一个对象到“GC Roots”对象没有任何引用链相连时就死掉了。这种方法用于java语言。
引用可以分为强引用、软引用、弱引用、虚引用。
垃圾收集算法
1、标记清除算法。首先标记所有需要回收的对象,在标记完成后统一回收所有被标记的对象。该算法存在两个问题:效率不高;会产生内存碎片。
2、复制算法。将堆内存划分为Eden空间和两个survivor空间,HotSpot默认Eden和Survivor比例大小为8:1,每次使用Eden和其中的一块Survivor,回收时将还活着的对象复制到另一块Survivor空间上,最后清理Eden和刚才用过的Survivor空间。当Survivor空间不够用时就根据分配担保机制放入老年代。
3、标记整理算法。思路与标记清除算法相似,只是回收时是让所有活着的对象向一端移动,然后清理掉边界以外的内存。
4、分代收集算法。一般将Java堆分为新生代和老年代,对象都在新生代出生,熬过几轮GC后仍没有死掉就进老年代。新生代一般死掉的概率较大适合用复制算法,老年代稳定性高一般用标记整理算法。
HotSpot算法实现
可达性分析算法需要从"GC Roots"节点找引用链,可作为“GC Roots”的节点主要在常量、静态属性、栈帧中的本地变量表,逐个检查需要消耗很多时间。HotSpot使用一组称为OopMap的数据结构在类加载完成时就把对象内什么偏移量上是什么类型的数据记录下来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用,这样GC扫描时就能直接得到这些信息了。
程序执行时并非在所有地方都可以GC,而是在特定的位置才记录当前的OopMap内容,这称为安全点,只有在安全点才可以GC。有两种方法使发生GC时所有的线程都跑到最近的安全点上停顿:抢先式中断和主动式中断。
对于处于睡眠和阻塞的线程无法响应中断,所以就设置了安全区域,安全区域是引用关系不会发生变化的一段代码,在这个区域中GC是安全的,线程进入安全区域时会进行标识,出安全区域时会检查是否完成了GC。
垃圾搜集器
1、Serial收集器。这是一个单线程收集器,GC时需要暂停其他所有线程。对于运行在Client模式下的虚拟机是一个很好的选择。
2、ParNew收集器。这是Serial收集器的多线程版本,是在Server模式下的虚拟机中首选的新生代收集器。
3、Parallel Scavenge收集器。它的目标是达到一个可控制的吞吐量,高吞吐量可以高效率的利用CPU时间,以尽快完成程序运算任务,适合在后台运算而不需要太多交互的任务。
4、Serial Old收集器。它是Serial的老年代版本,是单线程收集器,给Client模式下的虚拟机使用。
5、Parallel Old收集器。它是Parallel Scavenge的老年代版本,与Parallel Scavenge搭配使用,适合注重吞吐量以及CPU资源敏感的场合。
6、CMS收集器。它以获取最短回收停顿时间为目标。可分为4个步骤:初始标记;并发标记;重新标记;并发清除。初始标记、重新标记还需要停止其他线程,初始标记是标记GC Roots能直接关联到的对象,重新标记是修正并发标记期间因用户程序继续运作而导致标记变动的对象的标记记录,并发标记、并发清除可以与用户线程一起工作。它有三个缺点:对CPU资源非常敏感;无法处理浮动垃圾;会产生内存碎片。
7、G1收集器是面向服务端应用的垃圾收集器。它具备并行与并发;分代收集;空间整合;可预测的停顿等特点。
内存分配
大多数情况下,对象在新生代Eden区中分配,当Eden区没有足够空间时会发生一次Minor GC,大对象会直接在老年代分配,因为Full GC太慢,程序中应尽量避免大量的短命大对象。新生代中的对象有一个年龄计数器,每当熬过一次GC就加1,当到达一定年龄时就移入老年代。如果Survivor空间中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。