垃圾收集器与内存分配策略
程序计数器、虚拟机栈、本地方法栈三个区域随线程生灭,栈中的栈帧的随着方法进入/退出,且分配的内存大小在类结构确定后就已知,因此这些区域的内存分配回收确定。需要考虑CG的是Java堆和方法区,一个接口中的多个实现类需要的内存不同,方法的多个分支需要的内存也不同。因此这部分是垃圾收集器关注的。
1.确认对象“死活”
垃圾收集器回收“死去”的对象,因此需要判断对象的“死活”,方法:1.引用计数法 2.可达性分析算法
1.1.引用计数法
给对象添加引用计数器,有个一地方引用它,计数+1,引用失效计数-1,计数为0的对象不可能再被使用。
缺点:难以解决对象循环引用的问题(如下例子)
public class ReferenceCountingGc{ public Object instance = null; private static final int _iMB = 1024*1024; private byte[] bigSize = new byte[2*_1MB];// 占内存用,以便在GC日志中看是否被回收 public static void testGC(){ ReferenceCountingGC objA = new ReferenceCountingGC(); ReferenceCountingGC objB = new ReferenceCountingGC(); objA.instance = objB; objB.instance = objA; System.gc(); // 由于两个对象互相引用,因此引用计数不为0,无法回收 } }
1.2.可达性分析算法
在Java语言中,可以作为GCRoots的对象包括下面几种:
- 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。
- 方法区中的类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI(Native方法)引用的对象。
若不可达对象覆盖了finalize方法或其已被虚拟机调用过,则对象将逃脱一次“死亡”,只需将其与引用链上任一对象建立关联即可在第二次GC标记时免于“死亡”。
1.3.引用
原先的引用是对象只有引用与被引用两种状态,对于一些不太“重要”的对象来说就过于僵硬了,因此通过添加强引用、软引用、弱引用、虚引用方法使得内存空间若足够,就保留在内存中,若GC后内存不足就抛弃。
强引用:如Object obj =- new Object()
软引用:在发生内存溢出之前,将其回收
弱引用:弱引用的对象只能活到下一次GC过程之前,不论内存足够与否,都回收掉
虚引用:虚引用的存在不影响其生存时间。仅用来当对象被收集器回收后收到系统通知。
1.4.方法区回收
判断常量是否“废弃常量”的方法:
- 类的所有实例都被回收,Java堆中不存在该类的任何实例
- 加载的ClassLoader被回收
- 该类的java.lang.Class对象未在任何地方被引用(包括反射)
例:String对象“abc”进入常量池,但没有任何String对象引用这个常量,则发生内存回收时会回收这个对象
2.垃圾收集算法
2.1.标记—清楚算法
算法分为“标记”和“清除”两个阶段。标记阶段将需要回收的对象标记,在清除阶段进行清除
缺点:1.标记与清除的效率都不高。2.清除后会产生大量不连续的碎片,当需要分配较大的对象时,会提前触发另一次垃圾收集
2.2.复制算法(基于2.1)(常用)
将容量分为等量的两块,每次用其中的一块,当内存用完了,把还存在的对象复制到另一块上,再把已经使用过的内存空间一次清理掉,因此每次对整个半区进行内存回收
优点:效率高
缺点:内存搜小为了原来的一半,复制操作效率低
现代的商用虚拟机采用此种方法。由于98%的对象“朝生夕死”,因此内存分为较大的Eden空间和较小的Survivor空间。回收时,将Eden和Survivor中还活着的对象复制到另一块Survivor中,然后清除之前用的空间。若Survivor空间不够用,用老年代进行担保,对象直接通过分配担保机制进入老年代。
2.3.标记-整理算法(基于2.1)
相比标记—清除,在清除过程中不对可回收对象进行清理,而是让存活的对象向一端进行移动,而后清理掉边界外的内存。
2.4.分代收集算法
根据对象存活周期不同,将内存分为几块(新生代/老生代),根据每个年代的特点采用适合的算法。如死得多的用复制算法,活的多的用2.1或2.2
3.内存分配回收策略
总的说,在堆上分配,主要分配在新生代的Eden上,若Eden区没有足够空间,发生MinorGC(新生代的GC)发生,若还不够,将原有的对象担保到老年区去。
大对象进入老年代
长期存活的对象进入老年代
PS:老年代GC相较Minor GC慢10倍以上(Full GC/Major GC)