1. 垃圾收集器与内存分配策略
垃圾回收机制(Garbage Collection,GC),GC的历史要比java悠久。1960年诞生于MIT的Lisp是第一个真正使用内存动态分配和垃圾收集技术的语言。当时人们考虑GC需要解决三件事:
- 哪些内存需要回收
- 什么时候回收
- 如何回收
1.1 对象怎么判断是否需要回收
判断对象是否存活,如果存活则不需要回收,如果死亡则需要回收。引用计数算法和可达性分析算法是两个判断对象是否死亡的算法。
引用计数算法:就是当没有任何引用指向这个对象时,及判定此对象已死亡。但是他有缺点,当两个对象的属性分别指向两个对象,然后把两个对象的引用置为null,并且这两个对象已经没有其他引用指向这两个对象。此时这两个对象是已经死亡,但是他们又有其属性指向对方,所以引用计数并非为0。下面举个列子:
/** *testGC()方法执行后,objA和objB会不会被GC呢?
*
*/
public class ReferenceCountingGC{
public Object instance=null;
private static final int_1MB=1024*1024;
/**
*这个成员属性的唯一意义就是占点内存,以便能在GC日志中看清楚是否被
*回收过
*/
private byte[]bigSize=new byte[2*_1MB];
public static void testGC(){
ReferenceCountingGC objA=new ReferenceCountingGC();
ReferenceCountingGC objB=new ReferenceCountingGC();
objA.instance=objB;
objB.instance=objA;
objA=null;
objB=null;
//假设在这行发生GC,objA和objB是否能被回收?
System.gc();
} }
可达性分析算法:现在一些主流的语言判断对象是否已死都是通过可达性分析算法实现的。这个算法的实现思路:通过一系列的”GC Roots”为起点,从这个节点往下收索,节点直接的路径称为引用链,假如有些对象没有与”GC Roots”起始节点联系,则为对象不可达,既判断这个对象已死。在java语言中有哪些对象可以作为”GC Roots”呢?
- 虚拟机栈(栈帧中的本地变量表)中引用的对象.
- 方法区静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈Native中引用的对象
因为Object4、Object5、Object6是不可达的,所以就认为这三个对象以及死亡.Object1、Object2、Object3是可达的,所以这三个对象还还存活。
1.2 引用
自jdk 1.2 以后,java将引用分为:强引用(Strong Reference),软引用(Soft Reference),弱引用(Weak Reference),虚引用(Phantom Reference),这4种的引用强度依次逐渐减弱。
强引用: Object obj=new Object(); new出来的对象都是强引用。只要强引用存在,垃圾收集器就不会回收被引用的对象。
软引用:用于还有些用但并非必需的对象。系统在发生内存溢出之前,会把这种类型的对象进行二次回收。如果这次回收还没有足够的内存,则系统会抛出内存溢出的异常。在JDK1.2之后,提供了SoftReference类来实现软引用。
弱引用:用来描述非必需的对象,它的强度比软引用的强度弱一些。当下一次垃圾收集器发生的时候,无论内存是否只够都会回收掉只被弱引用的对象。在JDK1.2之后,提供了WeakReference类来实现弱引用。
虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引 用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一 个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。在 JDK 1.2之后,提供了PhantomReference类来实现虚引用。
强:String abc=new String(“abc”);
软:SoftReference<String> sr=new SoftReference<String>(“abc”);
弱: WeakReference<String> abcWeakRef = new WeakReference<String>(abc);
1.3 垃圾收集算法
标记-清除算法:算法分为”标记”和”清除”两个阶段。首先标记出需要回收的对象,标记分为两次轮询,第一次会执行finalize(),当这个方法执行后该对象仍然处于回收的队列则,第二次轮询该对象就会被回收。在这个方法中对象也可以自救。标记说完了就说一下清除,当确认这个对象已死,这系统就会把这个对象回收了称为清除。这个算法有两个问题: 1标记和清除的效率都很低2.空间问题,对象被清除后,空间就会变的不连续,从而大大的损耗了系统内存。
1.4 复制算法
为了解决效率问题,复制算法把空间划分为等同的两部分,一部分作为预留,一部分作为存储。复制算法的思路是把存活的对象直接复制到另一个预留区域,然后把这个存储区域直接清空。存储区域变成了预留区域,预留区域变成了存储区域。从未节省了空间。
现在的商业虚拟机都采用这种收集算法来回收新生代,IBM公司的专门研究表明,新生 代中的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存 分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor [1]。 当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最 后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是 8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10% 的内存会被“浪费”。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每 次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里 指老年代)进行分配担保(Handle Promotion)。
1.5 标记-整顿算法
我更喜欢称他为:标记-清除-整顿算法。正如他的名字一样,他是比标记,清除算法多了一个整顿的操作,复制算法适合与新生代,存活的对象比较少,这样效率自然高。但却不适合老年代,老年代每次回收的对象少存活的多,这样使用复制算法效率就会很低,这个时候就适合标记整顿算法。